...

Source file src/sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil/runtimeutil.go

Documentation: sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package runtimeutil
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path"
    12  	"strings"
    13  
    14  	"sigs.k8s.io/kustomize/kyaml/comments"
    15  	"sigs.k8s.io/kustomize/kyaml/errors"
    16  	"sigs.k8s.io/kustomize/kyaml/kio"
    17  	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
    18  	"sigs.k8s.io/kustomize/kyaml/order"
    19  
    20  	"sigs.k8s.io/kustomize/kyaml/yaml"
    21  )
    22  
    23  // FunctionFilter wraps another filter to be invoked in the context of a function.
    24  // FunctionFilter manages scoping the function, deferring failures, and saving results
    25  // to files.
    26  type FunctionFilter struct {
    27  	// Run implements the function.
    28  	Run func(reader io.Reader, writer io.Writer) error
    29  
    30  	// FunctionConfig is passed to the function through ResourceList.functionConfig.
    31  	FunctionConfig *yaml.RNode `yaml:"functionConfig,omitempty"`
    32  
    33  	// GlobalScope explicitly scopes the function to all input resources rather than only those
    34  	// resources scoped to it by path.
    35  	GlobalScope bool
    36  
    37  	// ResultsFile is the file to write function ResourceList.results to.
    38  	// If unset, results will not be written.
    39  	ResultsFile string
    40  
    41  	// DeferFailure will cause the Filter to return a nil error even if Run returns an error.
    42  	// The Run error will be available through GetExit().
    43  	DeferFailure bool
    44  
    45  	// results saves the results emitted from Run
    46  	Results *yaml.RNode
    47  
    48  	// exit saves the error returned from Run
    49  	exit error
    50  
    51  	ids map[string]*yaml.RNode
    52  }
    53  
    54  // GetExit returns the error from Run
    55  func (c FunctionFilter) GetExit() error {
    56  	return c.exit
    57  }
    58  
    59  // functionsDirectoryName is keyword directory name for functions scoped 1 directory higher
    60  const functionsDirectoryName = "functions"
    61  
    62  // getFunctionScope returns the path of the directory containing the function config,
    63  // or its parent directory if the base directory is named "functions"
    64  func (c *FunctionFilter) getFunctionScope() (string, error) {
    65  	m, err := c.FunctionConfig.GetMeta()
    66  	if err != nil {
    67  		return "", errors.Wrap(err)
    68  	}
    69  	var p string
    70  	var found bool
    71  	p, found = m.Annotations[kioutil.PathAnnotation]
    72  	if !found {
    73  		p, found = m.Annotations[kioutil.LegacyPathAnnotation]
    74  		if !found {
    75  			return "", nil
    76  		}
    77  	}
    78  
    79  	functionDir := path.Clean(path.Dir(p))
    80  
    81  	if path.Base(functionDir) == functionsDirectoryName {
    82  		// the scope of functions in a directory called "functions" is 1 level higher
    83  		// this is similar to how the golang "internal" directory scoping works
    84  		functionDir = path.Dir(functionDir)
    85  	}
    86  	return functionDir, nil
    87  }
    88  
    89  // scope partitions the input nodes into 2 slices.  The first slice contains only Resources
    90  // which are scoped under dir, and the second slice contains the Resources which are not.
    91  func (c *FunctionFilter) scope(dir string, nodes []*yaml.RNode) ([]*yaml.RNode, []*yaml.RNode, error) {
    92  	// scope container filtered Resources to Resources under that directory
    93  	var input, saved []*yaml.RNode
    94  	if c.GlobalScope {
    95  		return nodes, nil, nil
    96  	}
    97  
    98  	// global function
    99  	if dir == "" || dir == "." {
   100  		return nodes, nil, nil
   101  	}
   102  
   103  	// identify Resources read from directories under the function configuration
   104  	for i := range nodes {
   105  		m, err := nodes[i].GetMeta()
   106  		if err != nil {
   107  			return nil, nil, err
   108  		}
   109  		var p string
   110  		var found bool
   111  		p, found = m.Annotations[kioutil.PathAnnotation]
   112  		if !found {
   113  			p, found = m.Annotations[kioutil.LegacyPathAnnotation]
   114  			if !found {
   115  				// this Resource isn't scoped under the function -- don't know where it came from
   116  				// consider it out of scope
   117  				saved = append(saved, nodes[i])
   118  				continue
   119  			}
   120  		}
   121  
   122  		resourceDir := path.Clean(path.Dir(p))
   123  		if path.Base(resourceDir) == functionsDirectoryName {
   124  			// Functions in the `functions` directory are scoped to
   125  			// themselves, and should see themselves as input
   126  			resourceDir = path.Dir(resourceDir)
   127  		}
   128  		if !strings.HasPrefix(resourceDir, dir) {
   129  			// this Resource doesn't fall under the function scope if it
   130  			// isn't in a subdirectory of where the function lives
   131  			saved = append(saved, nodes[i])
   132  			continue
   133  		}
   134  
   135  		// this input is scoped under the function
   136  		input = append(input, nodes[i])
   137  	}
   138  
   139  	return input, saved, nil
   140  }
   141  
   142  func (c *FunctionFilter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
   143  	in := &bytes.Buffer{}
   144  	out := &bytes.Buffer{}
   145  
   146  	// only process Resources scoped to this function, save the others
   147  	functionDir, err := c.getFunctionScope()
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	input, saved, err := c.scope(functionDir, nodes)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	// set ids on each input so it is possible to copy comments from inputs back to outputs
   157  	if err := c.setIds(input); err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	// write the input
   162  	err = kio.ByteWriter{
   163  		WrappingAPIVersion:    kio.ResourceListAPIVersion,
   164  		WrappingKind:          kio.ResourceListKind,
   165  		Writer:                in,
   166  		KeepReaderAnnotations: true,
   167  		FunctionConfig:        c.FunctionConfig}.Write(input)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	// capture the command stdout for the return value
   173  	r := &kio.ByteReader{Reader: out}
   174  
   175  	// don't exit immediately if the function fails -- write out the validation
   176  	c.exit = c.Run(in, out)
   177  
   178  	output, err := r.Read()
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	// copy the comments and sync the order of fields from the inputs to the outputs
   184  	if err := c.copyCommentsAndSyncOrder(output); err != nil {
   185  		return nil, err
   186  	}
   187  
   188  	if err := c.doResults(r); err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	if c.exit != nil && !c.DeferFailure {
   193  		return append(output, saved...), c.exit
   194  	}
   195  
   196  	// annotate any generated Resources with a path and index if they don't already have one
   197  	if err := kioutil.DefaultPathAnnotation(functionDir, output); err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	// emit both the Resources output from the function, and the out-of-scope Resources
   202  	// which were not provided to the function
   203  	return append(output, saved...), nil
   204  }
   205  
   206  func (c *FunctionFilter) setIds(nodes []*yaml.RNode) error {
   207  	// set the id on each node to map inputs to outputs
   208  	var id int
   209  	c.ids = map[string]*yaml.RNode{}
   210  	for i := range nodes {
   211  		id++
   212  		idStr := fmt.Sprintf("%v", id)
   213  		err := nodes[i].PipeE(yaml.SetAnnotation(kioutil.IdAnnotation, idStr))
   214  		if err != nil {
   215  			return errors.Wrap(err)
   216  		}
   217  		err = nodes[i].PipeE(yaml.SetAnnotation(kioutil.LegacyIdAnnotation, idStr))
   218  		if err != nil {
   219  			return errors.Wrap(err)
   220  		}
   221  		c.ids[idStr] = nodes[i]
   222  	}
   223  	return nil
   224  }
   225  
   226  func (c *FunctionFilter) copyCommentsAndSyncOrder(nodes []*yaml.RNode) error {
   227  	for i := range nodes {
   228  		node := nodes[i]
   229  		anID, err := node.Pipe(yaml.GetAnnotation(kioutil.IdAnnotation))
   230  		if err != nil {
   231  			return errors.Wrap(err)
   232  		}
   233  		if anID == nil {
   234  			anID, err = node.Pipe(yaml.GetAnnotation(kioutil.LegacyIdAnnotation))
   235  			if err != nil {
   236  				return errors.Wrap(err)
   237  			}
   238  			if anID == nil {
   239  				continue
   240  			}
   241  		}
   242  
   243  		var in *yaml.RNode
   244  		var found bool
   245  		if in, found = c.ids[anID.YNode().Value]; !found {
   246  			continue
   247  		}
   248  		if err := comments.CopyComments(in, node); err != nil {
   249  			return errors.Wrap(err)
   250  		}
   251  		if err := order.SyncOrder(in, node); err != nil {
   252  			return errors.Wrap(err)
   253  		}
   254  		if err := node.PipeE(yaml.ClearAnnotation(kioutil.IdAnnotation)); err != nil {
   255  			return errors.Wrap(err)
   256  		}
   257  		if err := node.PipeE(yaml.ClearAnnotation(kioutil.LegacyIdAnnotation)); err != nil {
   258  			return errors.Wrap(err)
   259  		}
   260  	}
   261  	return nil
   262  }
   263  
   264  func (c *FunctionFilter) doResults(r *kio.ByteReader) error {
   265  	// Write the results to a file if configured to do so
   266  	if c.ResultsFile != "" && r.Results != nil {
   267  		results, err := r.Results.String()
   268  		if err != nil {
   269  			return err
   270  		}
   271  		err = os.WriteFile(c.ResultsFile, []byte(results), 0600)
   272  		if err != nil {
   273  			return err
   274  		}
   275  	}
   276  
   277  	if r.Results != nil {
   278  		c.Results = r.Results
   279  	}
   280  	return nil
   281  }
   282  

View as plain text