github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/deploy/kubectl/kubectl.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 kubectl 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "os" 26 "strings" 27 28 "github.com/segmentio/textio" 29 "go.opentelemetry.io/otel/trace" 30 apimachinery "k8s.io/apimachinery/pkg/runtime/schema" 31 32 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/access" 33 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/config" 34 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug" 35 component "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/component/kubernetes" 36 deployerr "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/error" 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 k8slogger "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/logger" 45 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest" 46 kstatus "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/status" 47 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/loader" 48 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/log" 49 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output" 50 olog "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log" 51 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest" 52 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/status" 53 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/sync" 54 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 55 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util/stringslice" 56 ) 57 58 // Deployer deploys workflows using kubectl CLI. 59 type Deployer struct { 60 *latest.KubectlDeploy 61 62 accessor access.Accessor 63 imageLoader loader.ImageLoader 64 logger k8slogger.Logger 65 debugger debug.Debugger 66 statusMonitor kstatus.Monitor 67 syncer sync.Syncer 68 hookRunner hooks.Runner 69 originalImages []graph.Artifact // the set of images marked as "local" by the Runner 70 localImages []graph.Artifact // the set of images parsed from the Deployer's manifest set 71 podSelector *kubernetes.ImageList 72 hydratedManifests []string 73 workingDir string 74 globalConfig string 75 gcsManifestDir string 76 defaultRepo *string 77 multiLevelRepo *bool 78 kubectl CLI 79 insecureRegistries map[string]bool 80 labeller *label.DefaultLabeller 81 skipRender bool 82 83 namespaces *[]string 84 85 transformableAllowlist map[apimachinery.GroupKind]latest.ResourceFilter 86 transformableDenylist map[apimachinery.GroupKind]latest.ResourceFilter 87 } 88 89 // NewDeployer returns a new Deployer for a DeployConfig filled 90 // with the needed configuration for `kubectl apply` 91 func NewDeployer(cfg Config, labeller *label.DefaultLabeller, d *latest.KubectlDeploy) (*Deployer, error) { 92 defaultNamespace := "" 93 if d.DefaultNamespace != nil { 94 var err error 95 defaultNamespace, err = util.ExpandEnvTemplate(*d.DefaultNamespace, nil) 96 if err != nil { 97 return nil, err 98 } 99 } 100 101 podSelector := kubernetes.NewImageList() 102 kubectl := NewCLI(cfg, d.Flags, defaultNamespace) 103 namespaces, err := deployutil.GetAllPodNamespaces(cfg.GetNamespace(), cfg.GetPipelines()) 104 if err != nil { 105 olog.Entry(context.TODO()).Warn("unable to parse namespaces - deploy might not work correctly!") 106 } 107 logger := component.NewLogger(cfg, kubectl.CLI, podSelector, &namespaces) 108 transformableAllowlist, transformableDenylist, err := deployutil.ConsolidateTransformConfiguration(cfg) 109 if err != nil { 110 return nil, err 111 } 112 return &Deployer{ 113 KubectlDeploy: d, 114 podSelector: podSelector, 115 namespaces: &namespaces, 116 accessor: component.NewAccessor(cfg, cfg.GetKubeContext(), kubectl.CLI, podSelector, labeller, &namespaces), 117 debugger: component.NewDebugger(cfg.Mode(), podSelector, &namespaces, cfg.GetKubeContext()), 118 imageLoader: component.NewImageLoader(cfg, kubectl.CLI), 119 logger: logger, 120 statusMonitor: component.NewMonitor(cfg, cfg.GetKubeContext(), labeller, &namespaces), 121 syncer: component.NewSyncer(kubectl.CLI, &namespaces, logger.GetFormatter()), 122 hookRunner: hooks.NewDeployRunner(kubectl.CLI, d.LifecycleHooks, &namespaces, logger.GetFormatter(), hooks.NewDeployEnvOpts(labeller.GetRunID(), kubectl.KubeContext, namespaces)), 123 workingDir: cfg.GetWorkingDir(), 124 globalConfig: cfg.GlobalConfig(), 125 defaultRepo: cfg.DefaultRepo(), 126 multiLevelRepo: cfg.MultiLevelRepo(), 127 kubectl: kubectl, 128 insecureRegistries: cfg.GetInsecureRegistries(), 129 skipRender: cfg.SkipRender(), 130 labeller: labeller, 131 hydratedManifests: cfg.HydratedManifests(), 132 transformableAllowlist: transformableAllowlist, 133 transformableDenylist: transformableDenylist, 134 }, nil 135 } 136 137 func (k *Deployer) GetAccessor() access.Accessor { 138 return k.accessor 139 } 140 141 func (k *Deployer) GetDebugger() debug.Debugger { 142 return k.debugger 143 } 144 145 func (k *Deployer) GetLogger() log.Logger { 146 return k.logger 147 } 148 149 func (k *Deployer) GetStatusMonitor() status.Monitor { 150 return k.statusMonitor 151 } 152 153 func (k *Deployer) GetSyncer() sync.Syncer { 154 return k.syncer 155 } 156 157 func (k *Deployer) RegisterLocalImages(images []graph.Artifact) { 158 k.localImages = images 159 } 160 161 func (k *Deployer) TrackBuildArtifacts(artifacts []graph.Artifact) { 162 deployutil.AddTagsToPodSelector(artifacts, k.originalImages, k.podSelector) 163 k.logger.RegisterArtifacts(artifacts) 164 } 165 166 func (k *Deployer) trackNamespaces(namespaces []string) { 167 *k.namespaces = deployutil.ConsolidateNamespaces(*k.namespaces, namespaces) 168 } 169 170 // Deploy templates the provided manifests with a simple `find and replace` and 171 // runs `kubectl apply` on those manifests 172 func (k *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Artifact) error { 173 var ( 174 manifests manifest.ManifestList 175 err error 176 childCtx context.Context 177 endTrace func(...trace.SpanEndOption) 178 ) 179 instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{ 180 "DeployerType": "kubectl", 181 }) 182 183 // Check that the cluster is reachable. 184 // This gives a better error message when the cluster can't 185 // be reached. 186 if err := kubernetes.FailIfClusterIsNotReachable(k.kubectl.KubeContext); err != nil { 187 return fmt.Errorf("unable to connect to Kubernetes: %w", err) 188 } 189 190 // if any hydrated manifests are passed to `skaffold apply`, only deploy these 191 // also, manually set the labels to ensure the runID is added 192 switch { 193 case len(k.hydratedManifests) > 0: 194 _, endTrace = instrumentation.StartTrace(ctx, "Deploy_readHydratedManifests") 195 manifests, err = k.kubectl.ReadManifests(ctx, k.hydratedManifests) 196 if err != nil { 197 endTrace(instrumentation.TraceEndError(err)) 198 return err 199 } 200 manifests, err = manifests.SetLabels(k.labeller.Labels(), manifest.NewResourceSelectorLabels(k.transformableAllowlist, k.transformableDenylist)) 201 endTrace() 202 case k.skipRender: 203 childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_readManifests") 204 manifests, err = k.readManifests(childCtx, false) 205 if err != nil { 206 endTrace(instrumentation.TraceEndError(err)) 207 return err 208 } 209 manifests, err = manifests.SetLabels(k.labeller.Labels(), manifest.NewResourceSelectorLabels(k.transformableAllowlist, k.transformableDenylist)) 210 endTrace() 211 default: 212 childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_renderManifests") 213 manifests, err = k.renderManifests(childCtx, out, builds, false) 214 endTrace() 215 } 216 217 if err != nil { 218 return err 219 } 220 221 if len(manifests) == 0 { 222 return nil 223 } 224 endTrace() 225 226 _, endTrace = instrumentation.StartTrace(ctx, "Deploy_LoadImages") 227 if err := k.imageLoader.LoadImages(childCtx, out, k.localImages, k.originalImages, builds); err != nil { 228 endTrace(instrumentation.TraceEndError(err)) 229 return err 230 } 231 endTrace() 232 233 _, endTrace = instrumentation.StartTrace(ctx, "Deploy_CollectNamespaces") 234 namespaces, err := manifests.CollectNamespaces() 235 if err != nil { 236 event.DeployInfoEvent(fmt.Errorf("could not fetch deployed resource namespace. "+ 237 "This might cause port-forward and deploy health-check to fail: %w", err)) 238 } 239 endTrace() 240 241 childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_WaitForDeletions") 242 if err := k.kubectl.WaitForDeletions(childCtx, textio.NewPrefixWriter(out, " - "), manifests); err != nil { 243 endTrace(instrumentation.TraceEndError(err)) 244 return err 245 } 246 endTrace() 247 248 childCtx, endTrace = instrumentation.StartTrace(ctx, "Deploy_KubectlApply") 249 if err := k.kubectl.Apply(childCtx, textio.NewPrefixWriter(out, " - "), manifests); err != nil { 250 endTrace(instrumentation.TraceEndError(err)) 251 return err 252 } 253 254 k.TrackBuildArtifacts(builds) 255 k.statusMonitor.RegisterDeployManifests(manifests) 256 endTrace() 257 k.trackNamespaces(namespaces) 258 return nil 259 } 260 261 func (k *Deployer) HasRunnableHooks() bool { 262 return len(k.KubectlDeploy.LifecycleHooks.PreHooks) > 0 || len(k.KubectlDeploy.LifecycleHooks.PostHooks) > 0 263 } 264 265 func (k *Deployer) PreDeployHooks(ctx context.Context, out io.Writer) error { 266 childCtx, endTrace := instrumentation.StartTrace(ctx, "Deploy_PreHooks") 267 if err := k.hookRunner.RunPreHooks(childCtx, out); err != nil { 268 endTrace(instrumentation.TraceEndError(err)) 269 return err 270 } 271 endTrace() 272 return nil 273 } 274 275 func (k *Deployer) PostDeployHooks(ctx context.Context, out io.Writer) error { 276 childCtx, endTrace := instrumentation.StartTrace(ctx, "Deploy_PostHooks") 277 if err := k.hookRunner.RunPostHooks(childCtx, out); err != nil { 278 endTrace(instrumentation.TraceEndError(err)) 279 return err 280 } 281 endTrace() 282 return nil 283 } 284 285 func (k *Deployer) manifestFiles(manifests []string) ([]string, error) { 286 var nonURLManifests, gcsManifests []string 287 for _, manifest := range manifests { 288 switch { 289 case util.IsURL(manifest): 290 case strings.HasPrefix(manifest, "gs://"): 291 gcsManifests = append(gcsManifests, manifest) 292 default: 293 nonURLManifests = append(nonURLManifests, manifest) 294 } 295 } 296 297 list, err := util.ExpandPathsGlob(k.workingDir, nonURLManifests) 298 if err != nil { 299 return nil, userErr(fmt.Errorf("expanding kubectl manifest paths: %w", err)) 300 } 301 302 if len(gcsManifests) != 0 { 303 // return tmp dir of the downloaded manifests 304 tmpDir, err := manifest.DownloadFromGCS(gcsManifests) 305 if err != nil { 306 return nil, userErr(fmt.Errorf("downloading from GCS: %w", err)) 307 } 308 k.gcsManifestDir = tmpDir 309 l, err := util.ExpandPathsGlob(tmpDir, []string{"*"}) 310 if err != nil { 311 return nil, userErr(fmt.Errorf("expanding kubectl manifest paths: %w", err)) 312 } 313 list = append(list, l...) 314 } 315 316 var filteredManifests []string 317 for _, f := range list { 318 if !kubernetes.HasKubernetesFileExtension(f) { 319 if !stringslice.Contains(manifests, f) { 320 olog.Entry(context.TODO()).Infof("refusing to deploy/delete non {json, yaml} file %s", f) 321 olog.Entry(context.TODO()).Info("If you still wish to deploy this file, please specify it directly, outside a glob pattern.") 322 continue 323 } 324 } 325 filteredManifests = append(filteredManifests, f) 326 } 327 328 return filteredManifests, nil 329 } 330 331 // readManifests reads the manifests to deploy/delete. 332 func (k *Deployer) readManifests(ctx context.Context, offline bool) (manifest.ManifestList, error) { 333 // Get file manifests 334 manifests, err := k.Dependencies() 335 // Clean the temporary directory that holds the manifests downloaded from GCS 336 defer os.RemoveAll(k.gcsManifestDir) 337 338 if err != nil { 339 return nil, listManifestErr(fmt.Errorf("listing manifests: %w", err)) 340 } 341 342 // Append URL manifests 343 hasURLManifest := false 344 for _, manifest := range k.KubectlDeploy.Manifests { 345 if util.IsURL(manifest) { 346 manifests = append(manifests, manifest) 347 hasURLManifest = true 348 } 349 } 350 351 if len(manifests) == 0 { 352 return manifest.ManifestList{}, nil 353 } 354 355 if !offline { 356 return k.kubectl.ReadManifests(ctx, manifests) 357 } 358 359 // In case no URLs are provided, we can stay offline - no need to run "kubectl create" which 360 // would try to connect to a cluster (https://github.com/kubernetes/kubernetes/issues/51475) 361 if hasURLManifest { 362 return nil, offlineModeErr() 363 } 364 return createManifestList(manifests) 365 } 366 367 func createManifestList(manifests []string) (manifest.ManifestList, error) { 368 var manifestList manifest.ManifestList 369 for _, manifestFilePath := range manifests { 370 manifestFileContent, err := ioutil.ReadFile(manifestFilePath) 371 if err != nil { 372 return nil, readManifestErr(fmt.Errorf("reading manifest file %v: %w", manifestFilePath, err)) 373 } 374 manifestList.Append(manifestFileContent) 375 } 376 return manifestList, nil 377 } 378 379 // readRemoteManifests will try to read manifests from the given kubernetes 380 // context in the specified namespace and for the specified type 381 func (k *Deployer) readRemoteManifest(ctx context.Context, name string) ([]byte, error) { 382 var args []string 383 ns := "" 384 if parts := strings.Split(name, ":"); len(parts) > 1 { 385 ns = parts[0] 386 name = parts[1] 387 } 388 args = append(args, name, "-o", "yaml") 389 390 var manifest bytes.Buffer 391 err := k.kubectl.RunInNamespace(ctx, nil, &manifest, "get", ns, args...) 392 if err != nil { 393 return nil, readRemoteManifestErr(fmt.Errorf("getting remote manifests: %w", err)) 394 } 395 396 return manifest.Bytes(), nil 397 } 398 399 func (k *Deployer) Render(ctx context.Context, out io.Writer, builds []graph.Artifact, offline bool, filepath string) error { 400 instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{ 401 "DeployerType": "kubectl", 402 }) 403 404 childCtx, endTrace := instrumentation.StartTrace(ctx, "Render_renderManifests") 405 manifests, err := k.renderManifests(childCtx, out, builds, offline) 406 if err != nil { 407 endTrace(instrumentation.TraceEndError(err)) 408 return err 409 } 410 k.statusMonitor.RegisterDeployManifests(manifests) 411 endTrace() 412 413 _, endTrace = instrumentation.StartTrace(ctx, "Render_manifest.Write") 414 defer endTrace() 415 return manifest.Write(manifests.String(), filepath, out) 416 } 417 418 func (k *Deployer) renderManifests(ctx context.Context, out io.Writer, builds []graph.Artifact, offline bool) (manifest.ManifestList, error) { 419 if err := k.kubectl.CheckVersion(ctx); err != nil { 420 output.Default.Fprintln(out, "kubectl client version:", k.kubectl.Version(ctx)) 421 output.Default.Fprintln(out, err) 422 } 423 424 debugHelpersRegistry, err := config.GetDebugHelpersRegistry(k.globalConfig) 425 if err != nil { 426 return nil, deployerr.DebugHelperRetrieveErr(fmt.Errorf("retrieving debug helpers registry: %w", err)) 427 } 428 var localManifests, remoteManifests manifest.ManifestList 429 localManifests, err = k.readManifests(ctx, offline) 430 if err != nil { 431 return nil, err 432 } 433 434 for _, m := range k.RemoteManifests { 435 manifest, err := k.readRemoteManifest(ctx, m) 436 if err != nil { 437 return nil, err 438 } 439 440 remoteManifests = append(remoteManifests, manifest) 441 } 442 443 originalManifests := append(localManifests, remoteManifests...) 444 445 if len(k.originalImages) == 0 { 446 // TODO(aaron-prindle) maybe use different resoureselector? 447 k.originalImages, err = originalManifests.GetImages(manifest.NewResourceSelectorImages(k.transformableAllowlist, k.transformableDenylist)) 448 // k.originalImages, err = originalManifests.GetImages(k.transformableAllowlist, k.transformableDenylist) 449 if err != nil { 450 return nil, err 451 } 452 } 453 454 if len(originalManifests) == 0 { 455 return nil, nil 456 } 457 458 if len(builds) == 0 { 459 for _, artifact := range k.originalImages { 460 tag, err := deployutil.ApplyDefaultRepo(k.globalConfig, k.defaultRepo, artifact.Tag) 461 if err != nil { 462 return nil, err 463 } 464 builds = append(builds, graph.Artifact{ 465 ImageName: artifact.ImageName, 466 Tag: tag, 467 }) 468 } 469 } 470 if len(remoteManifests) > 0 { 471 remoteManifests, err = remoteManifests.ReplaceRemoteManifestImages(ctx, builds, manifest.NewResourceSelectorImages(k.transformableAllowlist, k.transformableDenylist)) 472 if err != nil { 473 return nil, err 474 } 475 } 476 if len(localManifests) > 0 { 477 localManifests, err = localManifests.ReplaceImages(ctx, builds, manifest.NewResourceSelectorImages(k.transformableAllowlist, k.transformableDenylist)) 478 if err != nil { 479 return nil, err 480 } 481 } 482 483 modifiedManifests := append(localManifests, remoteManifests...) 484 485 if modifiedManifests, err = manifest.ApplyTransforms(modifiedManifests, builds, k.insecureRegistries, debugHelpersRegistry); err != nil { 486 return nil, err 487 } 488 489 return modifiedManifests.SetLabels(k.labeller.Labels(), manifest.NewResourceSelectorLabels(k.transformableAllowlist, k.transformableDenylist)) 490 } 491 492 // Cleanup deletes what was deployed by calling Deploy. 493 func (k *Deployer) Cleanup(ctx context.Context, out io.Writer, dryRun bool) error { 494 instrumentation.AddAttributesToCurrentSpanFromContext(ctx, map[string]string{ 495 "DeployerType": "kubectl", 496 }) 497 manifests, err := k.readManifests(ctx, false) 498 if err != nil { 499 return err 500 } 501 if dryRun { 502 for _, manifest := range manifests { 503 output.White.Fprintf(out, "---\n%s", manifest) 504 } 505 return nil 506 } 507 // revert remote manifests 508 // TODO(dgageot): That seems super dangerous and I don't understand 509 // why we need to update resources just before we delete them. 510 if len(k.RemoteManifests) > 0 { 511 var rm manifest.ManifestList 512 for _, m := range k.RemoteManifests { 513 manifest, err := k.readRemoteManifest(ctx, m) 514 if err != nil { 515 return err 516 } 517 rm = append(rm, manifest) 518 } 519 520 upd, err := rm.ReplaceRemoteManifestImages(ctx, k.originalImages, manifest.NewResourceSelectorImages(k.transformableAllowlist, k.transformableDenylist)) 521 if err != nil { 522 return err 523 } 524 525 if err := k.kubectl.Apply(ctx, out, upd); err != nil { 526 return err 527 } 528 } 529 530 if err := k.kubectl.Delete(ctx, textio.NewPrefixWriter(out, " - "), manifests); err != nil { 531 return err 532 } 533 534 return nil 535 } 536 537 // Dependencies lists all the files that describe what needs to be deployed. 538 func (k *Deployer) Dependencies() ([]string, error) { 539 return k.manifestFiles(k.KubectlDeploy.Manifests) 540 }