github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/deploy/kustomize/kustomize.go (about) 1 /* 2 Copyright 2019 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 kustomize 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "os" 24 "os/exec" 25 "path/filepath" 26 27 "github.com/segmentio/textio" 28 yamlv3 "gopkg.in/yaml.v3" 29 apimachinery "k8s.io/apimachinery/pkg/runtime/schema" 30 31 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/access" 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" 33 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug" 34 component "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/component/kubernetes" 35 deployerr "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/error" 36 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/kubectl" 37 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/label" 38 deployutil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/util" 39 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/event" 40 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph" 41 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/hooks" 42 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation" 43 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes" 44 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest" 45 kstatus "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/status" 46 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/loader" 47 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/log" 48 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output" 49 olog "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log" 50 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 51 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/status" 52 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync" 53 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 54 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util/stringset" 55 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/warnings" 56 ) 57 58 var ( 59 DefaultKustomizePath = "." 60 KustomizeFilePaths = []string{"kustomization.yaml", "kustomization.yml", "Kustomization"} 61 basePath = "base" 62 KustomizeBinaryCheck = kustomizeBinaryExists // For testing 63 ) 64 65 // kustomization is the content of a kustomization.yaml file. 66 type kustomization struct { 67 Components []string `yaml:"components"` 68 Bases []string `yaml:"bases"` 69 Resources []string `yaml:"resources"` 70 Patches []patchWrapper `yaml:"patches"` 71 PatchesStrategicMerge []strategicMergePatch `yaml:"patchesStrategicMerge"` 72 CRDs []string `yaml:"crds"` 73 PatchesJSON6902 []patchJSON6902 `yaml:"patchesJson6902"` 74 ConfigMapGenerator []configMapGenerator `yaml:"configMapGenerator"` 75 SecretGenerator []secretGenerator `yaml:"secretGenerator"` 76 } 77 78 type patchPath struct { 79 Path string `yaml:"path"` 80 Patch string `yaml:"patch"` 81 } 82 83 type patchWrapper struct { 84 *patchPath 85 } 86 87 type strategicMergePatch struct { 88 Path string 89 Patch string 90 } 91 92 type patchJSON6902 struct { 93 Path string `yaml:"path"` 94 } 95 96 type configMapGenerator struct { 97 Files []string `yaml:"files"` 98 Env string `yaml:"env"` 99 Envs []string `yaml:"envs"` 100 } 101 102 type secretGenerator struct { 103 Files []string `yaml:"files"` 104 Env string `yaml:"env"` 105 Envs []string `yaml:"envs"` 106 } 107 108 // Deployer deploys workflows using kustomize CLI. 109 type Deployer struct { 110 *latest.KustomizeDeploy 111 112 accessor access.Accessor 113 logger log.Logger 114 imageLoader loader.ImageLoader 115 debugger debug.Debugger 116 statusMonitor kstatus.Monitor 117 syncer sync.Syncer 118 hookRunner hooks.Runner 119 120 podSelector *kubernetes.ImageList 121 originalImages []graph.Artifact // the set of images parsed from the Deployer's manifest set 122 localImages []graph.Artifact // the set of images marked as "local" by the Runner 123 124 kubectl kubectl.CLI 125 insecureRegistries map[string]bool 126 labels map[string]string 127 globalConfig string 128 useKubectlKustomize bool 129 130 namespaces *[]string 131 132 transformableAllowlist map[apimachinery.GroupKind]latest.ResourceFilter 133 transformableDenylist map[apimachinery.GroupKind]latest.ResourceFilter 134 } 135 136 func NewDeployer(cfg kubectl.Config, labeller *label.DefaultLabeller, d *latest.KustomizeDeploy) (*Deployer, error) { 137 defaultNamespace := "" 138 if d.DefaultNamespace != nil { 139 var err error 140 defaultNamespace, err = util.ExpandEnvTemplate(*d.DefaultNamespace, nil) 141 if err != nil { 142 return nil, err 143 } 144 } 145 146 kubectl := kubectl.NewCLI(cfg, d.Flags, defaultNamespace) 147 // if user has kustomize binary, prioritize that over kubectl kustomize 148 useKubectlKustomize := !KustomizeBinaryCheck() && kubectlVersionCheck(kubectl) 149 150 podSelector := kubernetes.NewImageList() 151 namespaces, err := deployutil.GetAllPodNamespaces(cfg.GetNamespace(), cfg.GetPipelines()) 152 if err != nil { 153 olog.Entry(context.TODO()).Warn("unable to parse namespaces - deploy might not work correctly!") 154 } 155 logger := component.NewLogger(cfg, kubectl.CLI, podSelector, &namespaces) 156 transformableAllowlist, transformableDenylist, err := deployutil.ConsolidateTransformConfiguration(cfg) 157 if err != nil { 158 return nil, err 159 } 160 return &Deployer{ 161 KustomizeDeploy: d, 162 podSelector: podSelector, 163 namespaces: &namespaces, 164 accessor: component.NewAccessor(cfg, cfg.GetKubeContext(), kubectl.CLI, podSelector, labeller, &namespaces), 165 debugger: component.NewDebugger(cfg.Mode(), podSelector, &namespaces, cfg.GetKubeContext()), 166 hookRunner: hooks.NewDeployRunner(kubectl.CLI, d.LifecycleHooks, &namespaces, logger.GetFormatter(), hooks.NewDeployEnvOpts(labeller.GetRunID(), kubectl.KubeContext, namespaces)), 167 imageLoader: component.NewImageLoader(cfg, kubectl.CLI), 168 logger: logger, 169 statusMonitor: component.NewMonitor(cfg, cfg.GetKubeContext(), labeller, &namespaces), 170 syncer: component.NewSyncer(kubectl.CLI, &namespaces, logger.GetFormatter()), 171 kubectl: kubectl, 172 insecureRegistries: cfg.GetInsecureRegistries(), 173 globalConfig: cfg.GlobalConfig(), 174 labels: labeller.Labels(), 175 useKubectlKustomize: useKubectlKustomize, 176 transformableAllowlist: transformableAllowlist, 177 transformableDenylist: transformableDenylist, 178 }, nil 179 } 180 181 func (k *Deployer) trackNamespaces(namespaces []string) { 182 *k.namespaces = deployutil.ConsolidateNamespaces(*k.namespaces, namespaces) 183 } 184 185 func (k *Deployer) GetAccessor() access.Accessor { 186 return k.accessor 187 } 188 189 func (k *Deployer) GetDebugger() debug.Debugger { 190 return k.debugger 191 } 192 193 func (k *Deployer) GetLogger() log.Logger { 194 return k.logger 195 } 196 197 func (k *Deployer) GetStatusMonitor() status.Monitor { 198 return k.statusMonitor 199 } 200 201 func (k *Deployer) GetSyncer() sync.Syncer { 202 return k.syncer 203 } 204 205 func (k *Deployer) RegisterLocalImages(images []graph.Artifact) { 206 k.localImages = images 207 } 208 209 func (k *Deployer) TrackBuildArtifacts(artifacts []graph.Artifact) { 210 deployutil.AddTagsToPodSelector(artifacts, k.originalImages, k.podSelector) 211 k.logger.RegisterArtifacts(artifacts) 212 } 213 214 // Check for existence of kustomize binary in user's PATH 215 func kustomizeBinaryExists() bool { 216 _, err := exec.LookPath("kustomize") 217 218 return err == nil 219 } 220 221 func (k *Deployer) HasRunnableHooks() bool { 222 return len(k.KustomizeDeploy.LifecycleHooks.PreHooks) > 0 || len(k.KustomizeDeploy.LifecycleHooks.PostHooks) > 0 223 } 224 225 func (k *Deployer) PreDeployHooks(ctx context.Context, out io.Writer) error { 226 childCtx, endTrace := instrumentation.StartTrace(ctx, "Deploy_PreHooks") 227 if err := k.hookRunner.RunPreHooks(childCtx, out); err != nil { 228 endTrace(instrumentation.TraceEndError(err)) 229 return err 230 } 231 endTrace() 232 return nil 233 } 234 235 func (k *Deployer) PostDeployHooks(ctx context.Context, out io.Writer) error { 236 childCtx, endTrace := instrumentation.StartTrace(ctx, "Deploy_PostHooks") 237 if err := k.hookRunner.RunPostHooks(childCtx, out); err != nil { 238 endTrace(instrumentation.TraceEndError(err)) 239 return err 240 } 241 endTrace() 242 return nil 243 } 244 245 // Check that kubectl version is valid to use kubectl kustomize 246 func kubectlVersionCheck(kubectl kubectl.CLI) bool { 247 gt, err := kubectl.CompareVersionTo(context.Background(), 1, 14) 248 if err != nil { 249 return false 250 } 251 252 return gt == 1 253 } 254 255 // Deploy runs `kubectl apply` on the manifest generated by kustomize. 256 func (k *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Artifact) error { 257 instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{ 258 "DeployerType": "kustomize", 259 }) 260 261 // Check that the cluster is reachable. 262 // This gives a better error message when the cluster can't 263 // be reached. 264 if err := kubernetes.FailIfClusterIsNotReachable(k.kubectl.KubeContext); err != nil { 265 return fmt.Errorf("unable to connect to Kubernetes: %w", err) 266 } 267 268 childCtx, endTrace := instrumentation.StartTrace(ctx, "Deploy_renderManifests") 269 manifests, err := k.renderManifests(childCtx, out, builds) 270 if err != nil { 271 endTrace(instrumentation.TraceEndError(err)) 272 return err 273 } 274 275 if len(manifests) == 0 { 276 endTrace() 277 return nil 278 } 279 endTrace() 280 281 childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_LoadImages") 282 if err := k.imageLoader.LoadImages(childCtx, out, k.localImages, k.originalImages, builds); err != nil { 283 endTrace(instrumentation.TraceEndError(err)) 284 return err 285 } 286 endTrace() 287 288 _, endTrace = instrumentation.StartTrace(ctx, "Deploy_CollectNamespaces") 289 namespaces, err := manifests.CollectNamespaces() 290 if err != nil { 291 event.DeployInfoEvent(fmt.Errorf("could not fetch deployed resource namespace. "+ 292 "This might cause port-forward and deploy health-check to fail: %w", err)) 293 } 294 endTrace() 295 296 childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_WaitForDeletions") 297 if err := k.kubectl.WaitForDeletions(childCtx, textio.NewPrefixWriter(out, " - "), manifests); err != nil { 298 endTrace(instrumentation.TraceEndError(err)) 299 return err 300 } 301 endTrace() 302 303 childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_Apply") 304 if err := k.kubectl.Apply(childCtx, textio.NewPrefixWriter(out, " - "), manifests); err != nil { 305 endTrace(instrumentation.TraceEndError(err)) 306 return err 307 } 308 309 k.TrackBuildArtifacts(builds) 310 k.statusMonitor.RegisterDeployManifests(manifests) 311 endTrace() 312 313 k.trackNamespaces(namespaces) 314 return nil 315 } 316 317 func (k *Deployer) renderManifests(ctx context.Context, out io.Writer, builds []graph.Artifact) (manifest.ManifestList, error) { 318 if err := k.kubectl.CheckVersion(ctx); err != nil { 319 output.Default.Fprintln(out, "kubectl client version:", k.kubectl.Version(ctx)) 320 output.Default.Fprintln(out, err) 321 } 322 323 debugHelpersRegistry, err := config.GetDebugHelpersRegistry(k.globalConfig) 324 if err != nil { 325 return nil, deployerr.DebugHelperRetrieveErr(err) 326 } 327 328 manifests, err := k.readManifests(ctx) 329 if err != nil { 330 return nil, err 331 } 332 333 if len(manifests) == 0 { 334 return nil, nil 335 } 336 337 if len(k.originalImages) == 0 { 338 k.originalImages, err = manifests.GetImages(manifest.NewResourceSelectorImages(k.transformableAllowlist, k.transformableDenylist)) 339 if err != nil { 340 return nil, err 341 } 342 } 343 344 manifests, err = manifests.ReplaceImages(ctx, builds, manifest.NewResourceSelectorImages(k.transformableAllowlist, k.transformableDenylist)) 345 if err != nil { 346 return nil, err 347 } 348 349 if manifests, err = manifest.ApplyTransforms(manifests, builds, k.insecureRegistries, debugHelpersRegistry); err != nil { 350 return nil, err 351 } 352 353 return manifests.SetLabels(k.labels, manifest.NewResourceSelectorLabels(k.transformableAllowlist, k.transformableDenylist)) 354 } 355 356 // Cleanup deletes what was deployed by calling Deploy. 357 func (k *Deployer) Cleanup(ctx context.Context, out io.Writer, dryRun bool) error { 358 instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{ 359 "DeployerType": "kustomize", 360 }) 361 manifests, err := k.readManifests(ctx) 362 if err != nil { 363 return err 364 } 365 if dryRun { 366 for _, manifest := range manifests { 367 output.White.Fprintf(out, "---\n%s", manifest) 368 } 369 return nil 370 } 371 if err := k.kubectl.Delete(ctx, textio.NewPrefixWriter(out, " - "), manifests); err != nil { 372 return err 373 } 374 375 return nil 376 } 377 378 // Dependencies lists all the files that describe what needs to be deployed. 379 func (k *Deployer) Dependencies() ([]string, error) { 380 deps := stringset.New() 381 for _, kustomizePath := range k.KustomizePaths { 382 expandedKustomizePath, err := util.ExpandEnvTemplate(kustomizePath, nil) 383 if err != nil { 384 return nil, fmt.Errorf("unable to parse path %q: %w", kustomizePath, err) 385 } 386 depsForKustomization, err := DependenciesForKustomization(expandedKustomizePath) 387 if err != nil { 388 return nil, userErr(err) 389 } 390 deps.Insert(depsForKustomization...) 391 } 392 return deps.ToList(), nil 393 } 394 395 func (k *Deployer) Render(ctx context.Context, out io.Writer, builds []graph.Artifact, offline bool, filepath string) error { 396 instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{ 397 "DeployerType": "kustomize", 398 }) 399 400 childCtx, endTrace := instrumentation.StartTrace(ctx, "Render_renderManifests") 401 manifests, err := k.renderManifests(childCtx, out, builds) 402 if err != nil { 403 endTrace(instrumentation.TraceEndError(err)) 404 return err 405 } 406 k.statusMonitor.RegisterDeployManifests(manifests) 407 408 _, endTrace = instrumentation.StartTrace(ctx, "Render_manifest.Write") 409 defer endTrace() 410 return manifest.Write(manifests.String(), filepath, out) 411 } 412 413 // Values of `patchesStrategicMerge` can be either: 414 // + a file path, referenced as a plain string 415 // + an inline patch referenced as a string literal 416 func (p *strategicMergePatch) UnmarshalYAML(node *yamlv3.Node) error { 417 if node.Style == 0 || node.Style == yamlv3.DoubleQuotedStyle || node.Style == yamlv3.SingleQuotedStyle { 418 p.Path = node.Value 419 } else { 420 p.Patch = node.Value 421 } 422 423 return nil 424 } 425 426 // UnmarshalYAML implements JSON unmarshalling by reading an inline yaml fragment. 427 func (p *patchWrapper) UnmarshalYAML(unmarshal func(interface{}) error) (err error) { 428 pp := &patchPath{} 429 if err := unmarshal(&pp); err != nil { 430 var oldPathString string 431 if err := unmarshal(&oldPathString); err != nil { 432 return err 433 } 434 warnings.Printf("list of file paths deprecated: see https://github.com/kubernetes-sigs/kustomize/blob/master/docs/plugins/builtins.md#patchtransformer") 435 pp.Path = oldPathString 436 } 437 p.patchPath = pp 438 return nil 439 } 440 441 func pathExistsLocally(filename string, workingDir string) (bool, os.FileMode) { 442 path := filename 443 if !filepath.IsAbs(filename) { 444 path = filepath.Join(workingDir, filename) 445 } 446 if f, err := os.Stat(path); err == nil { 447 return true, f.Mode() 448 } 449 return false, 0 450 } 451 452 func (k *Deployer) readManifests(ctx context.Context) (manifest.ManifestList, error) { 453 var manifests manifest.ManifestList 454 for _, kustomizePath := range k.KustomizePaths { 455 var out []byte 456 var err error 457 458 expandedKustomizePath, err := util.ExpandEnvTemplate(kustomizePath, nil) 459 if err != nil { 460 return nil, fmt.Errorf("unable to parse path %q: %w", kustomizePath, err) 461 } 462 463 if k.useKubectlKustomize { 464 out, err = k.kubectl.Kustomize(ctx, BuildCommandArgs(k.BuildArgs, expandedKustomizePath)) 465 } else { 466 cmd := exec.CommandContext(ctx, "kustomize", append([]string{"build"}, BuildCommandArgs(k.BuildArgs, expandedKustomizePath)...)...) 467 out, err = util.RunCmdOut(ctx, cmd) 468 } 469 470 if err != nil { 471 return nil, userErr(err) 472 } 473 474 if len(out) == 0 { 475 continue 476 } 477 manifests.Append(out) 478 } 479 return manifests, nil 480 } 481 482 func IsKustomizationBase(path string) bool { 483 return filepath.Dir(path) == basePath 484 } 485 486 func IsKustomizationPath(path string) bool { 487 filename := filepath.Base(path) 488 for _, candidate := range KustomizeFilePaths { 489 if filename == candidate { 490 return true 491 } 492 } 493 return false 494 }