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  }