github.com/GoogleContainerTools/kpt@v1.0.0-beta.50.0.20240520170205-c25345ffcbee/internal/fnruntime/runner.go (about)

     1  // Copyright 2021 The kpt Authors
     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 fnruntime
    16  
    17  import (
    18  	"context"
    19  	goerrors "errors"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"path"
    24  	"path/filepath"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/GoogleContainerTools/kpt/internal/builtins"
    29  	"github.com/GoogleContainerTools/kpt/internal/errors"
    30  	"github.com/GoogleContainerTools/kpt/internal/pkg"
    31  	"github.com/GoogleContainerTools/kpt/internal/types"
    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  	"github.com/GoogleContainerTools/kpt/pkg/printer"
    36  	"github.com/google/shlex"
    37  	"sigs.k8s.io/kustomize/kyaml/filesys"
    38  	"sigs.k8s.io/kustomize/kyaml/fn/framework"
    39  	"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
    40  	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
    41  	"sigs.k8s.io/kustomize/kyaml/yaml"
    42  )
    43  
    44  const (
    45  	FuncGenPkgContext = "builtins/gen-pkg-context"
    46  )
    47  
    48  type RunnerOptions struct {
    49  	// ImagePullPolicy controls the image pulling behavior before running the container.
    50  	ImagePullPolicy ImagePullPolicy
    51  
    52  	// when set to true, function runner will set the package path annotation
    53  	// on resources that do not have it set. The resources generated by
    54  	// functions do not have this annotation set.
    55  	SetPkgPathAnnotation bool
    56  
    57  	DisplayResourceCount bool
    58  
    59  	// allowExec determines if function binary executable are allowed
    60  	// to be run during pipeline execution. Running function binaries is a
    61  	// privileged operation, so explicit permission is required.
    62  	AllowExec bool
    63  
    64  	// AllowNetwork specifies if container based functions are allowed
    65  	// to access network during pipeline execution. Accessing network is
    66  	// considered a privileged operation (and makes render operation non-hermetic),
    67  	// so explicit permission is desired.
    68  	AllowNetwork bool
    69  
    70  	// allowWasm determines if function wasm are allowed to be run during pipeline
    71  	// execution. Running wasm function is an alpha feature, so it needs to be
    72  	// enabled explicitly.
    73  	AllowWasm bool
    74  
    75  	// ResolveToImage will resolve a partial image to a fully-qualified one
    76  	ResolveToImage ImageResolveFunc
    77  }
    78  
    79  // ImageResolveFunc is the type for a function that can resolve a partial image to a (more) fully-qualified name
    80  type ImageResolveFunc func(ctx context.Context, image string) (string, error)
    81  
    82  func (o *RunnerOptions) InitDefaults() {
    83  	o.ImagePullPolicy = IfNotPresentPull
    84  	o.ResolveToImage = ResolveToImageForCLI
    85  }
    86  
    87  // NewRunner returns a FunctionRunner given a specification of a function
    88  // and it's config.
    89  func NewRunner(
    90  	ctx context.Context,
    91  	fsys filesys.FileSystem,
    92  	f *kptfilev1.Function,
    93  	pkgPath types.UniquePath,
    94  	fnResults *fnresult.ResultList,
    95  	opts RunnerOptions,
    96  	runtime fn.FunctionRuntime,
    97  ) (*FunctionRunner, error) {
    98  	config, err := newFnConfig(fsys, f, pkgPath)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	if f.Image != "" {
   103  		img, err := opts.ResolveToImage(ctx, f.Image)
   104  		if err != nil {
   105  			return nil, err
   106  		}
   107  		f.Image = img
   108  	}
   109  
   110  	fnResult := &fnresult.Result{
   111  		Image:    f.Image,
   112  		ExecPath: f.Exec,
   113  		// TODO(droot): This is required for making structured results subpackage aware.
   114  		// Enable this once test harness supports filepath based assertions.
   115  		// Pkg: string(pkgPath),
   116  	}
   117  
   118  	fltr := &runtimeutil.FunctionFilter{
   119  		FunctionConfig: config,
   120  		// by default, the inner most runtimeutil.FunctionFilter scopes resources to the
   121  		// directory specified by the functionConfig, kpt v1+ doesn't scope resources
   122  		// during function execution, so marking the scope to global.
   123  		// See https://github.com/GoogleContainerTools/kpt/issues/3230 for more details.
   124  		GlobalScope: true,
   125  	}
   126  
   127  	if runtime != nil {
   128  		if runner, err := runtime.GetRunner(ctx, f); err != nil {
   129  			return nil, fmt.Errorf("function runtime failed to evaluate function %q: %w", f.Image, err)
   130  		} else if runner != nil {
   131  			fltr.Run = runner.Run
   132  		}
   133  	}
   134  	if fltr.Run == nil {
   135  		if f.Image == FuncGenPkgContext {
   136  			pkgCtxGenerator := &builtins.PackageContextGenerator{}
   137  			fltr.Run = pkgCtxGenerator.Run
   138  		} else {
   139  			switch {
   140  			case f.Image != "":
   141  				// If allowWasm is true, we will use wasm runtime for image field.
   142  				if opts.AllowWasm {
   143  					wFn, err := NewWasmFn(NewOciLoader(filepath.Join(os.TempDir(), "kpt-fn-wasm"), f.Image))
   144  					if err != nil {
   145  						return nil, err
   146  					}
   147  					fltr.Run = wFn.Run
   148  				} else {
   149  					cfn := &ContainerFn{
   150  						Image:           f.Image,
   151  						ImagePullPolicy: opts.ImagePullPolicy,
   152  						Perm: ContainerFnPermission{
   153  							AllowNetwork: opts.AllowNetwork,
   154  							// mounts are disabled for render operations (currently)
   155  							// but it may change in the future.
   156  							// AllowMount: true,
   157  						},
   158  						Ctx:      ctx,
   159  						FnResult: fnResult,
   160  					}
   161  					fltr.Run = cfn.Run
   162  				}
   163  			case f.Exec != "":
   164  				// If AllowWasm is true, we will use wasm runtime for exec field.
   165  				if opts.AllowWasm {
   166  					wFn, err := NewWasmFn(&FsLoader{Filename: f.Exec})
   167  					if err != nil {
   168  						return nil, err
   169  					}
   170  					fltr.Run = wFn.Run
   171  				} else {
   172  					var execArgs []string
   173  					// assuming exec here
   174  					s, err := shlex.Split(f.Exec)
   175  					if err != nil {
   176  						return nil, fmt.Errorf("exec command %q must be valid: %w", f.Exec, err)
   177  					}
   178  					execPath := f.Exec
   179  					if len(s) > 0 {
   180  						execPath = s[0]
   181  					}
   182  					if len(s) > 1 {
   183  						execArgs = s[1:]
   184  					}
   185  					eFn := &ExecFn{
   186  						Path:     execPath,
   187  						Args:     execArgs,
   188  						FnResult: fnResult,
   189  					}
   190  					fltr.Run = eFn.Run
   191  				}
   192  			default:
   193  				return nil, fmt.Errorf("must specify `exec` or `image` to execute a function")
   194  			}
   195  		}
   196  	}
   197  	return NewFunctionRunner(ctx, fltr, pkgPath, fnResult, fnResults, opts)
   198  }
   199  
   200  // NewFunctionRunner returns a FunctionRunner given a specification of a function
   201  // and it's config.
   202  func NewFunctionRunner(ctx context.Context,
   203  	fltr *runtimeutil.FunctionFilter,
   204  	pkgPath types.UniquePath,
   205  	fnResult *fnresult.Result,
   206  	fnResults *fnresult.ResultList,
   207  	opts RunnerOptions) (*FunctionRunner, error) {
   208  	name := fnResult.Image
   209  	if name == "" {
   210  		name = fnResult.ExecPath
   211  	}
   212  	// by default, the inner most runtimeutil.FunctionFilter scopes resources to the
   213  	// directory specified by the functionConfig, kpt v1+ doesn't scope resources
   214  	// during function execution, so marking the scope to global.
   215  	// See https://github.com/GoogleContainerTools/kpt/issues/3230 for more details.
   216  	fltr.GlobalScope = true
   217  	return &FunctionRunner{
   218  		ctx:       ctx,
   219  		name:      name,
   220  		pkgPath:   pkgPath,
   221  		filter:    fltr,
   222  		fnResult:  fnResult,
   223  		fnResults: fnResults,
   224  		opts:      opts,
   225  	}, nil
   226  }
   227  
   228  // FunctionRunner wraps FunctionFilter and implements kio.Filter interface.
   229  type FunctionRunner struct {
   230  	ctx              context.Context
   231  	name             string
   232  	pkgPath          types.UniquePath
   233  	disableCLIOutput bool
   234  	filter           *runtimeutil.FunctionFilter
   235  	fnResult         *fnresult.Result
   236  	fnResults        *fnresult.ResultList
   237  	opts             RunnerOptions
   238  }
   239  
   240  func (fr *FunctionRunner) Filter(input []*yaml.RNode) (output []*yaml.RNode, err error) {
   241  	pr := printer.FromContextOrDie(fr.ctx)
   242  	if !fr.disableCLIOutput {
   243  		if fr.opts.AllowWasm {
   244  			pr.Printf("[RUNNING] WASM %q", fr.name)
   245  		} else {
   246  			pr.Printf("[RUNNING] %q", fr.name)
   247  		}
   248  		if fr.opts.DisplayResourceCount {
   249  			pr.Printf(" on %d resource(s)", len(input))
   250  		}
   251  		pr.Printf("\n")
   252  	}
   253  	t0 := time.Now()
   254  	output, err = fr.do(input)
   255  	if err != nil {
   256  		printOpt := printer.NewOpt()
   257  		pr.OptPrintf(printOpt, "[FAIL] %q in %v\n", fr.name, time.Since(t0).Truncate(time.Millisecond*100))
   258  		printFnResult(fr.ctx, fr.fnResult, printOpt)
   259  		var fnErr *ExecError
   260  		if goerrors.As(err, &fnErr) {
   261  			printFnExecErr(fr.ctx, fnErr)
   262  			return nil, errors.ErrAlreadyHandled
   263  		}
   264  		return nil, err
   265  	}
   266  	if !fr.disableCLIOutput {
   267  		pr.Printf("[PASS] %q in %v\n", fr.name, time.Since(t0).Truncate(time.Millisecond*100))
   268  		printFnResult(fr.ctx, fr.fnResult, printer.NewOpt())
   269  		printFnStderr(fr.ctx, fr.fnResult.Stderr)
   270  	}
   271  	return output, err
   272  }
   273  
   274  // SetFnConfig updates the functionConfig for the FunctionRunner instance.
   275  func (fr *FunctionRunner) SetFnConfig(conf *yaml.RNode) {
   276  	fr.filter.FunctionConfig = conf
   277  }
   278  
   279  // do executes the kpt function and returns the modified resources.
   280  // fnResult is updated with the function results returned by the kpt function.
   281  func (fr *FunctionRunner) do(input []*yaml.RNode) (output []*yaml.RNode, err error) {
   282  	if krmErr := kptfilev1.AreKRM(input); krmErr != nil {
   283  		return output, fmt.Errorf("input resource list must contain only KRM resources: %s", krmErr.Error())
   284  	}
   285  
   286  	fnResult := fr.fnResult
   287  	output, err = fr.filter.Filter(input)
   288  
   289  	if fr.opts.SetPkgPathAnnotation {
   290  		if pkgPathErr := setPkgPathAnnotationIfNotExist(output, fr.pkgPath); pkgPathErr != nil {
   291  			return output, pkgPathErr
   292  		}
   293  	}
   294  	if pathErr := enforcePathInvariants(output); pathErr != nil {
   295  		return output, pathErr
   296  	}
   297  	if krmErr := kptfilev1.AreKRM(output); krmErr != nil {
   298  		return output, fmt.Errorf("output resource list must contain only KRM resources: %s", krmErr.Error())
   299  	}
   300  
   301  	// parse the results irrespective of the success/failure of fn exec
   302  	resultErr := parseStructuredResult(fr.filter.Results, fnResult)
   303  	if resultErr != nil {
   304  		// Not sure if it's a good idea. This may mask the original
   305  		// function exec error. Revisit this if this turns out to be true.
   306  		return output, resultErr
   307  	}
   308  	if err != nil {
   309  		var execErr *ExecError
   310  		// set exitCode to non-zero by default in case of an error.
   311  		// It will be overridden with appropriate exitCode if the function runtime returns execError.
   312  		// builtinruntime and podEvaluator function runtime do not return execError so having
   313  		// a default is important.
   314  		fnResult.ExitCode = 1
   315  		fr.fnResults.ExitCode = 1
   316  		if goerrors.As(err, &execErr) {
   317  			fnResult.ExitCode = execErr.ExitCode
   318  			fnResult.Stderr = execErr.Stderr
   319  		}
   320  		// accumulate the results
   321  		fr.fnResults.Items = append(fr.fnResults.Items, *fnResult)
   322  		return output, err
   323  	}
   324  	fnResult.ExitCode = 0
   325  	fr.fnResults.Items = append(fr.fnResults.Items, *fnResult)
   326  	return output, nil
   327  }
   328  
   329  func setPkgPathAnnotationIfNotExist(resources []*yaml.RNode, pkgPath types.UniquePath) error {
   330  	for _, r := range resources {
   331  		currPkgPath, err := pkg.GetPkgPathAnnotation(r)
   332  		if err != nil {
   333  			return err
   334  		}
   335  		if currPkgPath == "" {
   336  			if err = pkg.SetPkgPathAnnotation(r, pkgPath); err != nil {
   337  				return err
   338  			}
   339  		}
   340  	}
   341  	return nil
   342  }
   343  
   344  func parseStructuredResult(yml *yaml.RNode, fnResult *fnresult.Result) error {
   345  	if yml.IsNilOrEmpty() {
   346  		return nil
   347  	}
   348  	// Note: TS SDK and Go SDK implements two different formats for the
   349  	// result. Go SDK wraps result items while TS SDK doesn't. So examine
   350  	// if items are wrapped or not to support both the formats for now.
   351  	// Refer to https://github.com/GoogleContainerTools/kpt/pull/1923#discussion_r628604165
   352  	// for some more details.
   353  	if yml.YNode().Kind == yaml.MappingNode {
   354  		// check if legacy structured result wraps ResultItems
   355  		itemsNode, err := yml.Pipe(yaml.Lookup("items"))
   356  		if err != nil {
   357  			return err
   358  		}
   359  		if !itemsNode.IsNilOrEmpty() {
   360  			// if legacy structured result, uplift the items
   361  			yml = itemsNode
   362  		}
   363  	}
   364  	err := yaml.Unmarshal([]byte(yml.MustString()), &fnResult.Results)
   365  	if err != nil {
   366  		return err
   367  	}
   368  
   369  	return migrateLegacyResult(yml, fnResult)
   370  }
   371  
   372  // migrateLegacyResult populates name and namespace in fnResult.Result if a
   373  // function (e.g. using kyaml Go SDKs) gives results in a schema
   374  // that puts a resourceRef's name and namespace under a metadata field
   375  // TODO: fix upstream (https://github.com/GoogleContainerTools/kpt/issues/2091)
   376  func migrateLegacyResult(yml *yaml.RNode, fnResult *fnresult.Result) error {
   377  	items, err := yml.Elements()
   378  	if err != nil {
   379  		return err
   380  	}
   381  
   382  	for i := range items {
   383  		if err = populateResourceRef(items[i], fnResult.Results[i]); err != nil {
   384  			return err
   385  		}
   386  		if err = populateProposedValue(items[i], fnResult.Results[i]); err != nil {
   387  			return err
   388  		}
   389  	}
   390  
   391  	return nil
   392  }
   393  
   394  func populateProposedValue(item *yaml.RNode, resultItem *framework.Result) error {
   395  	sv, err := item.Pipe(yaml.Lookup("field", "suggestedValue"))
   396  	if err != nil {
   397  		return err
   398  	}
   399  	if sv == nil {
   400  		return nil
   401  	}
   402  	if resultItem.Field == nil {
   403  		resultItem.Field = &framework.Field{}
   404  	}
   405  	resultItem.Field.ProposedValue = sv
   406  	return nil
   407  }
   408  
   409  func populateResourceRef(item *yaml.RNode, resultItem *framework.Result) error {
   410  	r, err := item.Pipe(yaml.Lookup("resourceRef", "metadata"))
   411  	if err != nil {
   412  		return err
   413  	}
   414  	if r == nil {
   415  		return nil
   416  	}
   417  	nameNode, err := r.Pipe(yaml.Lookup("name"))
   418  	if err != nil {
   419  		return err
   420  	}
   421  	namespaceNode, err := r.Pipe(yaml.Lookup("namespace"))
   422  	if err != nil {
   423  		return err
   424  	}
   425  	if nameNode != nil {
   426  		resultItem.ResourceRef.Name = strings.TrimSpace(nameNode.MustString())
   427  	}
   428  	if namespaceNode != nil {
   429  		namespace := strings.TrimSpace(namespaceNode.MustString())
   430  		if namespace != "" && namespace != "''" {
   431  			resultItem.ResourceRef.Namespace = strings.TrimSpace(namespace)
   432  		}
   433  	}
   434  	return nil
   435  }
   436  
   437  // printFnResult prints given function result in a user friendly
   438  // format on kpt CLI.
   439  func printFnResult(ctx context.Context, fnResult *fnresult.Result, opt *printer.Options) {
   440  	pr := printer.FromContextOrDie(ctx)
   441  	if len(fnResult.Results) > 0 {
   442  		// function returned structured results
   443  		var lines []string
   444  		for _, item := range fnResult.Results {
   445  			lines = append(lines, item.String())
   446  		}
   447  		ri := &MultiLineFormatter{
   448  			Title:          "Results",
   449  			Lines:          lines,
   450  			TruncateOutput: printer.TruncateOutput,
   451  		}
   452  		pr.OptPrintf(opt, "%s", ri.String())
   453  	}
   454  }
   455  
   456  // printFnExecErr prints given ExecError in a user friendly format
   457  // on kpt CLI.
   458  func printFnExecErr(ctx context.Context, fnErr *ExecError) {
   459  	pr := printer.FromContextOrDie(ctx)
   460  	printFnStderr(ctx, fnErr.Stderr)
   461  	pr.Printf("  Exit code: %d\n\n", fnErr.ExitCode)
   462  }
   463  
   464  // printFnStderr prints given stdErr in a user friendly format on kpt CLI.
   465  func printFnStderr(ctx context.Context, stdErr string) {
   466  	pr := printer.FromContextOrDie(ctx)
   467  	if len(stdErr) > 0 {
   468  		errLines := &MultiLineFormatter{
   469  			Title:          "Stderr",
   470  			Lines:          strings.Split(stdErr, "\n"),
   471  			UseQuote:       true,
   472  			TruncateOutput: printer.TruncateOutput,
   473  		}
   474  		pr.Printf("%s", errLines.String())
   475  	}
   476  }
   477  
   478  // path (location) of a KRM resources is tracked in a special key in
   479  // metadata.annotation field. enforcePathInvariants throws an error if there is a path
   480  // to a file outside the package, or if the same index/path is on multiple resources
   481  func enforcePathInvariants(nodes []*yaml.RNode) error {
   482  	// map has structure pkgPath-->path -> index -> bool
   483  	// to keep track of paths and indexes found
   484  	pkgPaths := make(map[string]map[string]map[string]bool)
   485  	for _, node := range nodes {
   486  		pkgPath, err := pkg.GetPkgPathAnnotation(node)
   487  		if err != nil {
   488  			return err
   489  		}
   490  		if pkgPaths[pkgPath] == nil {
   491  			pkgPaths[pkgPath] = make(map[string]map[string]bool)
   492  		}
   493  		currPath, index, err := kioutil.GetFileAnnotations(node)
   494  		if err != nil {
   495  			return err
   496  		}
   497  		fp := path.Clean(currPath)
   498  		if strings.HasPrefix(fp, "../") {
   499  			return fmt.Errorf("function must not modify resources outside of package: resource has path %s", currPath)
   500  		}
   501  		if pkgPaths[pkgPath][fp] == nil {
   502  			pkgPaths[pkgPath][fp] = make(map[string]bool)
   503  		}
   504  		if _, ok := pkgPaths[pkgPath][fp][index]; ok {
   505  			return fmt.Errorf("resource at path %q and index %q already exists", fp, index)
   506  		}
   507  		pkgPaths[pkgPath][fp][index] = true
   508  	}
   509  	return nil
   510  }
   511  
   512  // MultiLineFormatter knows how to format multiple lines in pretty format
   513  // that can be displayed to an end user.
   514  type MultiLineFormatter struct {
   515  	// Title under which lines need to be printed
   516  	Title string
   517  
   518  	// Lines to be printed on the CLI.
   519  	Lines []string
   520  
   521  	// TruncateOuput determines if output needs to be truncated or not.
   522  	TruncateOutput bool
   523  
   524  	// MaxLines to be printed if truncation is enabled.
   525  	MaxLines int
   526  
   527  	// UseQuote determines if line needs to be quoted or not
   528  	UseQuote bool
   529  }
   530  
   531  // String returns multiline string.
   532  func (ri *MultiLineFormatter) String() string {
   533  	if ri.MaxLines == 0 {
   534  		ri.MaxLines = FnExecErrorTruncateLines
   535  	}
   536  	strInterpolator := "%s"
   537  	if ri.UseQuote {
   538  		strInterpolator = "%q"
   539  	}
   540  
   541  	var b strings.Builder
   542  
   543  	b.WriteString(fmt.Sprintf("  %s:\n", ri.Title))
   544  	lineIndent := strings.Repeat(" ", FnExecErrorIndentation+2)
   545  	if !ri.TruncateOutput {
   546  		// stderr string should have indentations
   547  		for _, s := range ri.Lines {
   548  			// suppress newlines to avoid poor formatting
   549  			s = strings.ReplaceAll(s, "\n", " ")
   550  			b.WriteString(fmt.Sprintf(lineIndent+strInterpolator+"\n", s))
   551  		}
   552  		return b.String()
   553  	}
   554  	printedLines := 0
   555  	for i, s := range ri.Lines {
   556  		if i >= ri.MaxLines {
   557  			break
   558  		}
   559  		// suppress newlines to avoid poor formatting
   560  		s = strings.ReplaceAll(s, "\n", " ")
   561  		b.WriteString(fmt.Sprintf(lineIndent+strInterpolator+"\n", s))
   562  		printedLines++
   563  	}
   564  	truncatedLines := len(ri.Lines) - printedLines
   565  	if truncatedLines > 0 {
   566  		b.WriteString(fmt.Sprintf(lineIndent+"...(%d line(s) truncated, use '--truncate-output=false' to disable)\n", truncatedLines))
   567  	}
   568  	return b.String()
   569  }
   570  
   571  func newFnConfig(fsys filesys.FileSystem, f *kptfilev1.Function, pkgPath types.UniquePath) (*yaml.RNode, error) {
   572  	const op errors.Op = "fn.readConfig"
   573  	fn := errors.Fn(f.Image)
   574  
   575  	var node *yaml.RNode
   576  	switch {
   577  	case f.ConfigPath != "":
   578  		path := filepath.Join(string(pkgPath), f.ConfigPath)
   579  		file, err := fsys.Open(path)
   580  		if err != nil {
   581  			return nil, errors.E(op, fn,
   582  				fmt.Errorf("missing function config %q", f.ConfigPath))
   583  		}
   584  		defer file.Close()
   585  		b, err := io.ReadAll(file)
   586  		if err != nil {
   587  			return nil, errors.E(op, fn, err)
   588  		}
   589  		node, err = yaml.Parse(string(b))
   590  		if err != nil {
   591  			return nil, errors.E(op, fn, fmt.Errorf("invalid function config %q %w", f.ConfigPath, err))
   592  		}
   593  		// directly use the config from file
   594  		return node, nil
   595  	case len(f.ConfigMap) != 0:
   596  		configNode, err := NewConfigMap(f.ConfigMap)
   597  		if err != nil {
   598  			return nil, errors.E(op, fn, err)
   599  		}
   600  		return configNode, nil
   601  	}
   602  	// no need to return ConfigMap if no config given
   603  	return nil, nil
   604  }