github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/deploy/kpt/kpt.go (about) 1 /* 2 Copyright 2020 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package kpt 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "fmt" 24 "io" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "regexp" 29 "strings" 30 31 "golang.org/x/mod/semver" 32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 33 apimachinery "k8s.io/apimachinery/pkg/runtime/schema" 34 k8syaml "sigs.k8s.io/yaml" 35 36 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/access" 37 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" 38 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug" 39 component "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/component/kubernetes" 40 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/kubectl" 41 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/kustomize" 42 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/label" 43 deployutil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/util" 44 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/event" 45 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph" 46 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation" 47 pkgkubectl "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubectl" 48 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes" 49 kloader "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/loader" 50 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest" 51 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/portforward" 52 kstatus "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/status" 53 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/loader" 54 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/log" 55 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output" 56 olog "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log" 57 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 58 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/status" 59 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync" 60 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 61 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util/stringset" 62 ) 63 64 const ( 65 inventoryTemplate = "inventory-template.yaml" 66 kptHydrated = ".kpt-hydrated" 67 tmpKustomizeDir = ".kustomize" 68 kptFnAnnotation = "config.kubernetes.io/function" 69 kptFnLocalConfig = "config.kubernetes.io/local-config" 70 71 kptDownloadLink = "https://googlecontainertools.github.io/kpt/installation/" 72 kptMinVersionInclusive = "v0.38.1" 73 kptMaxVersionExclusive = "v1.0.0-alpha.1" 74 75 kustomizeDownloadLink = "https://kubernetes-sigs.github.io/kustomize/installation/" 76 kustomizeMinVersion = "v3.2.3" 77 kustomizeVersionRegexP = `{Version:(kustomize/)?(\S+) GitCommit:\S+ BuildDate:\S+ GoOs:\S+ GoArch:\S+}` 78 ) 79 80 // Deployer deploys workflows with kpt CLI 81 type Deployer struct { 82 *latest.KptDeploy 83 84 accessor access.Accessor 85 logger log.Logger 86 debugger debug.Debugger 87 imageLoader loader.ImageLoader 88 statusMonitor kstatus.Monitor 89 syncer sync.Syncer 90 91 podSelector *kubernetes.ImageList 92 originalImages []graph.Artifact // the set of images parsed from the Deployer's manifest set 93 localImages []graph.Artifact // the set of images marked as "local" by the Runner 94 95 insecureRegistries map[string]bool 96 labels map[string]string 97 globalConfig string 98 hasKustomization func(string) bool 99 kubeContext string 100 kubeConfig string 101 namespace string 102 103 namespaces *[]string 104 105 transformableAllowlist map[apimachinery.GroupKind]latest.ResourceFilter 106 transformableDenylist map[apimachinery.GroupKind]latest.ResourceFilter 107 } 108 109 type Config interface { 110 kubectl.Config 111 kstatus.Config 112 portforward.Config 113 kloader.Config 114 } 115 116 // NewDeployer generates a new Deployer object contains the kptDeploy schema. 117 func NewDeployer(cfg Config, labeller *label.DefaultLabeller, d *latest.KptDeploy) (*Deployer, error) { 118 podSelector := kubernetes.NewImageList() 119 kubectl := pkgkubectl.NewCLI(cfg, cfg.GetKubeNamespace()) 120 namespaces, err := deployutil.GetAllPodNamespaces(cfg.GetNamespace(), cfg.GetPipelines()) 121 if err != nil { 122 olog.Entry(context.TODO()).Warn("unable to parse namespaces - deploy might not work correctly!") 123 } 124 logger := component.NewLogger(cfg, kubectl, podSelector, &namespaces) 125 transformableAllowlist, transformableDenylist, err := deployutil.ConsolidateTransformConfiguration(cfg) 126 if err != nil { 127 return nil, err 128 } 129 return &Deployer{ 130 KptDeploy: d, 131 podSelector: podSelector, 132 namespaces: &namespaces, 133 accessor: component.NewAccessor(cfg, cfg.GetKubeContext(), kubectl, podSelector, labeller, &namespaces), 134 debugger: component.NewDebugger(cfg.Mode(), podSelector, &namespaces, cfg.GetKubeContext()), 135 imageLoader: component.NewImageLoader(cfg, kubectl), 136 logger: logger, 137 statusMonitor: component.NewMonitor(cfg, cfg.GetKubeContext(), labeller, &namespaces), 138 syncer: component.NewSyncer(kubectl, &namespaces, logger.GetFormatter()), 139 insecureRegistries: cfg.GetInsecureRegistries(), 140 labels: labeller.Labels(), 141 globalConfig: cfg.GlobalConfig(), 142 hasKustomization: hasKustomization, 143 kubeContext: cfg.GetKubeContext(), 144 kubeConfig: cfg.GetKubeConfig(), 145 namespace: cfg.GetKubeNamespace(), 146 transformableAllowlist: transformableAllowlist, 147 transformableDenylist: transformableDenylist, 148 }, nil 149 } 150 151 func (k *Deployer) trackNamespaces(namespaces []string) { 152 *k.namespaces = deployutil.ConsolidateNamespaces(*k.namespaces, namespaces) 153 } 154 155 func (k *Deployer) GetAccessor() access.Accessor { 156 return k.accessor 157 } 158 159 func (k *Deployer) GetDebugger() debug.Debugger { 160 return k.debugger 161 } 162 163 func (k *Deployer) GetLogger() log.Logger { 164 return k.logger 165 } 166 167 func (k *Deployer) GetStatusMonitor() status.Monitor { 168 return k.statusMonitor 169 } 170 171 func (k *Deployer) GetSyncer() sync.Syncer { 172 return k.syncer 173 } 174 175 func (k *Deployer) RegisterLocalImages(images []graph.Artifact) { 176 k.localImages = images 177 } 178 179 func (k *Deployer) TrackBuildArtifacts(artifacts []graph.Artifact) { 180 deployutil.AddTagsToPodSelector(artifacts, k.originalImages, k.podSelector) 181 k.logger.RegisterArtifacts(artifacts) 182 } 183 184 var sanityCheck = versionCheck 185 186 // versionCheck checks if the kpt and kustomize versions meet the minimum requirements. 187 func versionCheck(ctx context.Context, dir string, stdout io.Writer) error { 188 kptCmd := exec.Command("kpt", "version") 189 out, err := util.RunCmdOut(ctx, kptCmd) 190 if err != nil { 191 return fmt.Errorf("kpt is not installed yet\nSee kpt installation: %v", 192 kptDownloadLink) 193 } 194 // kpt follows semver but does not have "v" prefix. 195 version := "v" + strings.TrimSuffix(string(out), "\n") 196 if !semver.IsValid(version) { 197 return fmt.Errorf("unknown kpt version %v\nPlease install "+ 198 "kpt %v <= version < %v\nSee kpt installation: %v", 199 string(out), kptMinVersionInclusive, kptMaxVersionExclusive, kptDownloadLink) 200 } 201 if semver.Compare(version, kptMinVersionInclusive) < 0 || 202 semver.Compare(version, kptMaxVersionExclusive) >= 0 { 203 return fmt.Errorf("you are using kpt %q\nPlease install "+ 204 "kpt %v <= version < %v\nSee kpt installation: %v", 205 version, kptMinVersionInclusive, kptMaxVersionExclusive, kptDownloadLink) 206 } 207 208 // Users can choose not to use kustomize in kpt deployer mode. We only check the kustomize 209 // version when kustomization.yaml config is directed under .deploy.kpt.dir path. 210 if hasKustomization(dir) { 211 kustomizeCmd := exec.Command("kustomize", "version") 212 out, err := util.RunCmdOut(ctx, kustomizeCmd) 213 if err != nil { 214 return fmt.Errorf("kustomize is not installed yet\nSee kpt installation: %v", 215 kustomizeDownloadLink) 216 } 217 versionInfo := strings.TrimSuffix(string(out), "\n") 218 // Kustomize version information is in the form of 219 // {Version:$VERSION GitCommit:$COMMIT BuildDate:1970-01-01T00:00:00Z GoOs:darwin GoArch:amd64} 220 re := regexp.MustCompile(kustomizeVersionRegexP) 221 match := re.FindStringSubmatch(versionInfo) 222 if len(match) != 3 { 223 output.Yellow.Fprintf(stdout, "unable to determine kustomize version from %q\n"+ 224 "You can download the official kustomize (recommended >= %v) from %v\n", 225 string(out), kustomizeMinVersion, kustomizeDownloadLink) 226 } else if !semver.IsValid(match[2]) || semver.Compare(match[2], kustomizeMinVersion) < 0 { 227 output.Yellow.Fprintf(stdout, "you are using kustomize version %q "+ 228 "(recommended >= %v). You can download the official kustomize from %v\n", 229 match[2], kustomizeMinVersion, kustomizeDownloadLink) 230 } 231 } 232 return nil 233 } 234 235 // Deploy hydrates the manifests using kustomizations and kpt functions as described in the render method, 236 // outputs them to the applyDir, and runs `kpt live apply` against applyDir to create resources in the cluster. 237 // `kpt live apply` supports automated pruning declaratively via resources in the applyDir. 238 func (k *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Artifact) error { 239 instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{ 240 "DeployerType": "kpt", 241 }) 242 243 // Check that the cluster is reachable. 244 // This gives a better error message when the cluster can't 245 // be reached. 246 if err := kubernetes.FailIfClusterIsNotReachable(k.kubeContext); err != nil { 247 return fmt.Errorf("unable to connect to Kubernetes: %w", err) 248 } 249 250 _, endTrace := instrumentation.StartTrace(ctx, "Deploy_sanityCheck") 251 if err := sanityCheck(ctx, k.Dir, out); err != nil { 252 endTrace(instrumentation.TraceEndError(err)) 253 return err 254 } 255 endTrace() 256 257 childCtx, endTrace := instrumentation.StartTrace(ctx, "Deploy_loadImages") 258 if err := k.imageLoader.LoadImages(childCtx, out, k.localImages, k.originalImages, builds); err != nil { 259 endTrace(instrumentation.TraceEndError(err)) 260 return err 261 } 262 263 _, endTrace = instrumentation.StartTrace(ctx, "Deploy_renderManifests") 264 manifests, err := k.renderManifests(childCtx, builds) 265 if err != nil { 266 endTrace(instrumentation.TraceEndError(err)) 267 return err 268 } 269 270 if len(manifests) == 0 { 271 endTrace() 272 return nil 273 } 274 endTrace() 275 276 _, endTrace = instrumentation.StartTrace(ctx, "Deploy_CollectNamespaces") 277 namespaces, err := manifests.CollectNamespaces() 278 if err != nil { 279 event.DeployInfoEvent(fmt.Errorf("could not fetch deployed resource namespace. "+ 280 "This might cause port-forward and deploy health-check to fail: %w", err)) 281 } 282 endTrace() 283 284 childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_getApplyDir") 285 applyDir, err := k.getApplyDir(childCtx) 286 if err != nil { 287 return fmt.Errorf("getting applyDir: %w", err) 288 } 289 endTrace() 290 291 _, endTrace = instrumentation.StartTrace(ctx, "Deploy_manifest.Write") 292 if err = sink(ctx, []byte(manifests.String()), applyDir); err != nil { 293 return err 294 } 295 endTrace() 296 297 childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_execKptCommand") 298 cmd := exec.CommandContext(childCtx, "kpt", kptCommandArgs(applyDir, []string{"live", "apply"}, k.getKptLiveApplyArgs(), nil)...) 299 cmd.Stdout = out 300 cmd.Stderr = out 301 if err := util.RunCmd(ctx, cmd); err != nil { 302 endTrace(instrumentation.TraceEndError(err)) 303 return err 304 } 305 306 k.TrackBuildArtifacts(builds) 307 k.statusMonitor.RegisterDeployManifests(manifests) 308 endTrace() 309 k.trackNamespaces(namespaces) 310 return nil 311 } 312 313 // Dependencies returns a list of files that the deployer depends on. This does NOT include applyDir. 314 // In dev mode, a redeploy will be triggered if one of these files is updated. 315 func (k *Deployer) Dependencies() ([]string, error) { 316 deps := stringset.New() 317 318 // Add the app configuration manifests. It may already include kpt functions and kustomize 319 // config files. 320 configDeps, err := getResources(k.Dir) 321 if err != nil { 322 return nil, fmt.Errorf("finding dependencies in %s: %w", k.Dir, err) 323 } 324 deps.Insert(configDeps...) 325 326 // Add the kustomize resources which lives directly under k.Dir. 327 kustomizeDeps, err := kustomize.DependenciesForKustomization(k.Dir) 328 if err != nil { 329 return nil, fmt.Errorf("finding kustomization directly under %s: %w", k.Dir, err) 330 } 331 deps.Insert(kustomizeDeps...) 332 333 // Add the kpt function resources when they are outside of the k.Dir directory. 334 if len(k.Fn.FnPath) > 0 { 335 if rel, err := filepath.Rel(k.Dir, k.Fn.FnPath); err != nil { 336 return nil, fmt.Errorf("finding relative path from "+ 337 ".deploy.kpt.fn.fnPath %v to deploy.kpt.Dir %v: %w", k.Fn.FnPath, k.Dir, err) 338 } else if strings.HasPrefix(rel, "..") { 339 // kpt.FnDir is outside the config .Dir. 340 fnDeps, err := getResources(k.Fn.FnPath) 341 if err != nil { 342 return nil, fmt.Errorf("finding kpt function outside %s: %w", k.Dir, err) 343 } 344 deps.Insert(fnDeps...) 345 } 346 } 347 348 return deps.ToList(), nil 349 } 350 351 // Cleanup deletes what was deployed by calling `kpt live destroy`. 352 func (k *Deployer) Cleanup(ctx context.Context, out io.Writer, dryRun bool) error { 353 instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{ 354 "DeployerType": "kpt", 355 }) 356 357 applyDir, err := k.getApplyDir(ctx) 358 if err != nil { 359 return fmt.Errorf("getting applyDir: %w", err) 360 } 361 362 cmd := exec.CommandContext(ctx, "kpt", kptCommandArgs(applyDir, []string{"live", "destroy"}, k.getGlobalFlags(), nil)...) 363 cmd.Stdout = out 364 cmd.Stderr = out 365 if err := util.RunCmd(ctx, cmd); err != nil { 366 return err 367 } 368 369 return nil 370 } 371 372 // Render hydrates manifests using both kustomization and kpt functions. 373 func (k *Deployer) Render(ctx context.Context, out io.Writer, builds []graph.Artifact, _ bool, filepath string) error { 374 instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{ 375 "DeployerType": "kubectl", 376 }) 377 378 _, endTrace := instrumentation.StartTrace(ctx, "Render_sanityCheck") 379 380 if err := sanityCheck(ctx, k.Dir, out); err != nil { 381 endTrace(instrumentation.TraceEndError(err)) 382 return err 383 } 384 385 childCtx, endTrace := instrumentation.StartTrace(ctx, "Render_renderManifests") 386 manifests, err := k.renderManifests(childCtx, builds) 387 if err != nil { 388 endTrace(instrumentation.TraceEndError(err)) 389 return err 390 } 391 k.statusMonitor.RegisterDeployManifests(manifests) 392 endTrace() 393 394 _, endTrace = instrumentation.StartTrace(ctx, "Render_manifest.Write") 395 defer endTrace() 396 return manifest.Write(manifests.String(), filepath, out) 397 } 398 399 // renderManifests handles a majority of the hydration process for manifests. 400 // This involves reading configs from a source directory, running kustomize build, running kpt pipelines, 401 // adding image digests, and adding run-id labels. 402 func (k *Deployer) renderManifests(ctx context.Context, builds []graph.Artifact) ( 403 manifest.ManifestList, error) { 404 flags, err := k.getKptFnRunArgs() 405 if err != nil { 406 return nil, err 407 } 408 409 debugHelpersRegistry, err := config.GetDebugHelpersRegistry(k.globalConfig) 410 if err != nil { 411 return nil, fmt.Errorf("retrieving debug helpers registry: %w", err) 412 } 413 414 var buf []byte 415 // Read the manifests under k.Dir as "source". 416 cmd := exec.CommandContext( 417 ctx, "kpt", kptCommandArgs(k.Dir, []string{"fn", "source"}, 418 nil, nil)...) 419 if buf, err = util.RunCmdOut(ctx, cmd); err != nil { 420 return nil, fmt.Errorf("reading config manifests: %w", err) 421 } 422 423 // Run kpt functions against the manifests read from source. 424 cmd = exec.CommandContext(ctx, "kpt", kptCommandArgs("", []string{"fn", "run"}, flags, nil)...) 425 cmd.Stdin = bytes.NewBuffer(buf) 426 if buf, err = util.RunCmdOut(ctx, cmd); err != nil { 427 return nil, fmt.Errorf("running kpt functions: %w", err) 428 } 429 430 // Run kustomize on the output from the kpt functions if a kustomization is found. 431 // Note: kustomize cannot be used as a kpt fn yet and thus we run kustomize in a temp dir 432 // in the kpt pipeline: 433 // kpt source --> kpt run --> (workaround if kustomization exists) kustomize build --> kpt sink. 434 // 435 // Note: Optimally the user would be able to control the order in which kpt functions and 436 // Kustomize build happens, and even have Kustomize build happen between Kpt fn invocations. 437 // However, since we currently don't expose an API supporting that level of control running 438 // Kustomize build last seems like the best option. 439 // Pros: 440 // - Kustomize will remove all comments which breaks any Kpt fn relying on YAML comments. This 441 // includes https://github.com/GoogleContainerTools/kpt-functions-catalog/tree/master/functions/go/apply-setters 442 // which is the upcoming replacement for kpt cfg set and it will likely receive wide usage. 443 // This is the main reason for Kustomize last approach winning. 444 // - Kustomize mangles the directory structure so any Kpt fn relying on th relative file 445 // location of a resource as described by the config.kubernetes.io/path annotation will break 446 // if run after Kustomize. 447 // - This allows Kpt fns to modify and even create Kustomizations. 448 // Cons: 449 // - Kpt fns that expects the output of kustomize build as input might not work as expected, 450 // especially if the Kustomization references resources outside of the kpt dir. 451 // - Kpt fn run chokes on JSON patch files because the root node is an array. This can be worked 452 // around by avoiding the file extensions kpt fn reads from for such files (.yaml, .yml and 453 // .json) or inlining the patch. 454 defer func() { 455 if err = os.RemoveAll(tmpKustomizeDir); err != nil { 456 fmt.Printf("Unable to delete temporary Kusomize directory: %v\n", err) 457 } 458 }() 459 if err = sink(ctx, buf, tmpKustomizeDir); err != nil { 460 return nil, err 461 } 462 463 // Only run kustomize if kustomization.yaml is found in the output from the kpt functions. 464 if k.hasKustomization(tmpKustomizeDir) { 465 cmd = exec.CommandContext(ctx, "kustomize", append([]string{"build"}, tmpKustomizeDir)...) 466 if buf, err = util.RunCmdOut(ctx, cmd); err != nil { 467 return nil, fmt.Errorf("kustomize build: %w", err) 468 } 469 } 470 471 // Store the manipulated manifests to the sink dir. 472 if k.Fn.SinkDir != "" { 473 if err = sink(ctx, buf, k.Fn.SinkDir); err != nil { 474 return nil, err 475 } 476 fmt.Printf("Manipulated resources are stored in your sink directory: %v\n", k.Fn.SinkDir) 477 } 478 479 var manifests manifest.ManifestList 480 if len(buf) > 0 { 481 manifests.Append(buf) 482 } 483 manifests, err = k.excludeKptFn(manifests) 484 if err != nil { 485 return nil, fmt.Errorf("excluding kpt functions from manifests: %w", err) 486 } 487 if k.originalImages == nil { 488 k.originalImages, err = manifests.GetImages(manifest.NewResourceSelectorImages(k.transformableAllowlist, k.transformableDenylist)) 489 if err != nil { 490 return nil, err 491 } 492 } 493 manifests, err = manifests.ReplaceImages(ctx, builds, manifest.NewResourceSelectorImages(k.transformableAllowlist, k.transformableDenylist)) 494 if err != nil { 495 return nil, fmt.Errorf("replacing images in manifests: %w", err) 496 } 497 498 if manifests, err = manifest.ApplyTransforms(manifests, builds, k.insecureRegistries, debugHelpersRegistry); err != nil { 499 return nil, err 500 } 501 502 return manifests.SetLabels(k.labels, manifest.NewResourceSelectorLabels(k.transformableAllowlist, k.transformableDenylist)) 503 } 504 505 func sink(ctx context.Context, buf []byte, sinkDir string) error { 506 if err := os.RemoveAll(sinkDir); err != nil { 507 return fmt.Errorf("deleting sink directory %s: %w", sinkDir, err) 508 } 509 510 if err := os.MkdirAll(sinkDir, os.ModePerm); err != nil { 511 return fmt.Errorf("creating sink directory %s: %w", sinkDir, err) 512 } 513 514 cmd := exec.CommandContext(ctx, "kpt", kptCommandArgs(sinkDir, []string{"fn", "sink"}, nil, nil)...) 515 cmd.Stdin = bytes.NewBuffer(buf) 516 if _, err := util.RunCmdOut(ctx, cmd); err != nil { 517 return fmt.Errorf("sinking to directory %s: %w", sinkDir, err) 518 } 519 return nil 520 } 521 522 // excludeKptFn adds an annotation "config.kubernetes.io/local-config: 'true'" to kpt function. 523 // This will exclude kpt functions from deployed to the cluster in `kpt live apply`. 524 func (k *Deployer) excludeKptFn(originalManifest manifest.ManifestList) (manifest.ManifestList, error) { 525 var newManifest manifest.ManifestList 526 for _, yByte := range originalManifest { 527 // Convert yaml byte config to unstructured.Unstructured 528 jByte, err := k8syaml.YAMLToJSON(yByte) 529 if err != nil { 530 return nil, fmt.Errorf("yaml to json error: %w", err) 531 } 532 var obj unstructured.Unstructured 533 if err := obj.UnmarshalJSON(jByte); err != nil { 534 return nil, fmt.Errorf("unmarshaling config: %w", err) 535 } 536 // skip if the resource is not kpt fn config. 537 if _, ok := obj.GetAnnotations()[kptFnAnnotation]; !ok { 538 newManifest = append(newManifest, yByte) 539 continue 540 } 541 // skip if the kpt fn has local-config annotation specified. 542 if _, ok := obj.GetAnnotations()[kptFnLocalConfig]; ok { 543 newManifest = append(newManifest, yByte) 544 continue 545 } 546 547 // Add "local-config" annotation to kpt fn config. 548 anns := obj.GetAnnotations() 549 anns[kptFnLocalConfig] = "true" 550 obj.SetAnnotations(anns) 551 jByte, err = obj.MarshalJSON() 552 if err != nil { 553 return nil, fmt.Errorf("marshaling to json: %w", err) 554 } 555 newYByte, err := k8syaml.JSONToYAML(jByte) 556 if err != nil { 557 return nil, fmt.Errorf("converting json to yaml: %w", err) 558 } 559 newManifest.Append(newYByte) 560 } 561 return newManifest, nil 562 } 563 564 // getApplyDir returns the path to applyDir if specified by the user. Otherwise, getApplyDir 565 // creates a hidden directory named .kpt-hydrated in place of applyDir. 566 func (k *Deployer) getApplyDir(ctx context.Context) (string, error) { 567 if k.Live.Apply.Dir != "" { 568 if _, err := os.Stat(k.Live.Apply.Dir); os.IsNotExist(err) { 569 return "", err 570 } 571 return k.Live.Apply.Dir, nil 572 } 573 574 // 0755 is a permission setting where the owner can read, write, and execute. 575 // Others can read and execute but not modify the directory. 576 if err := os.MkdirAll(kptHydrated, os.ModePerm); err != nil { 577 return "", fmt.Errorf("applyDir was unspecified. creating applyDir: %w", err) 578 } 579 580 if _, err := os.Stat(filepath.Join(kptHydrated, inventoryTemplate)); os.IsNotExist(err) { 581 cmd := exec.CommandContext(ctx, "kpt", kptCommandArgs(kptHydrated, []string{"live", "init"}, k.getKptLiveInitArgs(), nil)...) 582 if _, err := util.RunCmdOut(ctx, cmd); err != nil { 583 return "", err 584 } 585 } 586 587 return kptHydrated, nil 588 } 589 590 // kptCommandArgs returns a list of additional arguments for the kpt command. 591 func kptCommandArgs(dir string, commands, flags, globalFlags []string) []string { 592 var args []string 593 594 for _, v := range commands { 595 parts := strings.Split(v, " ") 596 args = append(args, parts...) 597 } 598 599 if len(dir) > 0 { 600 args = append(args, dir) 601 } 602 603 for _, v := range flags { 604 parts := strings.Split(v, " ") 605 args = append(args, parts...) 606 } 607 608 for _, v := range globalFlags { 609 parts := strings.Split(v, " ") 610 args = append(args, parts...) 611 } 612 613 return args 614 } 615 616 // getResources returns a list of all file names in root that end in .yaml or .yml 617 func getResources(root string) ([]string, error) { 618 var files []string 619 620 if _, err := os.Stat(root); os.IsNotExist(err) { 621 return nil, err 622 } 623 624 err := filepath.Walk(root, func(path string, info os.FileInfo, _ error) error { 625 // Using regex match is not entirely accurate in deciding whether something is a resource or not. 626 // Kpt should provide better functionality for determining whether files are resources. 627 isResource, err := regexp.MatchString(`\.ya?ml$`, filepath.Base(path)) 628 if err != nil { 629 return fmt.Errorf("matching %s with regex: %w", filepath.Base(path), err) 630 } 631 632 if !info.IsDir() && isResource { 633 files = append(files, path) 634 } 635 636 return nil 637 }) 638 639 return files, err 640 } 641 642 // getKptFnRunArgs returns a list of arguments that the user specified for the `kpt fn run` command. 643 func (k *Deployer) getKptFnRunArgs() ([]string, error) { 644 var flags []string 645 646 if k.Fn.GlobalScope { 647 flags = append(flags, "--global-scope") 648 } 649 650 if len(k.Fn.Mount) > 0 { 651 flags = append(flags, "--mount", strings.Join(k.Fn.Mount, ",")) 652 } 653 654 if k.Fn.Network { 655 flags = append(flags, "--network") 656 } 657 658 if len(k.Fn.NetworkName) > 0 { 659 flags = append(flags, "--network-name", k.Fn.NetworkName) 660 } 661 662 count := 0 663 if len(k.Fn.FnPath) > 0 { 664 flags = append(flags, "--fn-path", k.Fn.FnPath) 665 count++ 666 } 667 668 if len(k.Fn.Image) > 0 { 669 flags = append(flags, "--image", k.Fn.Image) 670 count++ 671 } 672 673 if count > 1 { 674 return nil, errors.New("only one of `fn-path` or `image` may be specified") 675 } 676 677 return flags, nil 678 } 679 680 // getKptLiveApplyArgs returns a list of arguments that the user specified for the `kpt live apply` command. 681 func (k *Deployer) getKptLiveApplyArgs() []string { 682 var flags []string 683 684 if len(k.Live.Options.PollPeriod) > 0 { 685 flags = append(flags, "--poll-period", k.Live.Options.PollPeriod) 686 } 687 688 if len(k.Live.Options.PrunePropagationPolicy) > 0 { 689 flags = append(flags, "--prune-propagation-policy", k.Live.Options.PrunePropagationPolicy) 690 } 691 692 if len(k.Live.Options.PruneTimeout) > 0 { 693 flags = append(flags, "--prune-timeout", k.Live.Options.PruneTimeout) 694 } 695 696 if len(k.Live.Options.ReconcileTimeout) > 0 { 697 flags = append(flags, "--reconcile-timeout", k.Live.Options.ReconcileTimeout) 698 } 699 700 flags = append(flags, k.getGlobalFlags()...) 701 return flags 702 } 703 704 // getKptLiveInitArgs returns a list of arguments that the user specified for the `kpt live init` command. 705 func (k *Deployer) getKptLiveInitArgs() []string { 706 var flags []string 707 708 if len(k.Live.Apply.InventoryID) > 0 { 709 flags = append(flags, "--inventory-id", k.Live.Apply.InventoryID) 710 } 711 712 flags = append(flags, k.getGlobalFlags()...) 713 return flags 714 } 715 func (k *Deployer) getGlobalFlags() []string { 716 var flags []string 717 718 if k.kubeContext != "" { 719 flags = append(flags, "--context", k.kubeContext) 720 } 721 if k.kubeConfig != "" { 722 flags = append(flags, "--kubeconfig", k.kubeConfig) 723 } 724 if len(k.Live.Apply.InventoryNamespace) > 0 { 725 flags = append(flags, "--namespace", k.Live.Apply.InventoryNamespace) 726 } else if k.namespace != "" { 727 // Note: UI duplication. 728 flags = append(flags, "--namespace", k.namespace) 729 } 730 731 return flags 732 } 733 734 func hasKustomization(dir string) bool { 735 _, err := kustomize.FindKustomizationConfig(dir) 736 return err == nil 737 }