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  }