github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go (about) 1 // Copyright 2019 The Kubernetes Authors. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package cmdeval 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "io" 11 "os" 12 "path/filepath" 13 "strings" 14 15 docs "github.com/GoogleContainerTools/kpt/internal/docs/generated/fndocs" 16 "github.com/GoogleContainerTools/kpt/internal/fnruntime" 17 "github.com/GoogleContainerTools/kpt/internal/pkg" 18 "github.com/GoogleContainerTools/kpt/internal/printer" 19 "github.com/GoogleContainerTools/kpt/internal/util/argutil" 20 "github.com/GoogleContainerTools/kpt/internal/util/cmdutil" 21 "github.com/GoogleContainerTools/kpt/internal/util/pathutil" 22 kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1" 23 "github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil" 24 "github.com/GoogleContainerTools/kpt/thirdparty/cmdconfig/commands/runner" 25 "github.com/GoogleContainerTools/kpt/thirdparty/kyaml/runfn" 26 "github.com/google/shlex" 27 "github.com/spf13/cobra" 28 "sigs.k8s.io/kustomize/kyaml/comments" 29 "sigs.k8s.io/kustomize/kyaml/errors" 30 "sigs.k8s.io/kustomize/kyaml/filesys" 31 "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" 32 "sigs.k8s.io/kustomize/kyaml/order" 33 "sigs.k8s.io/kustomize/kyaml/yaml" 34 ) 35 36 // GetEvalFnRunner returns a EvalFnRunner. 37 func GetEvalFnRunner(ctx context.Context, parent string) *EvalFnRunner { 38 r := &EvalFnRunner{Ctx: ctx} 39 r.InitDefaults() 40 41 c := &cobra.Command{ 42 Use: "eval [DIR | -] [flags] [--fn-args]", 43 Short: docs.EvalShort, 44 Long: docs.EvalShort + "\n" + docs.EvalLong, 45 Example: docs.EvalExamples, 46 RunE: r.runE, 47 PreRunE: r.preRunE, 48 } 49 r.Command = c 50 r.Command.Flags().StringVarP(&r.Dest, "output", "o", "", 51 fmt.Sprintf("output resources are written to provided location. Allowed values: %s|%s|<OUT_DIR_PATH>", cmdutil.Stdout, cmdutil.Unwrap)) 52 r.Command.Flags().StringVarP( 53 &r.Image, "image", "i", "", "run this image as a function") 54 _ = r.Command.RegisterFlagCompletionFunc("image", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 55 return cmdutil.SuggestFunctions(cmd), cobra.ShellCompDirectiveDefault 56 }) 57 r.Command.Flags().StringArrayVarP( 58 &r.Keywords, "keywords", "k", nil, "filter functions that match one or more keywords") 59 _ = r.Command.RegisterFlagCompletionFunc("keywords", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 60 return cmdutil.SuggestKeywords(cmd), cobra.ShellCompDirectiveDefault 61 }) 62 r.Command.Flags().StringVarP(&r.FnType, "type", "t", "", 63 "`mutator` (default) or `validator`. tell the function type for autocompletion and `--save` flag") 64 _ = r.Command.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 65 return []string{"mutator", "validator"}, cobra.ShellCompDirectiveDefault 66 }) 67 r.Command.Flags().BoolVarP( 68 &r.SaveFn, "save", "s", false, 69 "save the function and its arguments to Kptfile") 70 r.Command.Flags().StringVar( 71 &r.Exec, "exec", "", "run an executable as a function") 72 r.Command.Flags().StringVar( 73 &r.FnConfigPath, "fn-config", "", "path to the function config file") 74 r.Command.Flags().BoolVarP( 75 &r.IncludeMetaResources, "include-meta-resources", "m", false, "include package meta resources in function input") 76 r.Command.Flags().StringVar( 77 &r.ResultsDir, "results-dir", "", "write function results to this dir") 78 r.Command.Flags().BoolVar( 79 &r.Network, "network", false, "enable network access for functions that declare it") 80 r.Command.Flags().StringArrayVar( 81 &r.Mounts, "mount", []string{}, 82 "a list of storage options read from the filesystem") 83 r.Command.Flags().StringArrayVarP( 84 &r.Env, "env", "e", []string{}, 85 "a list of environment variables to be used by functions") 86 r.Command.Flags().BoolVar( 87 &r.AsCurrentUser, "as-current-user", false, "use the uid and gid that kpt is running with to run the function in the container") 88 89 r.Command.Flags().Var(&r.RunnerOptions.ImagePullPolicy, "image-pull-policy", 90 "pull image before running the container "+r.RunnerOptions.ImagePullPolicy.HelpAllowedValues()) 91 _ = r.Command.RegisterFlagCompletionFunc("image-pull-policy", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 92 return r.RunnerOptions.ImagePullPolicy.AllStrings(), cobra.ShellCompDirectiveDefault 93 }) 94 95 r.Command.Flags().BoolVar( 96 &r.RunnerOptions.AllowWasm, "allow-alpha-wasm", false, "allow alpha wasm functions to be run. If true, you can specify a wasm image with --image flag or a path to a wasm file (must have the .wasm file extension) with --exec flag.") 97 98 // selector flags 99 r.Command.Flags().StringVar( 100 &r.Selector.APIVersion, "match-api-version", "", "select resources matching the given apiVersion") 101 r.Command.Flags().StringVar( 102 &r.Selector.Kind, "match-kind", "", "select resources matching the given kind") 103 r.Command.Flags().StringVar( 104 &r.Selector.Name, "match-name", "", "select resources matching the given name") 105 r.Command.Flags().StringVar( 106 &r.Selector.Namespace, "match-namespace", "", "select resources matching the given namespace") 107 r.Command.Flags().StringArrayVar( 108 &r.selectorAnnotations, "match-annotations", []string{}, "select resources matching the given annotations") 109 r.Command.Flags().StringArrayVar( 110 &r.selectorLabels, "match-labels", []string{}, "select resources matching the given labels") 111 112 // exclusion flags 113 r.Command.Flags().StringVar( 114 &r.Exclusion.APIVersion, "exclude-api-version", "", "exclude resources matching the given apiVersion") 115 r.Command.Flags().StringVar( 116 &r.Exclusion.Kind, "exclude-kind", "", "exclude resources matching the given kind") 117 r.Command.Flags().StringVar( 118 &r.Exclusion.Name, "exclude-name", "", "exclude resources matching the given name") 119 r.Command.Flags().StringVar( 120 &r.Exclusion.Namespace, "exclude-namespace", "", "exclude resources matching the given namespace") 121 r.Command.Flags().StringArrayVar( 122 &r.excludeAnnotations, "exclude-annotations", []string{}, "exclude resources matching the given annotations") 123 r.Command.Flags().StringArrayVar( 124 &r.excludeLabels, "exclude-labels", []string{}, "exclude resources matching the given labels") 125 126 if err := r.Command.Flags().MarkHidden("include-meta-resources"); err != nil { 127 panic(err) 128 } 129 cmdutil.FixDocs("kpt", parent, c) 130 return r 131 } 132 133 func EvalCommand(ctx context.Context, name string) *cobra.Command { 134 return GetEvalFnRunner(ctx, name).Command 135 } 136 137 // EvalFnRunner contains the run function 138 type EvalFnRunner struct { 139 Command *cobra.Command 140 Dest string 141 OutContent bytes.Buffer 142 FromStdin bool 143 Image string 144 SaveFn bool 145 Keywords []string 146 FnType string 147 Exec string 148 FnConfigPath string 149 ResultsDir string 150 Network bool 151 Mounts []string 152 Env []string 153 AsCurrentUser bool 154 IncludeMetaResources bool 155 Ctx context.Context 156 Selector kptfile.Selector 157 Exclusion kptfile.Selector 158 dataItems []string 159 160 RunnerOptions fnruntime.RunnerOptions 161 162 // we will need to parse these values into Selector and Exclusion 163 selectorLabels []string 164 selectorAnnotations []string 165 excludeLabels []string 166 excludeAnnotations []string 167 168 runFns runfn.RunFns 169 } 170 171 func (r *EvalFnRunner) InitDefaults() { 172 r.RunnerOptions.InitDefaults() 173 } 174 175 func (r *EvalFnRunner) runE(c *cobra.Command, _ []string) error { 176 err := runner.HandleError(r.Ctx, r.runFns.Execute()) 177 if err != nil { 178 return err 179 } 180 if err = cmdutil.WriteFnOutput(r.Dest, r.OutContent.String(), r.FromStdin, 181 printer.FromContextOrDie(r.Ctx).OutStream()); err != nil { 182 return err 183 } 184 if r.SaveFn { 185 r.SaveFnToKptfile() 186 } 187 return nil 188 } 189 190 // NewFunction creates a Kptfile.Function object which has the evaluated fn configurations. 191 // This object can be written to Kptfile `pipeline.mutators`. 192 func (r *EvalFnRunner) NewFunction() *kptfile.Function { 193 newFn := &kptfile.Function{} 194 if r.Image != "" { 195 newFn.Image = r.Image 196 } else { 197 newFn.Exec = r.Exec 198 } 199 if !r.Selector.IsEmpty() { 200 newFn.Selectors = []kptfile.Selector{r.Selector} 201 } 202 if !r.Exclusion.IsEmpty() { 203 newFn.Exclusions = []kptfile.Selector{r.Exclusion} 204 } 205 if r.FnConfigPath != "" { 206 fnConfigAbsPath, _, _ := pathutil.ResolveAbsAndRelPaths(r.FnConfigPath) 207 pkgAbsPath, _, _ := pathutil.ResolveAbsAndRelPaths(r.runFns.Path) 208 newFn.ConfigPath, _ = filepath.Rel(pkgAbsPath, fnConfigAbsPath) 209 } else { 210 data := map[string]string{} 211 for i, s := range r.dataItems { 212 kv := strings.SplitN(s, "=", 2) 213 if i == 0 && len(kv) == 1 { 214 continue 215 } 216 data[kv[0]] = kv[1] 217 } 218 if len(data) != 0 { 219 newFn.ConfigMap = data 220 } 221 } 222 return newFn 223 } 224 225 // Add the evaluated function to the kptfile.Function list, this Function can either be 226 // `pipeline.mutators` or `pipeline.validators` 227 func (r *EvalFnRunner) updateFnList(oldFNs []kptfile.Function) ([]kptfile.Function, string) { 228 var newFns []kptfile.Function 229 found := false 230 newFn := r.NewFunction() 231 var message string 232 for _, m := range oldFNs { 233 switch { 234 case m.Image != "" && m.Image == r.Image: 235 newFns = append(newFns, *newFn) 236 found = true 237 message = fmt.Sprintf("Updated %q as %v in the Kptfile.\n", r.Image, r.FnType) 238 case m.Exec != "" && m.Exec == r.Exec: 239 newFns = append(newFns, *newFn) 240 found = true 241 message = fmt.Sprintf("Updated %q as %v in the Kptfile.\n", r.Exec, r.FnType) 242 default: 243 newFns = append(newFns, m) 244 } 245 } 246 if !found { 247 newFns = append(newFns, *newFn) 248 if newFn.Image != "" { 249 message = fmt.Sprintf("Added %q as %v in the Kptfile.\n", r.Image, r.FnType) 250 } else if newFn.Exec != "" { 251 message = fmt.Sprintf("Added %q as %v in the Kptfile.\n", r.Exec, r.FnType) 252 } 253 } 254 return newFns, message 255 } 256 257 // SaveFnToKptfile adds the evaluated function and its arguments to Kptfile `pipeline.mutators` or `pipeline.validators` . 258 func (r *EvalFnRunner) SaveFnToKptfile() { 259 pr := printer.FromContextOrDie(r.Ctx) 260 kf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, r.runFns.Path) 261 if err != nil { 262 pr.Printf("function not added: Kptfile not exists\n") 263 return 264 } 265 266 if kf.Pipeline == nil { 267 kf.Pipeline = &kptfile.Pipeline{} 268 } 269 var usrMsg string 270 switch r.FnType { 271 case "mutator": 272 kf.Pipeline.Mutators, usrMsg = r.updateFnList(kf.Pipeline.Mutators) 273 case "validator": 274 kf.Pipeline.Validators, usrMsg = r.updateFnList(kf.Pipeline.Validators) 275 } 276 277 mutatedKfAsYNode, err := r.preserveCommentsAndFieldOrder(kf) 278 if err != nil { 279 pr.Printf("function is not added to Kptfile: %v\n", err) 280 } 281 282 // When saving function to Kptfile, the functionConfig should be the relative path 283 // to the kpt package, not the relative path to the current working dir. 284 // error handling are ignored since they have been validated in preRunE. 285 if err := kptfileutil.WriteFile(r.runFns.Path, mutatedKfAsYNode); err != nil { 286 pr.Printf("function is not added to Kptfile: %v\n", err) 287 return 288 } 289 pr.Printf(usrMsg) 290 } 291 292 // preserveCommentsAndFieldOrder syncs the mutated Kptfile with the original to preserve 293 // comments and field order, and returns the result as a yaml Node 294 func (r *EvalFnRunner) preserveCommentsAndFieldOrder(kf *kptfile.KptFile) (*yaml.Node, error) { 295 kfAsRNode, err := yaml.ReadFile(filepath.Join(r.runFns.Path, kptfile.KptFileName)) 296 if err != nil { 297 return nil, fmt.Errorf("could not read Kptfile: %v", err) 298 } 299 mutatedKfAsBytes, err := yaml.Marshal(kf) 300 if err != nil { 301 return nil, fmt.Errorf("could not Marshal Kptfile into bytes: %v", err) 302 } 303 mutatedKfAsRNode, err := yaml.Parse(string(mutatedKfAsBytes)) 304 if err != nil { 305 return nil, fmt.Errorf("could not parse Kptfile: %v", err) 306 } 307 // preserve comments and sync field order 308 if err := comments.CopyComments(kfAsRNode, mutatedKfAsRNode); err != nil { 309 return nil, fmt.Errorf("could not preserve Kptfile comments: %v", err) 310 } 311 if err := order.SyncOrder(kfAsRNode, mutatedKfAsRNode); err != nil { 312 return nil, fmt.Errorf("could not preserve Kptfile field order %v", err) 313 } 314 return mutatedKfAsRNode.YNode(), nil 315 } 316 317 // getCLIFunctionConfig parses the commandline flags and arguments into explicit 318 // function config 319 func (r *EvalFnRunner) getCLIFunctionConfig(ctx context.Context, dataItems []string) (*yaml.RNode, error) { 320 if r.Image == "" && r.Exec == "" { 321 return nil, nil 322 } 323 324 // TODO: This probably doesn't belong here, but moving it changes the test output 325 if r.Image != "" { 326 img, err := r.RunnerOptions.ResolveToImage(ctx, r.Image) 327 if err != nil { 328 return nil, err 329 } 330 r.Image = img 331 } 332 333 var err error 334 335 // create the function config 336 rc, err := yaml.Parse(` 337 metadata: 338 name: function-input 339 data: {} 340 `) 341 if err != nil { 342 return nil, err 343 } 344 345 // default the function config kind to ConfigMap, this may be overridden 346 var kind = "ConfigMap" 347 var version = "v1" 348 349 // populate the function config with data. this is a convention for functions 350 // to be more commandline friendly 351 if len(dataItems) > 0 { 352 dataField, err := rc.Pipe(yaml.Lookup("data")) 353 if err != nil { 354 return nil, err 355 } 356 for i, s := range dataItems { 357 kv := strings.SplitN(s, "=", 2) 358 if i == 0 && len(kv) == 1 { 359 // first argument may be the kind 360 kind = s 361 continue 362 } 363 if len(kv) != 2 { 364 return nil, fmt.Errorf("args must have keys and values separated by =") 365 } 366 // When we are using a ConfigMap as the functionConfig, we should create 367 // the node with type string instead of creating a scalar node. Because 368 // a scalar node might be parsed as int, float or bool later. 369 err := dataField.PipeE(yaml.SetField(kv[0], yaml.NewStringRNode(kv[1]))) 370 if err != nil { 371 return nil, err 372 } 373 } 374 } 375 err = rc.PipeE(yaml.SetField("kind", yaml.NewScalarRNode(kind))) 376 if err != nil { 377 return nil, err 378 } 379 err = rc.PipeE(yaml.SetField("apiVersion", yaml.NewScalarRNode(version))) 380 if err != nil { 381 return nil, err 382 } 383 return rc, nil 384 } 385 386 func (r *EvalFnRunner) getFunctionSpec() (*runtimeutil.FunctionSpec, []string, error) { 387 fn := &runtimeutil.FunctionSpec{} 388 var execArgs []string 389 if r.Image != "" { 390 if err := kptfile.ValidateFunctionImageURL(r.Image); err != nil { 391 return nil, nil, err 392 } 393 fn.Container.Image = r.Image 394 } else if r.Exec != "" { 395 // check the flags that doesn't make sense with exec function 396 // --mount, --as-current-user, --network and --env are 397 // only used with container functions 398 if r.AsCurrentUser || r.Network || 399 len(r.Mounts) != 0 || len(r.Env) != 0 { 400 return nil, nil, fmt.Errorf("--mount, --as-current-user, --network and --env can only be used with container functions") 401 } 402 s, err := shlex.Split(r.Exec) 403 if err != nil { 404 return nil, nil, fmt.Errorf("exec command %q must be valid: %w", r.Exec, err) 405 } 406 if len(s) > 0 { 407 fn.Exec.Path = s[0] 408 execArgs = s[1:] 409 } 410 } 411 return fn, execArgs, nil 412 } 413 414 func toStorageMounts(mounts []string) []runtimeutil.StorageMount { 415 var sms []runtimeutil.StorageMount 416 for _, mount := range mounts { 417 sms = append(sms, runtimeutil.StringToStorageMount(mount)) 418 } 419 return sms 420 } 421 422 func checkFnConfigPathExistence(path string) error { 423 // check does fn config file exist 424 if _, err := os.Stat(path); os.IsNotExist(err) { 425 return fmt.Errorf("missing function config file: %s", path) 426 } 427 return nil 428 } 429 430 func (r *EvalFnRunner) validateOptionalFlags() error { 431 // Let users know that --include-meta-resources is no longer necessary 432 // since meta resources are included by default. 433 if r.IncludeMetaResources { 434 return fmt.Errorf("--include-meta-resources is no longer necessary because meta resources are now included by default") 435 } 436 // SaveFn stores function to Kptfile. If not enabled, only make in-place changes. 437 if r.SaveFn { 438 if r.FnType == "" { 439 return fmt.Errorf("--type must be specified if saving functions to Kptfile (--save=true)") 440 } 441 if r.FnType != "mutator" && r.FnType != "validator" { 442 return fmt.Errorf("--type must be either `mutator` or `validator`") 443 } 444 } 445 // ResultsDir stores the hydrated output in a structured format to result dir. If not specified, only make 446 // in-place changes. 447 if r.ResultsDir != "" { 448 err := os.MkdirAll(r.ResultsDir, 0755) 449 if err != nil { 450 return fmt.Errorf("cannot read or create results dir %q: %w", r.ResultsDir, err) 451 } 452 } 453 454 return nil 455 } 456 457 func (r *EvalFnRunner) preRunE(c *cobra.Command, args []string) error { 458 // separate the optional flag validation to fix linter issue: cyclomatic complexity 459 if err := r.validateOptionalFlags(); err != nil { 460 return err 461 } 462 if r.Dest != "" && r.Dest != cmdutil.Stdout && r.Dest != cmdutil.Unwrap { 463 if err := cmdutil.CheckDirectoryNotPresent(r.Dest); err != nil { 464 return err 465 } 466 } 467 if r.Image == "" && r.Exec == "" { 468 return errors.Errorf("must specify --image or --exec") 469 } 470 var dataItems []string 471 if c.ArgsLenAtDash() >= 0 { 472 dataItems = append(dataItems, args[c.ArgsLenAtDash():]...) 473 args = args[:c.ArgsLenAtDash()] 474 } 475 if len(args) == 0 { 476 // default to current working directory 477 args = append(args, ".") 478 } 479 if len(args) > 1 { 480 return errors.Errorf("0 or 1 arguments supported, function arguments go after '--'") 481 } 482 if len(dataItems) > 0 && r.FnConfigPath != "" { 483 return fmt.Errorf("function arguments can only be specified without function config file") 484 } 485 fnConfig, err := r.getCLIFunctionConfig(c.Context(), dataItems) 486 if err != nil { 487 return err 488 } 489 r.dataItems = dataItems 490 fnSpec, execArgs, err := r.getFunctionSpec() 491 if err != nil { 492 return err 493 } 494 495 // set the output to stdout if in dry-run mode or no arguments are specified 496 var output io.Writer 497 var input io.Reader 498 r.OutContent = bytes.Buffer{} 499 if args[0] == "-" { 500 output = &r.OutContent 501 input = c.InOrStdin() 502 r.FromStdin = true 503 504 // clear args as it indicates stdin and not path 505 args = []string{} 506 } else if r.Dest != "" { 507 output = &r.OutContent 508 } 509 510 // set the path if specified as an argument 511 var path string 512 if len(args) == 1 { 513 // argument is the directory 514 path = args[0] 515 } 516 517 // parse mounts to set storageMounts 518 storageMounts := toStorageMounts(r.Mounts) 519 520 if r.FnConfigPath != "" { 521 err = checkFnConfigPathExistence(r.FnConfigPath) 522 if err != nil { 523 return err 524 } 525 } 526 527 if path != "" { 528 path, err = argutil.ResolveSymlink(r.Ctx, path) 529 if err != nil { 530 return err 531 } 532 } 533 if r.SaveFn && r.FnConfigPath != "" { 534 fnConfigAbsPath, _, _ := pathutil.ResolveAbsAndRelPaths(r.FnConfigPath) 535 pkgAbsPath, _, _ := pathutil.ResolveAbsAndRelPaths(path) 536 if !strings.HasPrefix(fnConfigAbsPath, pkgAbsPath) { 537 return fmt.Errorf("--fn-config must be under %v if saving functions to Kptfile (--save=true)", 538 pkgAbsPath) 539 } 540 } 541 r.parseSelectors() 542 r.runFns = runfn.RunFns{ 543 Ctx: r.Ctx, 544 Function: fnSpec, 545 ExecArgs: execArgs, 546 OriginalExec: r.Exec, 547 Output: output, 548 Input: input, 549 Path: path, 550 Network: r.Network, 551 StorageMounts: storageMounts, 552 ResultsDir: r.ResultsDir, 553 Env: r.Env, 554 AsCurrentUser: r.AsCurrentUser, 555 FnConfig: fnConfig, 556 FnConfigPath: r.FnConfigPath, 557 // fn eval should remove all files when all resources 558 // are deleted. 559 ContinueOnEmptyResult: true, 560 Selector: r.Selector, 561 Exclusion: r.Exclusion, 562 RunnerOptions: r.RunnerOptions, 563 } 564 565 return nil 566 } 567 568 // parses annotation and label based selectors and exclusion from the command line input 569 func (r *EvalFnRunner) parseSelectors() { 570 r.Selector.Annotations = parseSelectorMap(r.selectorAnnotations) 571 r.Selector.Labels = parseSelectorMap(r.selectorLabels) 572 r.Exclusion.Annotations = parseSelectorMap(r.excludeAnnotations) 573 r.Exclusion.Labels = parseSelectorMap(r.excludeLabels) 574 } 575 576 func parseSelectorMap(selectors []string) map[string]string { 577 if len(selectors) == 0 { 578 return nil 579 } 580 result := make(map[string]string) 581 for _, s := range selectors { 582 parts := strings.Split(s, "=") 583 key, value := parts[0], parts[1] 584 result[key] = value 585 } 586 return result 587 }