github.com/GoogleContainerTools/kpt@v1.0.0-beta.50.0.20240520170205-c25345ffcbee/thirdparty/kyaml/runfn/runfn.go (about)

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package runfn
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/user"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"github.com/GoogleContainerTools/kpt/internal/pkg"
    16  	"github.com/GoogleContainerTools/kpt/pkg/printer"
    17  	"sigs.k8s.io/kustomize/kyaml/errors"
    18  	"sigs.k8s.io/kustomize/kyaml/filesys"
    19  	"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
    20  	"sigs.k8s.io/kustomize/kyaml/kio"
    21  	"sigs.k8s.io/kustomize/kyaml/yaml"
    22  
    23  	"github.com/GoogleContainerTools/kpt/internal/fnruntime"
    24  	"github.com/GoogleContainerTools/kpt/internal/types"
    25  	"github.com/GoogleContainerTools/kpt/internal/util/printerutil"
    26  	fnresult "github.com/GoogleContainerTools/kpt/pkg/api/fnresult/v1"
    27  	kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
    28  )
    29  
    30  // RunFns runs the set of configuration functions in a local directory against
    31  // the Resources in that directory
    32  type RunFns struct {
    33  	Ctx context.Context
    34  
    35  	StorageMounts []runtimeutil.StorageMount
    36  
    37  	// Path is the path to the directory containing functions
    38  	Path string
    39  
    40  	// uniquePath is the absolute version of Path
    41  	uniquePath types.UniquePath
    42  
    43  	// FnConfigPath specifies a config file which contains the configs used in
    44  	// function input. It can be absolute or relative to kpt working directory.
    45  	// The exact format depends on the OS.
    46  	FnConfigPath string
    47  
    48  	// Function is an function to run against the input.
    49  	Function *runtimeutil.FunctionSpec
    50  
    51  	// FnConfig is the configurations passed from command line
    52  	FnConfig *yaml.RNode
    53  
    54  	// Input can be set to read the Resources from Input rather than from a directory
    55  	Input io.Reader
    56  
    57  	// Network enables network access for functions that declare it
    58  	Network bool
    59  
    60  	// Output can be set to write the result to Output rather than back to the directory
    61  	Output io.Writer
    62  
    63  	// ResultsDir is where to write each functions results
    64  	ResultsDir string
    65  
    66  	fnResults *fnresult.ResultList
    67  
    68  	// functionFilterProvider provides a filter to perform the function.
    69  	// this is a variable so it can be mocked in tests
    70  	functionFilterProvider func(
    71  		filter runtimeutil.FunctionSpec, fnConfig *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error)
    72  
    73  	// AsCurrentUser is a boolean to indicate whether docker container should use
    74  	// the uid and gid that run the command
    75  	AsCurrentUser bool
    76  
    77  	// Env contains environment variables that will be exported to container
    78  	Env []string
    79  
    80  	// ContinueOnEmptyResult configures what happens when the underlying pipeline
    81  	// returns an empty result.
    82  	// If it is false (default), subsequent functions will be skipped and the
    83  	// result will be returned immediately.
    84  	// If it is true, the empty result will be provided as input to the next
    85  	// function in the list.
    86  	ContinueOnEmptyResult bool
    87  
    88  	RunnerOptions fnruntime.RunnerOptions
    89  
    90  	// ExecArgs are the arguments for exec commands
    91  	ExecArgs []string
    92  
    93  	// OriginalExec is the original exec commands
    94  	OriginalExec string
    95  
    96  	Selector kptfile.Selector
    97  
    98  	Exclusion kptfile.Selector
    99  }
   100  
   101  // Execute runs the command
   102  func (r RunFns) Execute() error {
   103  	// default the containerFilterProvider if it hasn't been override.  Split out for testing.
   104  	err := (&r).init()
   105  	if err != nil {
   106  		return err
   107  	}
   108  	nodes, fltrs, output, err := r.getNodesAndFilters()
   109  	if err != nil {
   110  		return err
   111  	}
   112  	return r.runFunctions(nodes, output, fltrs)
   113  }
   114  
   115  func (r RunFns) getNodesAndFilters() (
   116  	*kio.PackageBuffer, []kio.Filter, *kio.LocalPackageReadWriter, error) {
   117  	// Read Resources from Directory or Input
   118  	buff := &kio.PackageBuffer{}
   119  	p := kio.Pipeline{Outputs: []kio.Writer{buff}}
   120  	// save the output dir because we will need it to write back
   121  	// the same one for reading must be used for writing if deleting Resources
   122  	var outputPkg *kio.LocalPackageReadWriter
   123  
   124  	if r.Path != "" {
   125  		outputPkg = &kio.LocalPackageReadWriter{
   126  			PackagePath:        string(r.uniquePath),
   127  			MatchFilesGlob:     pkg.MatchAllKRM,
   128  			PreserveSeqIndent:  true,
   129  			PackageFileName:    kptfile.KptFileName,
   130  			IncludeSubpackages: true,
   131  			WrapBareSeqNode:    true,
   132  		}
   133  	}
   134  
   135  	if r.Input == nil {
   136  		p.Inputs = []kio.Reader{outputPkg}
   137  	} else {
   138  		p.Inputs = []kio.Reader{&kio.ByteReader{Reader: r.Input, PreserveSeqIndent: true, WrapBareSeqNode: true}}
   139  	}
   140  	if err := p.Execute(); err != nil {
   141  		return nil, nil, outputPkg, err
   142  	}
   143  
   144  	fltrs, err := r.getFilters()
   145  	if err != nil {
   146  		return nil, nil, outputPkg, err
   147  	}
   148  	return buff, fltrs, outputPkg, nil
   149  }
   150  
   151  func (r RunFns) getFilters() ([]kio.Filter, error) {
   152  	spec := r.Function
   153  	if spec == nil {
   154  		return nil, nil
   155  	}
   156  	// merge envs from imperative and declarative
   157  	spec.Container.Env = r.mergeContainerEnv(spec.Container.Env)
   158  
   159  	c, err := r.functionFilterProvider(*spec, r.FnConfig, user.Current)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	if c == nil {
   165  		return nil, nil
   166  	}
   167  	return []kio.Filter{c}, nil
   168  }
   169  
   170  // runFunctions runs the fltrs against the input and writes to either r.Output or output
   171  func (r RunFns) runFunctions(input kio.Reader, output kio.Writer, fltrs []kio.Filter) error {
   172  	// use the previously read Resources as input
   173  	var outputs []kio.Writer
   174  	if r.Output == nil {
   175  		// write back to the package
   176  		outputs = append(outputs, output)
   177  	} else {
   178  		// write to the output instead of the directory if r.Output is specified or
   179  		// the output is nil (reading from Input)
   180  		outputs = append(outputs, kio.ByteWriter{
   181  			Writer:                r.Output,
   182  			KeepReaderAnnotations: true,
   183  			WrappingKind:          kio.ResourceListKind,
   184  			WrappingAPIVersion:    kio.ResourceListAPIVersion,
   185  		})
   186  	}
   187  
   188  	inputResources, err := input.Read()
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	selectedInput := inputResources
   194  
   195  	if !r.Selector.IsEmpty() || !r.Exclusion.IsEmpty() {
   196  		err = fnruntime.SetResourceIds(inputResources)
   197  		if err != nil {
   198  			return err
   199  		}
   200  
   201  		// select the resources on which function should be applied
   202  		selectedInput, err = fnruntime.SelectInput(
   203  			inputResources,
   204  			[]kptfile.Selector{r.Selector},
   205  			[]kptfile.Selector{r.Exclusion},
   206  			&fnruntime.SelectionContext{RootPackagePath: r.uniquePath})
   207  		if err != nil {
   208  			return err
   209  		}
   210  	}
   211  
   212  	pb := &kio.PackageBuffer{}
   213  	pipeline := kio.Pipeline{
   214  		Inputs:                []kio.Reader{&kio.PackageBuffer{Nodes: selectedInput}},
   215  		Filters:               fltrs,
   216  		Outputs:               []kio.Writer{pb},
   217  		ContinueOnEmptyResult: r.ContinueOnEmptyResult,
   218  	}
   219  	err = pipeline.Execute()
   220  	outputResources := pb.Nodes
   221  
   222  	if !r.Selector.IsEmpty() || !r.Exclusion.IsEmpty() {
   223  		outputResources = fnruntime.MergeWithInput(pb.Nodes, selectedInput, inputResources)
   224  		deleteAnnoErr := fnruntime.DeleteResourceIds(outputResources)
   225  		if deleteAnnoErr != nil {
   226  			return deleteAnnoErr
   227  		}
   228  	}
   229  
   230  	if err == nil {
   231  		writeErr := outputs[0].Write(outputResources)
   232  		if writeErr != nil {
   233  			return writeErr
   234  		}
   235  	}
   236  	resultsFile, resultErr := fnruntime.SaveResults(filesys.FileSystemOrOnDisk{}, r.ResultsDir, r.fnResults)
   237  	if err != nil {
   238  		// function fails
   239  		if resultErr == nil {
   240  			r.printFnResultsStatus(resultsFile)
   241  		}
   242  		return err
   243  	}
   244  	if resultErr == nil {
   245  		r.printFnResultsStatus(resultsFile)
   246  	}
   247  	return nil
   248  }
   249  
   250  func (r RunFns) printFnResultsStatus(resultsFile string) {
   251  	printerutil.PrintFnResultInfo(r.Ctx, resultsFile, true)
   252  }
   253  
   254  // mergeContainerEnv will merge the envs specified by command line (imperative) and config
   255  // file (declarative). If they have same key, the imperative value will be respected.
   256  func (r RunFns) mergeContainerEnv(envs []string) []string {
   257  	imperative := fnruntime.NewContainerEnvFromStringSlice(r.Env)
   258  	declarative := fnruntime.NewContainerEnvFromStringSlice(envs)
   259  	for key, value := range imperative.EnvVars {
   260  		declarative.AddKeyValue(key, value)
   261  	}
   262  
   263  	for _, key := range imperative.VarsToExport {
   264  		declarative.AddKey(key)
   265  	}
   266  
   267  	return declarative.Raw()
   268  }
   269  
   270  // init initializes the RunFns with a containerFilterProvider.
   271  func (r *RunFns) init() error {
   272  	// if no path is specified, default reading from stdin and writing to stdout
   273  	if r.Path == "" {
   274  		if r.Output == nil {
   275  			r.Output = printer.FromContextOrDie(r.Ctx).OutStream()
   276  		}
   277  		if r.Input == nil {
   278  			r.Input = os.Stdin
   279  		}
   280  	} else {
   281  		// make the path absolute so it works on mac
   282  		var err error
   283  		absPath, err := filepath.Abs(r.Path)
   284  		if err != nil {
   285  			return errors.Wrap(err)
   286  		}
   287  		r.uniquePath = types.UniquePath(absPath)
   288  	}
   289  
   290  	r.fnResults = fnresult.NewResultList()
   291  
   292  	// functionFilterProvider set the filter provider
   293  	if r.functionFilterProvider == nil {
   294  		r.functionFilterProvider = r.defaultFnFilterProvider
   295  	}
   296  
   297  	// fn config path should be absolute
   298  	if r.FnConfigPath != "" && !filepath.IsAbs(r.FnConfigPath) {
   299  		// if the FnConfigPath is relative, we should use the
   300  		// current directory to construct full path.
   301  		path, err := os.Getwd()
   302  		if err != nil {
   303  			return fmt.Errorf("failed to get working directory: %w", err)
   304  		}
   305  		r.FnConfigPath = filepath.Join(path, r.FnConfigPath)
   306  	}
   307  	return nil
   308  }
   309  
   310  type currentUserFunc func() (*user.User, error)
   311  
   312  // getUIDGID will return "nobody" if asCurrentUser is false. Otherwise
   313  // return "uid:gid" according to the return from currentUser function.
   314  func getUIDGID(asCurrentUser bool, currentUser currentUserFunc) (string, error) {
   315  	if !asCurrentUser {
   316  		return "nobody", nil
   317  	}
   318  
   319  	u, err := currentUser()
   320  	if err != nil {
   321  		return "", err
   322  	}
   323  	return fmt.Sprintf("%s:%s", u.Uid, u.Gid), nil
   324  }
   325  
   326  // getFunctionConfig returns yaml representation of functionConfig that can
   327  // be provided to a function as input.
   328  func (r *RunFns) getFunctionConfig() (*yaml.RNode, error) {
   329  	return kptfile.GetValidatedFnConfigFromPath(filesys.FileSystemOrOnDisk{}, "", r.FnConfigPath)
   330  }
   331  
   332  // defaultFnFilterProvider provides function filters
   333  func (r *RunFns) defaultFnFilterProvider(spec runtimeutil.FunctionSpec, fnConfig *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) {
   334  	if spec.Container.Image == "" && spec.Exec.Path == "" {
   335  		return nil, fmt.Errorf("either image name or executable path need to be provided")
   336  	}
   337  
   338  	var err error
   339  	if r.FnConfigPath != "" {
   340  		fnConfig, err = r.getFunctionConfig()
   341  		if err != nil {
   342  			return nil, err
   343  		}
   344  	}
   345  	fltr := &runtimeutil.FunctionFilter{
   346  		FunctionConfig: fnConfig,
   347  		DeferFailure:   spec.DeferFailure,
   348  	}
   349  	fnResult := &fnresult.Result{
   350  		// TODO(droot): This is required for making structured results subpackage aware.
   351  		// Enable this once test harness supports filepath based assertions.
   352  		// Pkg: string(r.uniquePath),
   353  	}
   354  	if spec.Container.Image != "" {
   355  		fnResult.Image = spec.Container.Image
   356  
   357  		resolvedImage, err := r.RunnerOptions.ResolveToImage(context.TODO(), spec.Container.Image)
   358  		if err != nil {
   359  			return nil, err
   360  		}
   361  		// If AllowWasm is true, we try to use the image field as a wasm image.
   362  		// TODO: we can be smarter here. If the image doesn't support wasm/js platform,
   363  		// it should fallback to run it as container fn.
   364  		if r.RunnerOptions.AllowWasm {
   365  			wFn, err := fnruntime.NewWasmFn(fnruntime.NewOciLoader(filepath.Join(os.TempDir(), "kpt-fn-wasm"), resolvedImage))
   366  			if err != nil {
   367  				return nil, err
   368  			}
   369  			fltr.Run = wFn.Run
   370  		} else {
   371  			// TODO: Add a test for this behavior
   372  			uidgid, err := getUIDGID(r.AsCurrentUser, currentUser)
   373  			if err != nil {
   374  				return nil, err
   375  			}
   376  			c := &fnruntime.ContainerFn{
   377  				Image:           resolvedImage,
   378  				ImagePullPolicy: r.RunnerOptions.ImagePullPolicy,
   379  				UIDGID:          uidgid,
   380  				StorageMounts:   r.StorageMounts,
   381  				Env:             spec.Container.Env,
   382  				FnResult:        fnResult,
   383  				Perm: fnruntime.ContainerFnPermission{
   384  					AllowNetwork: r.Network,
   385  					// mounts are always from CLI flags so we allow
   386  					// them by default for eval
   387  					AllowMount: true,
   388  				},
   389  			}
   390  			fltr.Run = c.Run
   391  		}
   392  	}
   393  
   394  	if spec.Exec.Path != "" {
   395  		fnResult.ExecPath = r.OriginalExec
   396  
   397  		if r.RunnerOptions.AllowWasm && strings.HasSuffix(spec.Exec.Path, ".wasm") {
   398  			wFn, err := fnruntime.NewWasmFn(&fnruntime.FsLoader{Filename: spec.Exec.Path})
   399  			if err != nil {
   400  				return nil, err
   401  			}
   402  			fltr.Run = wFn.Run
   403  		} else {
   404  			e := &fnruntime.ExecFn{
   405  				Path:     spec.Exec.Path,
   406  				Args:     r.ExecArgs,
   407  				FnResult: fnResult,
   408  			}
   409  			fltr.Run = e.Run
   410  		}
   411  	}
   412  
   413  	opts := r.RunnerOptions
   414  	if !r.Selector.IsEmpty() || !r.Exclusion.IsEmpty() {
   415  		opts.DisplayResourceCount = true
   416  	}
   417  
   418  	return fnruntime.NewFunctionRunner(r.Ctx, fltr, "", fnResult, r.fnResults, opts)
   419  }