// Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package runtimeutil import ( "bytes" "fmt" "io" "io/ioutil" "path" "strings" "sigs.k8s.io/kustomize/kyaml/comments" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/kio/kioutil" "sigs.k8s.io/kustomize/kyaml/order" "sigs.k8s.io/kustomize/kyaml/yaml" ) // FunctionFilter wraps another filter to be invoked in the context of a function. // FunctionFilter manages scoping the function, deferring failures, and saving results // to files. type FunctionFilter struct { // Run implements the function. Run func(reader io.Reader, writer io.Writer) error // FunctionConfig is passed to the function through ResourceList.functionConfig. FunctionConfig *yaml.RNode `yaml:"functionConfig,omitempty"` // GlobalScope explicitly scopes the function to all input resources rather than only those // resources scoped to it by path. GlobalScope bool // ResultsFile is the file to write function ResourceList.results to. // If unset, results will not be written. ResultsFile string // DeferFailure will cause the Filter to return a nil error even if Run returns an error. // The Run error will be available through GetExit(). DeferFailure bool // results saves the results emitted from Run Results *yaml.RNode // exit saves the error returned from Run exit error ids map[string]*yaml.RNode } // GetExit returns the error from Run func (c FunctionFilter) GetExit() error { return c.exit } // functionsDirectoryName is keyword directory name for functions scoped 1 directory higher const functionsDirectoryName = "functions" // getFunctionScope returns the path of the directory containing the function config, // or its parent directory if the base directory is named "functions" func (c *FunctionFilter) getFunctionScope() (string, error) { m, err := c.FunctionConfig.GetMeta() if err != nil { return "", errors.Wrap(err) } var p string var found bool p, found = m.Annotations[kioutil.PathAnnotation] if !found { p, found = m.Annotations[kioutil.LegacyPathAnnotation] if !found { return "", nil } } functionDir := path.Clean(path.Dir(p)) if path.Base(functionDir) == functionsDirectoryName { // the scope of functions in a directory called "functions" is 1 level higher // this is similar to how the golang "internal" directory scoping works functionDir = path.Dir(functionDir) } return functionDir, nil } // scope partitions the input nodes into 2 slices. The first slice contains only Resources // which are scoped under dir, and the second slice contains the Resources which are not. func (c *FunctionFilter) scope(dir string, nodes []*yaml.RNode) ([]*yaml.RNode, []*yaml.RNode, error) { // scope container filtered Resources to Resources under that directory var input, saved []*yaml.RNode if c.GlobalScope { return nodes, nil, nil } // global function if dir == "" || dir == "." { return nodes, nil, nil } // identify Resources read from directories under the function configuration for i := range nodes { m, err := nodes[i].GetMeta() if err != nil { return nil, nil, err } var p string var found bool p, found = m.Annotations[kioutil.PathAnnotation] if !found { p, found = m.Annotations[kioutil.LegacyPathAnnotation] if !found { // this Resource isn't scoped under the function -- don't know where it came from // consider it out of scope saved = append(saved, nodes[i]) continue } } resourceDir := path.Clean(path.Dir(p)) if path.Base(resourceDir) == functionsDirectoryName { // Functions in the `functions` directory are scoped to // themselves, and should see themselves as input resourceDir = path.Dir(resourceDir) } if !strings.HasPrefix(resourceDir, dir) { // this Resource doesn't fall under the function scope if it // isn't in a subdirectory of where the function lives saved = append(saved, nodes[i]) continue } // this input is scoped under the function input = append(input, nodes[i]) } return input, saved, nil } func (c *FunctionFilter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { in := &bytes.Buffer{} out := &bytes.Buffer{} // only process Resources scoped to this function, save the others functionDir, err := c.getFunctionScope() if err != nil { return nil, err } input, saved, err := c.scope(functionDir, nodes) if err != nil { return nil, err } // set ids on each input so it is possible to copy comments from inputs back to outputs if err := c.setIds(input); err != nil { return nil, err } // write the input err = kio.ByteWriter{ WrappingAPIVersion: kio.ResourceListAPIVersion, WrappingKind: kio.ResourceListKind, Writer: in, KeepReaderAnnotations: true, FunctionConfig: c.FunctionConfig}.Write(input) if err != nil { return nil, err } // capture the command stdout for the return value r := &kio.ByteReader{Reader: out} // don't exit immediately if the function fails -- write out the validation c.exit = c.Run(in, out) output, err := r.Read() if err != nil { return nil, err } // copy the comments and sync the order of fields from the inputs to the outputs if err := c.copyCommentsAndSyncOrder(output); err != nil { return nil, err } if err := c.doResults(r); err != nil { return nil, err } if c.exit != nil && !c.DeferFailure { return append(output, saved...), c.exit } // annotate any generated Resources with a path and index if they don't already have one if err := kioutil.DefaultPathAnnotation(functionDir, output); err != nil { return nil, err } // emit both the Resources output from the function, and the out-of-scope Resources // which were not provided to the function return append(output, saved...), nil } func (c *FunctionFilter) setIds(nodes []*yaml.RNode) error { // set the id on each node to map inputs to outputs var id int c.ids = map[string]*yaml.RNode{} for i := range nodes { id++ idStr := fmt.Sprintf("%v", id) err := nodes[i].PipeE(yaml.SetAnnotation(kioutil.IdAnnotation, idStr)) if err != nil { return errors.Wrap(err) } err = nodes[i].PipeE(yaml.SetAnnotation(kioutil.LegacyIdAnnotation, idStr)) if err != nil { return errors.Wrap(err) } c.ids[idStr] = nodes[i] } return nil } func (c *FunctionFilter) copyCommentsAndSyncOrder(nodes []*yaml.RNode) error { for i := range nodes { node := nodes[i] anID, err := node.Pipe(yaml.GetAnnotation(kioutil.IdAnnotation)) if err != nil { return errors.Wrap(err) } if anID == nil { anID, err = node.Pipe(yaml.GetAnnotation(kioutil.LegacyIdAnnotation)) if err != nil { return errors.Wrap(err) } if anID == nil { continue } } var in *yaml.RNode var found bool if in, found = c.ids[anID.YNode().Value]; !found { continue } if err := comments.CopyComments(in, node); err != nil { return errors.Wrap(err) } if err := order.SyncOrder(in, node); err != nil { return errors.Wrap(err) } if err := node.PipeE(yaml.ClearAnnotation(kioutil.IdAnnotation)); err != nil { return errors.Wrap(err) } if err := node.PipeE(yaml.ClearAnnotation(kioutil.LegacyIdAnnotation)); err != nil { return errors.Wrap(err) } } return nil } func (c *FunctionFilter) doResults(r *kio.ByteReader) error { // Write the results to a file if configured to do so if c.ResultsFile != "" && r.Results != nil { results, err := r.Results.String() if err != nil { return err } err = ioutil.WriteFile(c.ResultsFile, []byte(results), 0600) if err != nil { return err } } if r.Results != nil { c.Results = r.Results } return nil }