...

Source file src/helm.sh/helm/v3/pkg/action/install.go

Documentation: helm.sh/helm/v3/pkg/action

     1  /*
     2  Copyright The Helm 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 action
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"io"
    24  	"net/url"
    25  	"os"
    26  	"path"
    27  	"path/filepath"
    28  	"strings"
    29  	"sync"
    30  	"text/template"
    31  	"time"
    32  
    33  	"github.com/Masterminds/sprig/v3"
    34  	"github.com/pkg/errors"
    35  	v1 "k8s.io/api/core/v1"
    36  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    37  	"k8s.io/apimachinery/pkg/api/meta"
    38  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    39  	"k8s.io/cli-runtime/pkg/resource"
    40  	"sigs.k8s.io/yaml"
    41  
    42  	"helm.sh/helm/v3/pkg/chart"
    43  	"helm.sh/helm/v3/pkg/chartutil"
    44  	"helm.sh/helm/v3/pkg/cli"
    45  	"helm.sh/helm/v3/pkg/downloader"
    46  	"helm.sh/helm/v3/pkg/getter"
    47  	"helm.sh/helm/v3/pkg/kube"
    48  	kubefake "helm.sh/helm/v3/pkg/kube/fake"
    49  	"helm.sh/helm/v3/pkg/postrender"
    50  	"helm.sh/helm/v3/pkg/registry"
    51  	"helm.sh/helm/v3/pkg/release"
    52  	"helm.sh/helm/v3/pkg/releaseutil"
    53  	"helm.sh/helm/v3/pkg/repo"
    54  	"helm.sh/helm/v3/pkg/storage"
    55  	"helm.sh/helm/v3/pkg/storage/driver"
    56  )
    57  
    58  // NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine
    59  // but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually
    60  // wants to see this file after rendering in the status command. However, it must be a suffix
    61  // since there can be filepath in front of it.
    62  const notesFileSuffix = "NOTES.txt"
    63  
    64  const defaultDirectoryPermission = 0755
    65  
    66  // Install performs an installation operation.
    67  type Install struct {
    68  	cfg *Configuration
    69  
    70  	ChartPathOptions
    71  
    72  	ClientOnly      bool
    73  	Force           bool
    74  	CreateNamespace bool
    75  	DryRun          bool
    76  	DryRunOption    string
    77  	// HideSecret can be set to true when DryRun is enabled in order to hide
    78  	// Kubernetes Secrets in the output. It cannot be used outside of DryRun.
    79  	HideSecret               bool
    80  	DisableHooks             bool
    81  	Replace                  bool
    82  	Wait                     bool
    83  	WaitForJobs              bool
    84  	Devel                    bool
    85  	DependencyUpdate         bool
    86  	Timeout                  time.Duration
    87  	Namespace                string
    88  	ReleaseName              string
    89  	GenerateName             bool
    90  	NameTemplate             string
    91  	Description              string
    92  	OutputDir                string
    93  	Atomic                   bool
    94  	SkipCRDs                 bool
    95  	SubNotes                 bool
    96  	DisableOpenAPIValidation bool
    97  	IncludeCRDs              bool
    98  	Labels                   map[string]string
    99  	// KubeVersion allows specifying a custom kubernetes version to use and
   100  	// APIVersions allows a manual set of supported API Versions to be passed
   101  	// (for things like templating). These are ignored if ClientOnly is false
   102  	KubeVersion *chartutil.KubeVersion
   103  	APIVersions chartutil.VersionSet
   104  	// Used by helm template to render charts with .Release.IsUpgrade. Ignored if Dry-Run is false
   105  	IsUpgrade bool
   106  	// Enable DNS lookups when rendering templates
   107  	EnableDNS bool
   108  	// Used by helm template to add the release as part of OutputDir path
   109  	// OutputDir/<ReleaseName>
   110  	UseReleaseName bool
   111  	PostRenderer   postrender.PostRenderer
   112  	// Lock to control raceconditions when the process receives a SIGTERM
   113  	Lock sync.Mutex
   114  }
   115  
   116  // ChartPathOptions captures common options used for controlling chart paths
   117  type ChartPathOptions struct {
   118  	CaFile                string // --ca-file
   119  	CertFile              string // --cert-file
   120  	KeyFile               string // --key-file
   121  	InsecureSkipTLSverify bool   // --insecure-skip-verify
   122  	PlainHTTP             bool   // --plain-http
   123  	Keyring               string // --keyring
   124  	Password              string // --password
   125  	PassCredentialsAll    bool   // --pass-credentials
   126  	RepoURL               string // --repo
   127  	Username              string // --username
   128  	Verify                bool   // --verify
   129  	Version               string // --version
   130  
   131  	// registryClient provides a registry client but is not added with
   132  	// options from a flag
   133  	registryClient *registry.Client
   134  }
   135  
   136  // NewInstall creates a new Install object with the given configuration.
   137  func NewInstall(cfg *Configuration) *Install {
   138  	in := &Install{
   139  		cfg: cfg,
   140  	}
   141  	in.ChartPathOptions.registryClient = cfg.RegistryClient
   142  
   143  	return in
   144  }
   145  
   146  // SetRegistryClient sets the registry client for the install action
   147  func (i *Install) SetRegistryClient(registryClient *registry.Client) {
   148  	i.ChartPathOptions.registryClient = registryClient
   149  }
   150  
   151  // GetRegistryClient get the registry client.
   152  func (i *Install) GetRegistryClient() *registry.Client {
   153  	return i.ChartPathOptions.registryClient
   154  }
   155  
   156  func (i *Install) installCRDs(crds []chart.CRD) error {
   157  	// We do these one file at a time in the order they were read.
   158  	totalItems := []*resource.Info{}
   159  	for _, obj := range crds {
   160  		// Read in the resources
   161  		res, err := i.cfg.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false)
   162  		if err != nil {
   163  			return errors.Wrapf(err, "failed to install CRD %s", obj.Name)
   164  		}
   165  
   166  		// Send them to Kube
   167  		if _, err := i.cfg.KubeClient.Create(res); err != nil {
   168  			// If the error is CRD already exists, continue.
   169  			if apierrors.IsAlreadyExists(err) {
   170  				crdName := res[0].Name
   171  				i.cfg.Log("CRD %s is already present. Skipping.", crdName)
   172  				continue
   173  			}
   174  			return errors.Wrapf(err, "failed to install CRD %s", obj.Name)
   175  		}
   176  		totalItems = append(totalItems, res...)
   177  	}
   178  	if len(totalItems) > 0 {
   179  		// Give time for the CRD to be recognized.
   180  		if err := i.cfg.KubeClient.Wait(totalItems, 60*time.Second); err != nil {
   181  			return err
   182  		}
   183  
   184  		// If we have already gathered the capabilities, we need to invalidate
   185  		// the cache so that the new CRDs are recognized. This should only be
   186  		// the case when an action configuration is reused for multiple actions,
   187  		// as otherwise it is later loaded by ourselves when getCapabilities
   188  		// is called later on in the installation process.
   189  		if i.cfg.Capabilities != nil {
   190  			discoveryClient, err := i.cfg.RESTClientGetter.ToDiscoveryClient()
   191  			if err != nil {
   192  				return err
   193  			}
   194  
   195  			i.cfg.Log("Clearing discovery cache")
   196  			discoveryClient.Invalidate()
   197  
   198  			_, _ = discoveryClient.ServerGroups()
   199  		}
   200  
   201  		// Invalidate the REST mapper, since it will not have the new CRDs
   202  		// present.
   203  		restMapper, err := i.cfg.RESTClientGetter.ToRESTMapper()
   204  		if err != nil {
   205  			return err
   206  		}
   207  		if resettable, ok := restMapper.(meta.ResettableRESTMapper); ok {
   208  			i.cfg.Log("Clearing REST mapper cache")
   209  			resettable.Reset()
   210  		}
   211  	}
   212  	return nil
   213  }
   214  
   215  // Run executes the installation
   216  //
   217  // If DryRun is set to true, this will prepare the release, but not install it
   218  
   219  func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
   220  	ctx := context.Background()
   221  	return i.RunWithContext(ctx, chrt, vals)
   222  }
   223  
   224  // Run executes the installation with Context
   225  //
   226  // When the task is cancelled through ctx, the function returns and the install
   227  // proceeds in the background.
   228  func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
   229  	// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
   230  	if !i.ClientOnly {
   231  		if err := i.cfg.KubeClient.IsReachable(); err != nil {
   232  			return nil, err
   233  		}
   234  	}
   235  
   236  	// HideSecret must be used with dry run. Otherwise, return an error.
   237  	if !i.isDryRun() && i.HideSecret {
   238  		return nil, errors.New("Hiding Kubernetes secrets requires a dry-run mode")
   239  	}
   240  
   241  	if err := i.availableName(); err != nil {
   242  		return nil, err
   243  	}
   244  
   245  	if err := chartutil.ProcessDependenciesWithMerge(chrt, vals); err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	var interactWithRemote bool
   250  	if !i.isDryRun() || i.DryRunOption == "server" || i.DryRunOption == "none" || i.DryRunOption == "false" {
   251  		interactWithRemote = true
   252  	}
   253  
   254  	// Pre-install anything in the crd/ directory. We do this before Helm
   255  	// contacts the upstream server and builds the capabilities object.
   256  	if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
   257  		// On dry run, bail here
   258  		if i.isDryRun() {
   259  			i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
   260  		} else if err := i.installCRDs(crds); err != nil {
   261  			return nil, err
   262  		}
   263  	}
   264  
   265  	if i.ClientOnly {
   266  		// Add mock objects in here so it doesn't use Kube API server
   267  		// NOTE(bacongobbler): used for `helm template`
   268  		i.cfg.Capabilities = chartutil.DefaultCapabilities.Copy()
   269  		if i.KubeVersion != nil {
   270  			i.cfg.Capabilities.KubeVersion = *i.KubeVersion
   271  		}
   272  		i.cfg.Capabilities.APIVersions = append(i.cfg.Capabilities.APIVersions, i.APIVersions...)
   273  		i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard}
   274  
   275  		mem := driver.NewMemory()
   276  		mem.SetNamespace(i.Namespace)
   277  		i.cfg.Releases = storage.Init(mem)
   278  	} else if !i.ClientOnly && len(i.APIVersions) > 0 {
   279  		i.cfg.Log("API Version list given outside of client only mode, this list will be ignored")
   280  	}
   281  
   282  	// Make sure if Atomic is set, that wait is set as well. This makes it so
   283  	// the user doesn't have to specify both
   284  	i.Wait = i.Wait || i.Atomic
   285  
   286  	caps, err := i.cfg.getCapabilities()
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	// special case for helm template --is-upgrade
   292  	isUpgrade := i.IsUpgrade && i.isDryRun()
   293  	options := chartutil.ReleaseOptions{
   294  		Name:      i.ReleaseName,
   295  		Namespace: i.Namespace,
   296  		Revision:  1,
   297  		IsInstall: !isUpgrade,
   298  		IsUpgrade: isUpgrade,
   299  	}
   300  	valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps)
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  
   305  	if driver.ContainsSystemLabels(i.Labels) {
   306  		return nil, fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels())
   307  	}
   308  
   309  	rel := i.createRelease(chrt, vals, i.Labels)
   310  
   311  	var manifestDoc *bytes.Buffer
   312  	rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS, i.HideSecret)
   313  	// Even for errors, attach this if available
   314  	if manifestDoc != nil {
   315  		rel.Manifest = manifestDoc.String()
   316  	}
   317  	// Check error from render
   318  	if err != nil {
   319  		rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
   320  		// Return a release with partial data so that the client can show debugging information.
   321  		return rel, err
   322  	}
   323  
   324  	// Mark this release as in-progress
   325  	rel.SetStatus(release.StatusPendingInstall, "Initial install underway")
   326  
   327  	var toBeAdopted kube.ResourceList
   328  	resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation)
   329  	if err != nil {
   330  		return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest")
   331  	}
   332  
   333  	// It is safe to use "force" here because these are resources currently rendered by the chart.
   334  	err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace, true))
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	// Install requires an extra validation step of checking that resources
   340  	// don't already exist before we actually create resources. If we continue
   341  	// forward and create the release object with resources that already exist,
   342  	// we'll end up in a state where we will delete those resources upon
   343  	// deleting the release because the manifest will be pointing at that
   344  	// resource
   345  	if !i.ClientOnly && !isUpgrade && len(resources) > 0 {
   346  		toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace)
   347  		if err != nil {
   348  			return nil, errors.Wrap(err, "Unable to continue with install")
   349  		}
   350  	}
   351  
   352  	// Bail out here if it is a dry run
   353  	if i.isDryRun() {
   354  		rel.Info.Description = "Dry run complete"
   355  		return rel, nil
   356  	}
   357  
   358  	if i.CreateNamespace {
   359  		ns := &v1.Namespace{
   360  			TypeMeta: metav1.TypeMeta{
   361  				APIVersion: "v1",
   362  				Kind:       "Namespace",
   363  			},
   364  			ObjectMeta: metav1.ObjectMeta{
   365  				Name: i.Namespace,
   366  				Labels: map[string]string{
   367  					"name": i.Namespace,
   368  				},
   369  			},
   370  		}
   371  		buf, err := yaml.Marshal(ns)
   372  		if err != nil {
   373  			return nil, err
   374  		}
   375  		resourceList, err := i.cfg.KubeClient.Build(bytes.NewBuffer(buf), true)
   376  		if err != nil {
   377  			return nil, err
   378  		}
   379  		if _, err := i.cfg.KubeClient.Create(resourceList); err != nil && !apierrors.IsAlreadyExists(err) {
   380  			return nil, err
   381  		}
   382  	}
   383  
   384  	// If Replace is true, we need to supercede the last release.
   385  	if i.Replace {
   386  		if err := i.replaceRelease(rel); err != nil {
   387  			return nil, err
   388  		}
   389  	}
   390  
   391  	// Store the release in history before continuing (new in Helm 3). We always know
   392  	// that this is a create operation.
   393  	if err := i.cfg.Releases.Create(rel); err != nil {
   394  		// We could try to recover gracefully here, but since nothing has been installed
   395  		// yet, this is probably safer than trying to continue when we know storage is
   396  		// not working.
   397  		return rel, err
   398  	}
   399  
   400  	rel, err = i.performInstallCtx(ctx, rel, toBeAdopted, resources)
   401  	if err != nil {
   402  		rel, err = i.failRelease(rel, err)
   403  	}
   404  	return rel, err
   405  }
   406  
   407  func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
   408  	type Msg struct {
   409  		r *release.Release
   410  		e error
   411  	}
   412  	resultChan := make(chan Msg, 1)
   413  
   414  	go func() {
   415  		rel, err := i.performInstall(rel, toBeAdopted, resources)
   416  		resultChan <- Msg{rel, err}
   417  	}()
   418  	select {
   419  	case <-ctx.Done():
   420  		err := ctx.Err()
   421  		return rel, err
   422  	case msg := <-resultChan:
   423  		return msg.r, msg.e
   424  	}
   425  }
   426  
   427  // isDryRun returns true if Upgrade is set to run as a DryRun
   428  func (i *Install) isDryRun() bool {
   429  	if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" {
   430  		return true
   431  	}
   432  	return false
   433  }
   434  
   435  func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
   436  	var err error
   437  	// pre-install hooks
   438  	if !i.DisableHooks {
   439  		if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil {
   440  			return rel, fmt.Errorf("failed pre-install: %s", err)
   441  		}
   442  	}
   443  
   444  	// At this point, we can do the install. Note that before we were detecting whether to
   445  	// do an update, but it's not clear whether we WANT to do an update if the re-use is set
   446  	// to true, since that is basically an upgrade operation.
   447  	if len(toBeAdopted) == 0 && len(resources) > 0 {
   448  		_, err = i.cfg.KubeClient.Create(resources)
   449  	} else if len(resources) > 0 {
   450  		_, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force)
   451  	}
   452  	if err != nil {
   453  		return rel, err
   454  	}
   455  
   456  	if i.Wait {
   457  		if i.WaitForJobs {
   458  			err = i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout)
   459  		} else {
   460  			err = i.cfg.KubeClient.Wait(resources, i.Timeout)
   461  		}
   462  		if err != nil {
   463  			return rel, err
   464  		}
   465  	}
   466  
   467  	if !i.DisableHooks {
   468  		if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil {
   469  			return rel, fmt.Errorf("failed post-install: %s", err)
   470  		}
   471  	}
   472  
   473  	if len(i.Description) > 0 {
   474  		rel.SetStatus(release.StatusDeployed, i.Description)
   475  	} else {
   476  		rel.SetStatus(release.StatusDeployed, "Install complete")
   477  	}
   478  
   479  	// This is a tricky case. The release has been created, but the result
   480  	// cannot be recorded. The truest thing to tell the user is that the
   481  	// release was created. However, the user will not be able to do anything
   482  	// further with this release.
   483  	//
   484  	// One possible strategy would be to do a timed retry to see if we can get
   485  	// this stored in the future.
   486  	if err := i.recordRelease(rel); err != nil {
   487  		i.cfg.Log("failed to record the release: %s", err)
   488  	}
   489  
   490  	return rel, nil
   491  }
   492  
   493  func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
   494  	rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
   495  	if i.Atomic {
   496  		i.cfg.Log("Install failed and atomic is set, uninstalling release")
   497  		uninstall := NewUninstall(i.cfg)
   498  		uninstall.DisableHooks = i.DisableHooks
   499  		uninstall.KeepHistory = false
   500  		uninstall.Timeout = i.Timeout
   501  		if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil {
   502  			return rel, errors.Wrapf(uninstallErr, "an error occurred while uninstalling the release. original install error: %s", err)
   503  		}
   504  		return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled due to atomic being set", i.ReleaseName)
   505  	}
   506  	i.recordRelease(rel) // Ignore the error, since we have another error to deal with.
   507  	return rel, err
   508  }
   509  
   510  // availableName tests whether a name is available
   511  //
   512  // Roughly, this will return an error if name is
   513  //
   514  //   - empty
   515  //   - too long
   516  //   - already in use, and not deleted
   517  //   - used by a deleted release, and i.Replace is false
   518  func (i *Install) availableName() error {
   519  	start := i.ReleaseName
   520  
   521  	if err := chartutil.ValidateReleaseName(start); err != nil {
   522  		return errors.Wrapf(err, "release name %q", start)
   523  	}
   524  	// On dry run, bail here
   525  	if i.isDryRun() {
   526  		return nil
   527  	}
   528  
   529  	h, err := i.cfg.Releases.History(start)
   530  	if err != nil || len(h) < 1 {
   531  		return nil
   532  	}
   533  	releaseutil.Reverse(h, releaseutil.SortByRevision)
   534  	rel := h[0]
   535  
   536  	if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) {
   537  		return nil
   538  	}
   539  	return errors.New("cannot re-use a name that is still in use")
   540  }
   541  
   542  // createRelease creates a new release object
   543  func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release {
   544  	ts := i.cfg.Now()
   545  	return &release.Release{
   546  		Name:      i.ReleaseName,
   547  		Namespace: i.Namespace,
   548  		Chart:     chrt,
   549  		Config:    rawVals,
   550  		Info: &release.Info{
   551  			FirstDeployed: ts,
   552  			LastDeployed:  ts,
   553  			Status:        release.StatusUnknown,
   554  		},
   555  		Version: 1,
   556  		Labels:  labels,
   557  	}
   558  }
   559  
   560  // recordRelease with an update operation in case reuse has been set.
   561  func (i *Install) recordRelease(r *release.Release) error {
   562  	// This is a legacy function which has been reduced to a oneliner. Could probably
   563  	// refactor it out.
   564  	return i.cfg.Releases.Update(r)
   565  }
   566  
   567  // replaceRelease replaces an older release with this one
   568  //
   569  // This allows us to re-use names by superseding an existing release with a new one
   570  func (i *Install) replaceRelease(rel *release.Release) error {
   571  	hist, err := i.cfg.Releases.History(rel.Name)
   572  	if err != nil || len(hist) == 0 {
   573  		// No releases exist for this name, so we can return early
   574  		return nil
   575  	}
   576  
   577  	releaseutil.Reverse(hist, releaseutil.SortByRevision)
   578  	last := hist[0]
   579  
   580  	// Update version to the next available
   581  	rel.Version = last.Version + 1
   582  
   583  	// Do not change the status of a failed release.
   584  	if last.Info.Status == release.StatusFailed {
   585  		return nil
   586  	}
   587  
   588  	// For any other status, mark it as superseded and store the old record
   589  	last.SetStatus(release.StatusSuperseded, "superseded by new release")
   590  	return i.recordRelease(last)
   591  }
   592  
   593  // write the <data> to <output-dir>/<name>. <append> controls if the file is created or content will be appended
   594  func writeToFile(outputDir string, name string, data string, append bool) error {
   595  	outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator))
   596  
   597  	err := ensureDirectoryForFile(outfileName)
   598  	if err != nil {
   599  		return err
   600  	}
   601  
   602  	f, err := createOrOpenFile(outfileName, append)
   603  	if err != nil {
   604  		return err
   605  	}
   606  
   607  	defer f.Close()
   608  
   609  	_, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data))
   610  
   611  	if err != nil {
   612  		return err
   613  	}
   614  
   615  	fmt.Printf("wrote %s\n", outfileName)
   616  	return nil
   617  }
   618  
   619  func createOrOpenFile(filename string, append bool) (*os.File, error) {
   620  	if append {
   621  		return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600)
   622  	}
   623  	return os.Create(filename)
   624  }
   625  
   626  // check if the directory exists to create file. creates if don't exists
   627  func ensureDirectoryForFile(file string) error {
   628  	baseDir := path.Dir(file)
   629  	_, err := os.Stat(baseDir)
   630  	if err != nil && !os.IsNotExist(err) {
   631  		return err
   632  	}
   633  
   634  	return os.MkdirAll(baseDir, defaultDirectoryPermission)
   635  }
   636  
   637  // NameAndChart returns the name and chart that should be used.
   638  //
   639  // This will read the flags and handle name generation if necessary.
   640  func (i *Install) NameAndChart(args []string) (string, string, error) {
   641  	flagsNotSet := func() error {
   642  		if i.GenerateName {
   643  			return errors.New("cannot set --generate-name and also specify a name")
   644  		}
   645  		if i.NameTemplate != "" {
   646  			return errors.New("cannot set --name-template and also specify a name")
   647  		}
   648  		return nil
   649  	}
   650  
   651  	if len(args) > 2 {
   652  		return args[0], args[1], errors.Errorf("expected at most two arguments, unexpected arguments: %v", strings.Join(args[2:], ", "))
   653  	}
   654  
   655  	if len(args) == 2 {
   656  		return args[0], args[1], flagsNotSet()
   657  	}
   658  
   659  	if i.NameTemplate != "" {
   660  		name, err := TemplateName(i.NameTemplate)
   661  		return name, args[0], err
   662  	}
   663  
   664  	if i.ReleaseName != "" {
   665  		return i.ReleaseName, args[0], nil
   666  	}
   667  
   668  	if !i.GenerateName {
   669  		return "", args[0], errors.New("must either provide a name or specify --generate-name")
   670  	}
   671  
   672  	base := filepath.Base(args[0])
   673  	if base == "." || base == "" {
   674  		base = "chart"
   675  	}
   676  	// if present, strip out the file extension from the name
   677  	if idx := strings.Index(base, "."); idx != -1 {
   678  		base = base[0:idx]
   679  	}
   680  
   681  	return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil
   682  }
   683  
   684  // TemplateName renders a name template, returning the name or an error.
   685  func TemplateName(nameTemplate string) (string, error) {
   686  	if nameTemplate == "" {
   687  		return "", nil
   688  	}
   689  
   690  	t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
   691  	if err != nil {
   692  		return "", err
   693  	}
   694  	var b bytes.Buffer
   695  	if err := t.Execute(&b, nil); err != nil {
   696  		return "", err
   697  	}
   698  
   699  	return b.String(), nil
   700  }
   701  
   702  // CheckDependencies checks the dependencies for a chart.
   703  func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error {
   704  	var missing []string
   705  
   706  OUTER:
   707  	for _, r := range reqs {
   708  		for _, d := range ch.Dependencies() {
   709  			if d.Name() == r.Name {
   710  				continue OUTER
   711  			}
   712  		}
   713  		missing = append(missing, r.Name)
   714  	}
   715  
   716  	if len(missing) > 0 {
   717  		return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", "))
   718  	}
   719  	return nil
   720  }
   721  
   722  // LocateChart looks for a chart directory in known places, and returns either the full path or an error.
   723  //
   724  // This does not ensure that the chart is well-formed; only that the requested filename exists.
   725  //
   726  // Order of resolution:
   727  // - relative to current working directory
   728  // - if path is absolute or begins with '.', error out here
   729  // - URL
   730  //
   731  // If 'verify' was set on ChartPathOptions, this will attempt to also verify the chart.
   732  func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (string, error) {
   733  	if registry.IsOCI(name) && c.registryClient == nil {
   734  		return "", fmt.Errorf("unable to lookup chart %q, missing registry client", name)
   735  	}
   736  
   737  	name = strings.TrimSpace(name)
   738  	version := strings.TrimSpace(c.Version)
   739  
   740  	if _, err := os.Stat(name); err == nil {
   741  		abs, err := filepath.Abs(name)
   742  		if err != nil {
   743  			return abs, err
   744  		}
   745  		if c.Verify {
   746  			if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil {
   747  				return "", err
   748  			}
   749  		}
   750  		return abs, nil
   751  	}
   752  	if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
   753  		return name, errors.Errorf("path %q not found", name)
   754  	}
   755  
   756  	dl := downloader.ChartDownloader{
   757  		Out:     os.Stdout,
   758  		Keyring: c.Keyring,
   759  		Getters: getter.All(settings),
   760  		Options: []getter.Option{
   761  			getter.WithPassCredentialsAll(c.PassCredentialsAll),
   762  			getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile),
   763  			getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify),
   764  			getter.WithPlainHTTP(c.PlainHTTP),
   765  		},
   766  		RepositoryConfig: settings.RepositoryConfig,
   767  		RepositoryCache:  settings.RepositoryCache,
   768  		RegistryClient:   c.registryClient,
   769  	}
   770  
   771  	if registry.IsOCI(name) {
   772  		dl.Options = append(dl.Options, getter.WithRegistryClient(c.registryClient))
   773  	}
   774  
   775  	if c.Verify {
   776  		dl.Verify = downloader.VerifyAlways
   777  	}
   778  	if c.RepoURL != "" {
   779  		chartURL, err := repo.FindChartInAuthAndTLSAndPassRepoURL(c.RepoURL, c.Username, c.Password, name, version,
   780  			c.CertFile, c.KeyFile, c.CaFile, c.InsecureSkipTLSverify, c.PassCredentialsAll, getter.All(settings))
   781  		if err != nil {
   782  			return "", err
   783  		}
   784  		name = chartURL
   785  
   786  		// Only pass the user/pass on when the user has said to or when the
   787  		// location of the chart repo and the chart are the same domain.
   788  		u1, err := url.Parse(c.RepoURL)
   789  		if err != nil {
   790  			return "", err
   791  		}
   792  		u2, err := url.Parse(chartURL)
   793  		if err != nil {
   794  			return "", err
   795  		}
   796  
   797  		// Host on URL (returned from url.Parse) contains the port if present.
   798  		// This check ensures credentials are not passed between different
   799  		// services on different ports.
   800  		if c.PassCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) {
   801  			dl.Options = append(dl.Options, getter.WithBasicAuth(c.Username, c.Password))
   802  		} else {
   803  			dl.Options = append(dl.Options, getter.WithBasicAuth("", ""))
   804  		}
   805  	} else {
   806  		dl.Options = append(dl.Options, getter.WithBasicAuth(c.Username, c.Password))
   807  	}
   808  
   809  	if err := os.MkdirAll(settings.RepositoryCache, 0755); err != nil {
   810  		return "", err
   811  	}
   812  
   813  	filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache)
   814  	if err != nil {
   815  		return "", err
   816  	}
   817  
   818  	lname, err := filepath.Abs(filename)
   819  	if err != nil {
   820  		return filename, err
   821  	}
   822  	return lname, nil
   823  }
   824  

View as plain text