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 }