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  }