github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/internal/util/render/executor.go (about)

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package render
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"github.com/GoogleContainerTools/kpt/internal/errors"
    26  	"github.com/GoogleContainerTools/kpt/internal/fnruntime"
    27  	"github.com/GoogleContainerTools/kpt/internal/pkg"
    28  	"github.com/GoogleContainerTools/kpt/internal/printer"
    29  	"github.com/GoogleContainerTools/kpt/internal/types"
    30  	"github.com/GoogleContainerTools/kpt/internal/util/attribution"
    31  	"github.com/GoogleContainerTools/kpt/internal/util/printerutil"
    32  	fnresult "github.com/GoogleContainerTools/kpt/pkg/api/fnresult/v1"
    33  	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    34  	"github.com/GoogleContainerTools/kpt/pkg/fn"
    35  	"sigs.k8s.io/kustomize/kyaml/filesys"
    36  	"sigs.k8s.io/kustomize/kyaml/kio"
    37  	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
    38  	"sigs.k8s.io/kustomize/kyaml/sets"
    39  	"sigs.k8s.io/kustomize/kyaml/yaml"
    40  )
    41  
    42  var errAllowedExecNotSpecified = fmt.Errorf("must run with `--allow-exec` option to allow running function binaries")
    43  
    44  // Renderer hydrates a given pkg by running the functions in the input pipeline
    45  type Renderer struct {
    46  	// PkgPath is the absolute path to the root package
    47  	PkgPath string
    48  
    49  	// Runtime knows how to pick a function runner for a given function
    50  	Runtime fn.FunctionRuntime
    51  
    52  	// ResultsDirPath is absolute path to the directory to write results
    53  	ResultsDirPath string
    54  
    55  	// fnResultsList is the list of results from the pipeline execution
    56  	fnResultsList *fnresult.ResultList
    57  
    58  	// Output is the writer to which the output resources are written
    59  	Output io.Writer
    60  
    61  	// RunnerOptions contains options controlling function execution.
    62  	RunnerOptions fnruntime.RunnerOptions
    63  
    64  	// FileSystem is the input filesystem to operate on
    65  	FileSystem filesys.FileSystem
    66  }
    67  
    68  // Execute runs a pipeline.
    69  func (e *Renderer) Execute(ctx context.Context) error {
    70  	const op errors.Op = "fn.render"
    71  
    72  	pr := printer.FromContextOrDie(ctx)
    73  
    74  	root, err := newPkgNode(e.FileSystem, e.PkgPath, nil)
    75  	if err != nil {
    76  		return errors.E(op, types.UniquePath(e.PkgPath), err)
    77  	}
    78  
    79  	// initialize hydration context
    80  	hctx := &hydrationContext{
    81  		root:          root,
    82  		pkgs:          map[types.UniquePath]*pkgNode{},
    83  		fnResults:     fnresult.NewResultList(),
    84  		runnerOptions: e.RunnerOptions,
    85  		fileSystem:    e.FileSystem,
    86  		runtime:       e.Runtime,
    87  	}
    88  
    89  	if _, err = hydrate(ctx, root, hctx); err != nil {
    90  		// Note(droot): ignore the error in function result saving
    91  		// to avoid masking the hydration error.
    92  		// don't disable the CLI output in case of error
    93  		_ = e.saveFnResults(ctx, hctx.fnResults)
    94  		return errors.E(op, root.pkg.UniquePath, err)
    95  	}
    96  
    97  	// adjust the relative paths of the resources.
    98  	err = adjustRelPath(hctx)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	if err = trackOutputFiles(hctx); err != nil {
   104  		return err
   105  	}
   106  
   107  	// add metrics annotation to output resources to track the usage as the resources
   108  	// are rendered by kpt fn group
   109  	at := attribution.Attributor{Resources: hctx.root.resources, CmdGroup: "fn"}
   110  	at.Process()
   111  
   112  	if e.Output == nil {
   113  		// the intent of the user is to modify resources in-place
   114  		pkgWriter := &kio.LocalPackageReadWriter{
   115  			PackagePath:        string(root.pkg.UniquePath),
   116  			PreserveSeqIndent:  true,
   117  			PackageFileName:    kptfilev1.KptFileName,
   118  			IncludeSubpackages: true,
   119  			WrapBareSeqNode:    true,
   120  			FileSystem:         filesys.FileSystemOrOnDisk{FileSystem: e.FileSystem},
   121  			MatchFilesGlob:     pkg.MatchAllKRM,
   122  		}
   123  		err = pkgWriter.Write(hctx.root.resources)
   124  		if err != nil {
   125  			return fmt.Errorf("failed to save resources: %w", err)
   126  		}
   127  
   128  		if err = pruneResources(e.FileSystem, hctx); err != nil {
   129  			return err
   130  		}
   131  		pr.Printf("Successfully executed %d function(s) in %d package(s).\n", hctx.executedFunctionCnt, len(hctx.pkgs))
   132  	} else {
   133  		// the intent of the user is to write the resources to either stdout|unwrapped|<OUT_DIR>
   134  		// so, write the resources to provided e.Output which will be written to appropriate destination by cobra layer
   135  		writer := &kio.ByteWriter{
   136  			Writer:                e.Output,
   137  			KeepReaderAnnotations: true,
   138  			WrappingAPIVersion:    kio.ResourceListAPIVersion,
   139  			WrappingKind:          kio.ResourceListKind,
   140  		}
   141  		err = writer.Write(hctx.root.resources)
   142  		if err != nil {
   143  			return fmt.Errorf("failed to write resources: %w", err)
   144  		}
   145  	}
   146  
   147  	return e.saveFnResults(ctx, hctx.fnResults)
   148  }
   149  
   150  func (e *Renderer) saveFnResults(ctx context.Context, fnResults *fnresult.ResultList) error {
   151  	e.fnResultsList = fnResults
   152  	resultsFile, err := fnruntime.SaveResults(e.FileSystem, e.ResultsDirPath, fnResults)
   153  	if err != nil {
   154  		return fmt.Errorf("failed to save function results: %w", err)
   155  	}
   156  
   157  	printerutil.PrintFnResultInfo(ctx, resultsFile, false)
   158  	return nil
   159  }
   160  
   161  // hydrationContext contains bits to track state of a package hydration.
   162  // This is sort of global state that is available to hydration step at
   163  // each pkg along the hydration walk.
   164  type hydrationContext struct {
   165  	// root points to the root pkg of hydration graph
   166  	root *pkgNode
   167  
   168  	// pkgs refers to the packages undergoing hydration. pkgs are key'd by their
   169  	// unique paths.
   170  	pkgs map[types.UniquePath]*pkgNode
   171  
   172  	// inputFiles is a set of filepaths containing input resources to the
   173  	// functions across all the packages during hydration.
   174  	// The file paths are relative to the root package.
   175  	inputFiles sets.String
   176  
   177  	// outputFiles is a set of filepaths containing output resources. This
   178  	// will be compared with the inputFiles to identify files be pruned.
   179  	outputFiles sets.String
   180  
   181  	// executedFunctionCnt is the counter for functions that have been executed.
   182  	executedFunctionCnt int
   183  
   184  	// fnResults stores function results gathered
   185  	// during pipeline execution.
   186  	fnResults *fnresult.ResultList
   187  
   188  	runnerOptions fnruntime.RunnerOptions
   189  
   190  	fileSystem filesys.FileSystem
   191  
   192  	// function runtime
   193  	runtime fn.FunctionRuntime
   194  }
   195  
   196  // pkgNode represents a package being hydrated. Think of it as a node in the hydration DAG.
   197  type pkgNode struct {
   198  	pkg *pkg.Pkg
   199  
   200  	// state indicates if the pkg is being hydrated or done.
   201  	state hydrationState
   202  
   203  	// KRM resources that we have gathered post hydration for this package.
   204  	// These inludes resources at this pkg as well all it's children.
   205  	resources []*yaml.RNode
   206  }
   207  
   208  // newPkgNode returns a pkgNode instance given a path or pkg.
   209  func newPkgNode(fsys filesys.FileSystem, path string, p *pkg.Pkg) (pn *pkgNode, err error) {
   210  	const op errors.Op = "pkg.read"
   211  
   212  	if path == "" && p == nil {
   213  		return pn, fmt.Errorf("missing package path %s or package", path)
   214  	}
   215  	if path != "" {
   216  		p, err = pkg.New(fsys, path)
   217  		if err != nil {
   218  			return pn, errors.E(op, path, err)
   219  		}
   220  	}
   221  	// Note: Ensuring the presence of Kptfile can probably be moved
   222  	// to the lower level pkg abstraction, but not sure if that
   223  	// is desired in all the cases. So revisit this.
   224  	kf, err := p.Kptfile()
   225  	if err != nil {
   226  		return pn, errors.E(op, p.UniquePath, err)
   227  	}
   228  
   229  	if err := kf.Validate(fsys, p.UniquePath); err != nil {
   230  		return pn, errors.E(op, p.UniquePath, err)
   231  	}
   232  
   233  	pn = &pkgNode{
   234  		pkg:   p,
   235  		state: Dry, // package starts in dry state
   236  	}
   237  	return pn, nil
   238  }
   239  
   240  // hydrationState represent hydration state of a pkg.
   241  type hydrationState int
   242  
   243  // constants for all the hydration states
   244  const (
   245  	Dry hydrationState = iota
   246  	Hydrating
   247  	Wet
   248  )
   249  
   250  func (s hydrationState) String() string {
   251  	return []string{"Dry", "Hydrating", "Wet"}[s]
   252  }
   253  
   254  // hydrate hydrates given pkg and returns wet resources.
   255  func hydrate(ctx context.Context, pn *pkgNode, hctx *hydrationContext) (output []*yaml.RNode, err error) {
   256  	const op errors.Op = "pkg.render"
   257  
   258  	curr, found := hctx.pkgs[pn.pkg.UniquePath]
   259  	if found {
   260  		switch curr.state {
   261  		case Hydrating:
   262  			// we detected a cycle
   263  			err = fmt.Errorf("cycle detected in pkg dependencies")
   264  			return output, errors.E(op, curr.pkg.UniquePath, err)
   265  		case Wet:
   266  			output = curr.resources
   267  			return output, nil
   268  		default:
   269  			return output, errors.E(op, curr.pkg.UniquePath,
   270  				fmt.Errorf("package found in invalid state %v", curr.state))
   271  		}
   272  	}
   273  	// add it to the discovered package list
   274  	hctx.pkgs[pn.pkg.UniquePath] = pn
   275  	curr = pn
   276  	// mark the pkg in hydrating
   277  	curr.state = Hydrating
   278  
   279  	relPath, err := curr.pkg.RelativePathTo(hctx.root.pkg)
   280  	if err != nil {
   281  		return nil, errors.E(op, curr.pkg.UniquePath, err)
   282  	}
   283  
   284  	var input []*yaml.RNode
   285  
   286  	// determine sub packages to be hydrated
   287  	subpkgs, err := curr.pkg.DirectSubpackages()
   288  	if err != nil {
   289  		return output, errors.E(op, curr.pkg.UniquePath, err)
   290  	}
   291  	// hydrate recursively and gather hydated transitive resources.
   292  	for _, subpkg := range subpkgs {
   293  		var transitiveResources []*yaml.RNode
   294  		var subPkgNode *pkgNode
   295  
   296  		if subPkgNode, err = newPkgNode(hctx.fileSystem, "", subpkg); err != nil {
   297  			return output, errors.E(op, subpkg.UniquePath, err)
   298  		}
   299  
   300  		transitiveResources, err = hydrate(ctx, subPkgNode, hctx)
   301  		if err != nil {
   302  			return output, errors.E(op, subpkg.UniquePath, err)
   303  		}
   304  
   305  		input = append(input, transitiveResources...)
   306  	}
   307  
   308  	// gather resources present at the current package
   309  	currPkgResources, err := curr.pkg.LocalResources()
   310  	if err != nil {
   311  		return output, errors.E(op, curr.pkg.UniquePath, err)
   312  	}
   313  
   314  	err = trackInputFiles(hctx, relPath, currPkgResources)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  
   319  	// include current package's resources in the input resource list
   320  	input = append(input, currPkgResources...)
   321  
   322  	output, err = curr.runPipeline(ctx, hctx, input)
   323  	if err != nil {
   324  		return output, errors.E(op, curr.pkg.UniquePath, err)
   325  	}
   326  
   327  	// pkg is hydrated, mark the pkg as wet and update the resources
   328  	curr.state = Wet
   329  	curr.resources = output
   330  
   331  	return output, err
   332  }
   333  
   334  // runPipeline runs the pipeline defined at current pkgNode on given input resources.
   335  func (pn *pkgNode) runPipeline(ctx context.Context, hctx *hydrationContext, input []*yaml.RNode) ([]*yaml.RNode, error) {
   336  	const op errors.Op = "pipeline.run"
   337  	pr := printer.FromContextOrDie(ctx)
   338  	// TODO: the DisplayPath is a relative file path. It cannot represent the
   339  	// package structure. We should have function to get the relative package
   340  	// path here.
   341  	pr.OptPrintf(printer.NewOpt().PkgDisplay(pn.pkg.DisplayPath), "\n")
   342  
   343  	pl, err := pn.pkg.Pipeline()
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  
   348  	if pl.IsEmpty() {
   349  		if err := kptfilev1.AreKRM(input); err != nil {
   350  			return nil, fmt.Errorf("input resource list must contain only KRM resources: %s", err.Error())
   351  		}
   352  		return input, nil
   353  	}
   354  
   355  	// perform runtime validation for pipeline
   356  	if err := pn.pkg.ValidatePipeline(); err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	mutatedResources, err := pn.runMutators(ctx, hctx, input)
   361  	if err != nil {
   362  		return nil, errors.E(op, pn.pkg.UniquePath, err)
   363  	}
   364  
   365  	if err = pn.runValidators(ctx, hctx, mutatedResources); err != nil {
   366  		return nil, errors.E(op, pn.pkg.UniquePath, err)
   367  	}
   368  	// print a new line after a pipeline running
   369  	pr.Printf("\n")
   370  	return mutatedResources, nil
   371  }
   372  
   373  // runMutators runs a set of mutators functions on given input resources.
   374  func (pn *pkgNode) runMutators(ctx context.Context, hctx *hydrationContext, input []*yaml.RNode) ([]*yaml.RNode, error) {
   375  	pl, err := pn.pkg.Pipeline()
   376  	if err != nil {
   377  		return nil, err
   378  	}
   379  
   380  	if len(pl.Mutators) == 0 {
   381  		return input, nil
   382  	}
   383  
   384  	mutators, err := fnChain(ctx, hctx, pn.pkg.UniquePath, pl.Mutators)
   385  	if err != nil {
   386  		return nil, err
   387  	}
   388  
   389  	for i, mutator := range mutators {
   390  		if pl.Mutators[i].ConfigPath != "" {
   391  			// kpt v1.0.0-beta15+ onwards, functionConfigs are included in the
   392  			// function inputs during `render` and as a result, they can be
   393  			// mutated during the `render`.
   394  			// So functionConfigs needs be updated in the FunctionRunner instance
   395  			// before every run.
   396  			for _, r := range input {
   397  				pkgPath, err := pkg.GetPkgPathAnnotation(r)
   398  				if err != nil {
   399  					return nil, err
   400  				}
   401  				currPath, _, err := kioutil.GetFileAnnotations(r)
   402  				if err != nil {
   403  					return nil, err
   404  				}
   405  				if pkgPath == pn.pkg.UniquePath.String() && // resource belong to current package
   406  					currPath == pl.Mutators[i].ConfigPath { // configPath matches
   407  					mutator.SetFnConfig(r)
   408  					continue
   409  				}
   410  			}
   411  		}
   412  
   413  		selectors := pl.Mutators[i].Selectors
   414  		exclusions := pl.Mutators[i].Exclusions
   415  
   416  		if len(selectors) > 0 || len(exclusions) > 0 {
   417  			// set kpt-resource-id annotation on each resource before mutation
   418  			err = fnruntime.SetResourceIds(input)
   419  			if err != nil {
   420  				return nil, err
   421  			}
   422  		}
   423  		// select the resources on which function should be applied
   424  		selectedInput, err := fnruntime.SelectInput(input, selectors, exclusions, &fnruntime.SelectionContext{RootPackagePath: hctx.root.pkg.UniquePath})
   425  		if err != nil {
   426  			return nil, err
   427  		}
   428  		output := &kio.PackageBuffer{}
   429  		// create a kio pipeline from kyaml library to execute the function chains
   430  		mutation := kio.Pipeline{
   431  			Inputs: []kio.Reader{
   432  				&kio.PackageBuffer{Nodes: selectedInput},
   433  			},
   434  			Filters: []kio.Filter{mutator},
   435  			Outputs: []kio.Writer{output},
   436  		}
   437  		err = mutation.Execute()
   438  		if err != nil {
   439  			return nil, err
   440  		}
   441  		hctx.executedFunctionCnt++
   442  
   443  		if len(selectors) > 0 || len(exclusions) > 0 {
   444  			// merge the output resources with input resources
   445  			input = fnruntime.MergeWithInput(output.Nodes, selectedInput, input)
   446  			// delete the kpt-resource-id annotation on each resource
   447  			err = fnruntime.DeleteResourceIds(input)
   448  			if err != nil {
   449  				return nil, err
   450  			}
   451  		} else {
   452  			input = output.Nodes
   453  		}
   454  	}
   455  	return input, nil
   456  }
   457  
   458  // runValidators runs a set of validator functions on input resources.
   459  // We bail out on first validation failure today, but the logic can be
   460  // improved to report multiple failures. Reporting multiple failures
   461  // will require changes to the way we print errors
   462  func (pn *pkgNode) runValidators(ctx context.Context, hctx *hydrationContext, input []*yaml.RNode) error {
   463  	pl, err := pn.pkg.Pipeline()
   464  	if err != nil {
   465  		return err
   466  	}
   467  
   468  	if len(pl.Validators) == 0 {
   469  		return nil
   470  	}
   471  
   472  	for i := range pl.Validators {
   473  		function := pl.Validators[i]
   474  		// validators are run on a copy of mutated resources to ensure
   475  		// resources are not mutated.
   476  		selectedResources, err := fnruntime.SelectInput(input, function.Selectors, function.Exclusions, &fnruntime.SelectionContext{RootPackagePath: hctx.root.pkg.UniquePath})
   477  		if err != nil {
   478  			return err
   479  		}
   480  		var validator kio.Filter
   481  		displayResourceCount := false
   482  		if len(function.Selectors) > 0 || len(function.Exclusions) > 0 {
   483  			displayResourceCount = true
   484  		}
   485  		if function.Exec != "" && !hctx.runnerOptions.AllowExec {
   486  			return errAllowedExecNotSpecified
   487  		}
   488  		opts := hctx.runnerOptions
   489  		opts.SetPkgPathAnnotation = true
   490  		opts.DisplayResourceCount = displayResourceCount
   491  		validator, err = fnruntime.NewRunner(ctx, hctx.fileSystem, &function, pn.pkg.UniquePath, hctx.fnResults, opts, hctx.runtime)
   492  		if err != nil {
   493  			return err
   494  		}
   495  		if _, err = validator.Filter(cloneResources(selectedResources)); err != nil {
   496  			return err
   497  		}
   498  		hctx.executedFunctionCnt++
   499  	}
   500  	return nil
   501  }
   502  
   503  func cloneResources(input []*yaml.RNode) (output []*yaml.RNode) {
   504  	for _, resource := range input {
   505  		output = append(output, resource.Copy())
   506  	}
   507  	return
   508  }
   509  
   510  // path (location) of a KRM resources is tracked in a special key in
   511  // metadata.annotation field that is used to write the resources to the filesystem.
   512  // When resources are read from local filesystem or generated at a package level, the
   513  // path annotation in a resource points to path relative to that package. But the resources
   514  // are written to the file system at the root package level, so
   515  // the path annotation in each resources needs to be adjusted to be relative to the rootPkg.
   516  // adjustRelPath updates the path annotation by prepending the path of the package
   517  // relative to the root package.
   518  func adjustRelPath(hctx *hydrationContext) error {
   519  	resources := hctx.root.resources
   520  	for _, r := range resources {
   521  		pkgPath, err := pkg.GetPkgPathAnnotation(r)
   522  		if err != nil {
   523  			return err
   524  		}
   525  		// Note: kioutil.GetFileAnnotation returns OS specific
   526  		// paths today, https://github.com/kubernetes-sigs/kustomize/issues/3749
   527  		currPath, _, err := kioutil.GetFileAnnotations(r)
   528  		if err != nil {
   529  			return err
   530  		}
   531  		newPath, err := pathRelToRoot(string(hctx.root.pkg.UniquePath), pkgPath, currPath)
   532  		if err != nil {
   533  			return err
   534  		}
   535  		// in kyaml v0.12.0, we are supporting both the new path annotation key
   536  		// internal.config.kubernetes.io/path, as well as the legacy one config.kubernetes.io/path
   537  		if err = r.PipeE(yaml.SetAnnotation(kioutil.PathAnnotation, newPath)); err != nil {
   538  			return err
   539  		}
   540  		if err = r.PipeE(yaml.SetAnnotation(kioutil.LegacyPathAnnotation, newPath)); err != nil { // nolint:staticcheck
   541  			return err
   542  		}
   543  		if err = pkg.RemovePkgPathAnnotation(r); err != nil {
   544  			return err
   545  		}
   546  	}
   547  	return nil
   548  }
   549  
   550  // pathRelToRoot computes resource's path relative to root package given:
   551  // rootPkgPath: absolute path to the root package
   552  // subpkgPath: absolute path to subpackage
   553  // resourcePath: resource's path relative to the subpackage
   554  // All the inputs paths are assumed to be OS specific.
   555  func pathRelToRoot(rootPkgPath, subPkgPath, resourcePath string) (relativePath string, err error) {
   556  	if !filepath.IsAbs(rootPkgPath) {
   557  		return "", fmt.Errorf("root package path %q must be absolute", rootPkgPath)
   558  	}
   559  
   560  	if !filepath.IsAbs(subPkgPath) {
   561  		return "", fmt.Errorf("subpackage path %q must be absolute", subPkgPath)
   562  	}
   563  
   564  	if subPkgPath == "" {
   565  		// empty subpackage path means resource belongs to the root package
   566  		return resourcePath, nil
   567  	}
   568  
   569  	// subpackage's path relative to the root package
   570  	subPkgRelPath, err := filepath.Rel(rootPkgPath, subPkgPath)
   571  	if err != nil {
   572  		return "", fmt.Errorf("subpackage %q must be relative to %q: %w",
   573  			rootPkgPath, subPkgPath, err)
   574  	}
   575  	// Note: Rel("/tmp", "/a") = "../", which isn't valid for our use-case.
   576  	dotdot := ".." + string(os.PathSeparator)
   577  	if strings.HasPrefix(subPkgRelPath, dotdot) || subPkgRelPath == ".." {
   578  		return "", fmt.Errorf("subpackage %q is not a descendant of %q", subPkgPath, rootPkgPath)
   579  	}
   580  	relativePath = filepath.Join(subPkgRelPath, filepath.Clean(resourcePath))
   581  	return relativePath, nil
   582  }
   583  
   584  // fnChain returns a slice of function runners given a list of functions defined in pipeline.
   585  func fnChain(ctx context.Context, hctx *hydrationContext, pkgPath types.UniquePath, fns []kptfilev1.Function) ([]*fnruntime.FunctionRunner, error) {
   586  	var runners []*fnruntime.FunctionRunner
   587  	for i := range fns {
   588  		var err error
   589  		var runner *fnruntime.FunctionRunner
   590  		function := fns[i]
   591  		displayResourceCount := false
   592  		if len(function.Selectors) > 0 || len(function.Exclusions) > 0 {
   593  			displayResourceCount = true
   594  		}
   595  		if function.Exec != "" && !hctx.runnerOptions.AllowExec {
   596  			return nil, errAllowedExecNotSpecified
   597  		}
   598  		opts := hctx.runnerOptions
   599  		opts.SetPkgPathAnnotation = true
   600  		opts.DisplayResourceCount = displayResourceCount
   601  		runner, err = fnruntime.NewRunner(ctx, hctx.fileSystem, &function, pkgPath, hctx.fnResults, opts, hctx.runtime)
   602  		if err != nil {
   603  			return nil, err
   604  		}
   605  		runners = append(runners, runner)
   606  	}
   607  	return runners, nil
   608  }
   609  
   610  // trackInputFiles records file paths of input resources in the hydration context.
   611  func trackInputFiles(hctx *hydrationContext, relPath string, input []*yaml.RNode) error {
   612  	if hctx.inputFiles == nil {
   613  		hctx.inputFiles = sets.String{}
   614  	}
   615  	for _, r := range input {
   616  		path, _, err := kioutil.GetFileAnnotations(r)
   617  		if err != nil {
   618  			return fmt.Errorf("path annotation missing: %w", err)
   619  		}
   620  		path = filepath.Join(relPath, filepath.Clean(path))
   621  		hctx.inputFiles.Insert(path)
   622  	}
   623  	return nil
   624  }
   625  
   626  // trackOutputFiles records the file paths of output resources in the hydration
   627  // context. It should be invoked post hydration.
   628  func trackOutputFiles(hctx *hydrationContext) error {
   629  	outputSet := sets.String{}
   630  
   631  	for _, r := range hctx.root.resources {
   632  		path, _, err := kioutil.GetFileAnnotations(r)
   633  		if err != nil {
   634  			return fmt.Errorf("path annotation missing: %w", err)
   635  		}
   636  		outputSet.Insert(path)
   637  	}
   638  	hctx.outputFiles = outputSet
   639  	return nil
   640  }
   641  
   642  // pruneResources compares the input and output of the hydration and prunes
   643  // resources that are no longer present in the output of the hydration.
   644  func pruneResources(fsys filesys.FileSystem, hctx *hydrationContext) error {
   645  	filesToBeDeleted := hctx.inputFiles.Difference(hctx.outputFiles)
   646  	for f := range filesToBeDeleted {
   647  		if err := fsys.RemoveAll(filepath.Join(string(hctx.root.pkg.UniquePath), f)); err != nil {
   648  			return fmt.Errorf("failed to delete file: %w", err)
   649  		}
   650  	}
   651  	return nil
   652  }