github.com/aaronmell/helm@v3.0.0-beta.2+incompatible/pkg/action/install.go (about)

     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  	"fmt"
    22  	"io/ioutil"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"strings"
    27  	"text/template"
    28  	"time"
    29  
    30  	"github.com/Masterminds/sprig"
    31  	"github.com/pkg/errors"
    32  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    33  
    34  	"helm.sh/helm/pkg/chart"
    35  	"helm.sh/helm/pkg/chartutil"
    36  	"helm.sh/helm/pkg/cli"
    37  	"helm.sh/helm/pkg/downloader"
    38  	"helm.sh/helm/pkg/engine"
    39  	"helm.sh/helm/pkg/getter"
    40  	kubefake "helm.sh/helm/pkg/kube/fake"
    41  	"helm.sh/helm/pkg/release"
    42  	"helm.sh/helm/pkg/releaseutil"
    43  	"helm.sh/helm/pkg/repo"
    44  	"helm.sh/helm/pkg/storage"
    45  	"helm.sh/helm/pkg/storage/driver"
    46  )
    47  
    48  // releaseNameMaxLen is the maximum length of a release name.
    49  //
    50  // As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for
    51  // charts to add data. Effectively, that gives us 53 chars.
    52  // See https://github.com/helm/helm/issues/1528
    53  const releaseNameMaxLen = 53
    54  
    55  // NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine
    56  // but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually
    57  // wants to see this file after rendering in the status command. However, it must be a suffix
    58  // since there can be filepath in front of it.
    59  const notesFileSuffix = "NOTES.txt"
    60  
    61  const defaultDirectoryPermission = 0755
    62  
    63  // Install performs an installation operation.
    64  type Install struct {
    65  	cfg *Configuration
    66  
    67  	ChartPathOptions
    68  
    69  	ClientOnly       bool
    70  	DryRun           bool
    71  	DisableHooks     bool
    72  	Replace          bool
    73  	Wait             bool
    74  	Devel            bool
    75  	DependencyUpdate bool
    76  	Timeout          time.Duration
    77  	Namespace        string
    78  	ReleaseName      string
    79  	GenerateName     bool
    80  	NameTemplate     string
    81  	OutputDir        string
    82  	Atomic           bool
    83  	SkipCRDs         bool
    84  }
    85  
    86  // ChartPathOptions captures common options used for controlling chart paths
    87  type ChartPathOptions struct {
    88  	CaFile   string // --ca-file
    89  	CertFile string // --cert-file
    90  	KeyFile  string // --key-file
    91  	Keyring  string // --keyring
    92  	Password string // --password
    93  	RepoURL  string // --repo
    94  	Username string // --username
    95  	Verify   bool   // --verify
    96  	Version  string // --version
    97  }
    98  
    99  // NewInstall creates a new Install object with the given configuration.
   100  func NewInstall(cfg *Configuration) *Install {
   101  	return &Install{
   102  		cfg: cfg,
   103  	}
   104  }
   105  
   106  // Run executes the installation
   107  //
   108  // If DryRun is set to true, this will prepare the release, but not install it
   109  func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
   110  	if err := i.availableName(); err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	if i.ClientOnly {
   115  		// Add mock objects in here so it doesn't use Kube API server
   116  		// NOTE(bacongobbler): used for `helm template`
   117  		i.cfg.Capabilities = chartutil.DefaultCapabilities
   118  		i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}
   119  		i.cfg.Releases = storage.Init(driver.NewMemory())
   120  	}
   121  
   122  	if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	// Make sure if Atomic is set, that wait is set as well. This makes it so
   127  	// the user doesn't have to specify both
   128  	i.Wait = i.Wait || i.Atomic
   129  
   130  	caps, err := i.cfg.getCapabilities()
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  
   135  	options := chartutil.ReleaseOptions{
   136  		Name:      i.ReleaseName,
   137  		Namespace: i.Namespace,
   138  		IsInstall: true,
   139  	}
   140  	valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	rel := i.createRelease(chrt, vals)
   146  
   147  	// Pre-install anything in the crd/ directory
   148  	if crds := chrt.CRDs(); !i.SkipCRDs && len(crds) > 0 {
   149  		// We do these one at a time in the order they were read.
   150  		for _, obj := range crds {
   151  			// Read in the resources
   152  			res, err := i.cfg.KubeClient.Build(bytes.NewBuffer(obj.Data))
   153  			if err != nil {
   154  				// We bail out immediately
   155  				return nil, errors.Wrapf(err, "failed to install CRD %s", obj.Name)
   156  			}
   157  			// On dry run, bail here
   158  			if i.DryRun {
   159  				i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
   160  				continue
   161  			}
   162  			// Send them to Kube
   163  			if _, err := i.cfg.KubeClient.Create(res); err != nil {
   164  				// If the error is CRD already exists, continue.
   165  				if apierrors.IsAlreadyExists(err) {
   166  					crdName := res[0].Name
   167  					i.cfg.Log("CRD %s is already present. Skipping.", crdName)
   168  					continue
   169  				}
   170  				return i.failRelease(rel, err)
   171  			}
   172  		}
   173  	}
   174  
   175  	var manifestDoc *bytes.Buffer
   176  	rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.OutputDir)
   177  	// Even for errors, attach this if available
   178  	if manifestDoc != nil {
   179  		rel.Manifest = manifestDoc.String()
   180  	}
   181  	// Check error from render
   182  	if err != nil {
   183  		rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
   184  		// Return a release with partial data so that the client can show debugging information.
   185  		return rel, err
   186  	}
   187  
   188  	// Mark this release as in-progress
   189  	rel.SetStatus(release.StatusPendingInstall, "Initial install underway")
   190  
   191  	resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest))
   192  	if err != nil {
   193  		return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest")
   194  	}
   195  
   196  	// Bail out here if it is a dry run
   197  	if i.DryRun {
   198  		rel.Info.Description = "Dry run complete"
   199  		return rel, nil
   200  	}
   201  
   202  	// If Replace is true, we need to supercede the last release.
   203  	if i.Replace {
   204  		if err := i.replaceRelease(rel); err != nil {
   205  			return nil, err
   206  		}
   207  	}
   208  
   209  	// Store the release in history before continuing (new in Helm 3). We always know
   210  	// that this is a create operation.
   211  	if err := i.cfg.Releases.Create(rel); err != nil {
   212  		// We could try to recover gracefully here, but since nothing has been installed
   213  		// yet, this is probably safer than trying to continue when we know storage is
   214  		// not working.
   215  		return rel, err
   216  	}
   217  
   218  	// pre-install hooks
   219  	if !i.DisableHooks {
   220  		if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil {
   221  			return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err))
   222  		}
   223  	}
   224  
   225  	// At this point, we can do the install. Note that before we were detecting whether to
   226  	// do an update, but it's not clear whether we WANT to do an update if the re-use is set
   227  	// to true, since that is basically an upgrade operation.
   228  	if _, err := i.cfg.KubeClient.Create(resources); err != nil {
   229  		return i.failRelease(rel, err)
   230  	}
   231  
   232  	if i.Wait {
   233  		if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil {
   234  			return i.failRelease(rel, err)
   235  		}
   236  
   237  	}
   238  
   239  	if !i.DisableHooks {
   240  		if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil {
   241  			return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err))
   242  		}
   243  	}
   244  
   245  	rel.SetStatus(release.StatusDeployed, "Install complete")
   246  
   247  	// This is a tricky case. The release has been created, but the result
   248  	// cannot be recorded. The truest thing to tell the user is that the
   249  	// release was created. However, the user will not be able to do anything
   250  	// further with this release.
   251  	//
   252  	// One possible strategy would be to do a timed retry to see if we can get
   253  	// this stored in the future.
   254  	i.recordRelease(rel)
   255  
   256  	return rel, nil
   257  }
   258  
   259  func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
   260  	rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
   261  	if i.Atomic {
   262  		i.cfg.Log("Install failed and atomic is set, uninstalling release")
   263  		uninstall := NewUninstall(i.cfg)
   264  		uninstall.DisableHooks = i.DisableHooks
   265  		uninstall.KeepHistory = false
   266  		uninstall.Timeout = i.Timeout
   267  		if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil {
   268  			return rel, errors.Wrapf(uninstallErr, "an error occurred while uninstalling the release. original install error: %s", err)
   269  		}
   270  		return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled due to atomic being set", i.ReleaseName)
   271  	}
   272  	i.recordRelease(rel) // Ignore the error, since we have another error to deal with.
   273  	return rel, err
   274  }
   275  
   276  // availableName tests whether a name is available
   277  //
   278  // Roughly, this will return an error if name is
   279  //
   280  //	- empty
   281  //	- too long
   282  //	- already in use, and not deleted
   283  //	- used by a deleted release, and i.Replace is false
   284  func (i *Install) availableName() error {
   285  	start := i.ReleaseName
   286  	if start == "" {
   287  		return errors.New("name is required")
   288  	}
   289  
   290  	if len(start) > releaseNameMaxLen {
   291  		return errors.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen)
   292  	}
   293  
   294  	if i.DryRun {
   295  		return nil
   296  	}
   297  
   298  	h, err := i.cfg.Releases.History(start)
   299  	if err != nil || len(h) < 1 {
   300  		return nil
   301  	}
   302  	releaseutil.Reverse(h, releaseutil.SortByRevision)
   303  	rel := h[0]
   304  
   305  	if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) {
   306  		return nil
   307  	}
   308  	return errors.New("cannot re-use a name that is still in use")
   309  }
   310  
   311  // createRelease creates a new release object
   312  func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}) *release.Release {
   313  	ts := i.cfg.Now()
   314  	return &release.Release{
   315  		Name:      i.ReleaseName,
   316  		Namespace: i.Namespace,
   317  		Chart:     chrt,
   318  		Config:    rawVals,
   319  		Info: &release.Info{
   320  			FirstDeployed: ts,
   321  			LastDeployed:  ts,
   322  			Status:        release.StatusUnknown,
   323  		},
   324  		Version: 1,
   325  	}
   326  }
   327  
   328  // recordRelease with an update operation in case reuse has been set.
   329  func (i *Install) recordRelease(r *release.Release) error {
   330  	// This is a legacy function which has been reduced to a oneliner. Could probably
   331  	// refactor it out.
   332  	return i.cfg.Releases.Update(r)
   333  }
   334  
   335  // replaceRelease replaces an older release with this one
   336  //
   337  // This allows us to re-use names by superseding an existing release with a new one
   338  func (i *Install) replaceRelease(rel *release.Release) error {
   339  	hist, err := i.cfg.Releases.History(rel.Name)
   340  	if err != nil || len(hist) == 0 {
   341  		// No releases exist for this name, so we can return early
   342  		return nil
   343  	}
   344  
   345  	releaseutil.Reverse(hist, releaseutil.SortByRevision)
   346  	last := hist[0]
   347  
   348  	// Update version to the next available
   349  	rel.Version = last.Version + 1
   350  
   351  	// Do not change the status of a failed release.
   352  	if last.Info.Status == release.StatusFailed {
   353  		return nil
   354  	}
   355  
   356  	// For any other status, mark it as superseded and store the old record
   357  	last.SetStatus(release.StatusSuperseded, "superseded by new release")
   358  	return i.recordRelease(last)
   359  }
   360  
   361  // renderResources renders the templates in a chart
   362  func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, outputDir string) ([]*release.Hook, *bytes.Buffer, string, error) {
   363  	hs := []*release.Hook{}
   364  	b := bytes.NewBuffer(nil)
   365  
   366  	caps, err := c.getCapabilities()
   367  	if err != nil {
   368  		return hs, b, "", err
   369  	}
   370  
   371  	if ch.Metadata.KubeVersion != "" {
   372  		if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
   373  			return hs, b, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
   374  		}
   375  	}
   376  
   377  	files, err := engine.Render(ch, values)
   378  	if err != nil {
   379  		return hs, b, "", err
   380  	}
   381  
   382  	// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
   383  	// pull it out of here into a separate file so that we can actually use the output of the rendered
   384  	// text file. We have to spin through this map because the file contains path information, so we
   385  	// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
   386  	// it in the sortHooks.
   387  	notes := ""
   388  	for k, v := range files {
   389  		if strings.HasSuffix(k, notesFileSuffix) {
   390  			// Only apply the notes if it belongs to the parent chart
   391  			// Note: Do not use filePath.Join since it creates a path with \ which is not expected
   392  			if k == path.Join(ch.Name(), "templates", notesFileSuffix) {
   393  				notes = v
   394  			}
   395  			delete(files, k)
   396  		}
   397  	}
   398  
   399  	// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
   400  	// as partials are not used after renderer.Render. Empty manifests are also
   401  	// removed here.
   402  	hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder)
   403  	if err != nil {
   404  		// By catching parse errors here, we can prevent bogus releases from going
   405  		// to Kubernetes.
   406  		//
   407  		// We return the files as a big blob of data to help the user debug parser
   408  		// errors.
   409  		for name, content := range files {
   410  			if strings.TrimSpace(content) == "" {
   411  				continue
   412  			}
   413  			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
   414  		}
   415  		return hs, b, "", err
   416  	}
   417  
   418  	// Aggregate all valid manifests into one big doc.
   419  	fileWritten := make(map[string]bool)
   420  	for _, m := range manifests {
   421  		if outputDir == "" {
   422  			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
   423  		} else {
   424  			err = writeToFile(outputDir, m.Name, m.Content, fileWritten[m.Name])
   425  			if err != nil {
   426  				return hs, b, "", err
   427  			}
   428  			fileWritten[m.Name] = true
   429  		}
   430  	}
   431  
   432  	return hs, b, notes, nil
   433  }
   434  
   435  // write the <data> to <output-dir>/<name>. <append> controls if the file is created or content will be appended
   436  func writeToFile(outputDir string, name string, data string, append bool) error {
   437  	outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator))
   438  
   439  	err := ensureDirectoryForFile(outfileName)
   440  	if err != nil {
   441  		return err
   442  	}
   443  
   444  	f, err := createOrOpenFile(outfileName, append)
   445  	if err != nil {
   446  		return err
   447  	}
   448  
   449  	defer f.Close()
   450  
   451  	_, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data))
   452  
   453  	if err != nil {
   454  		return err
   455  	}
   456  
   457  	fmt.Printf("wrote %s\n", outfileName)
   458  	return nil
   459  }
   460  
   461  func createOrOpenFile(filename string, append bool) (*os.File, error) {
   462  	if append {
   463  		return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600)
   464  	}
   465  	return os.Create(filename)
   466  }
   467  
   468  // check if the directory exists to create file. creates if don't exists
   469  func ensureDirectoryForFile(file string) error {
   470  	baseDir := path.Dir(file)
   471  	_, err := os.Stat(baseDir)
   472  	if err != nil && !os.IsNotExist(err) {
   473  		return err
   474  	}
   475  
   476  	return os.MkdirAll(baseDir, defaultDirectoryPermission)
   477  }
   478  
   479  // NameAndChart returns the name and chart that should be used.
   480  //
   481  // This will read the flags and handle name generation if necessary.
   482  func (i *Install) NameAndChart(args []string) (string, string, error) {
   483  	flagsNotSet := func() error {
   484  		if i.GenerateName {
   485  			return errors.New("cannot set --generate-name and also specify a name")
   486  		}
   487  		if i.NameTemplate != "" {
   488  			return errors.New("cannot set --name-template and also specify a name")
   489  		}
   490  		return nil
   491  	}
   492  
   493  	if len(args) == 2 {
   494  		return args[0], args[1], flagsNotSet()
   495  	}
   496  
   497  	if i.NameTemplate != "" {
   498  		name, err := TemplateName(i.NameTemplate)
   499  		return name, args[0], err
   500  	}
   501  
   502  	if i.ReleaseName != "" {
   503  		return i.ReleaseName, args[0], nil
   504  	}
   505  
   506  	if !i.GenerateName {
   507  		return "", args[0], errors.New("must either provide a name or specify --generate-name")
   508  	}
   509  
   510  	base := filepath.Base(args[0])
   511  	if base == "." || base == "" {
   512  		base = "chart"
   513  	}
   514  
   515  	return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil
   516  }
   517  
   518  // TemplateName renders a name template, returning the name or an error.
   519  func TemplateName(nameTemplate string) (string, error) {
   520  	if nameTemplate == "" {
   521  		return "", nil
   522  	}
   523  
   524  	t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
   525  	if err != nil {
   526  		return "", err
   527  	}
   528  	var b bytes.Buffer
   529  	if err := t.Execute(&b, nil); err != nil {
   530  		return "", err
   531  	}
   532  
   533  	return b.String(), nil
   534  }
   535  
   536  // CheckDependencies checks the dependencies for a chart.
   537  func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error {
   538  	var missing []string
   539  
   540  OUTER:
   541  	for _, r := range reqs {
   542  		for _, d := range ch.Dependencies() {
   543  			if d.Name() == r.Name {
   544  				continue OUTER
   545  			}
   546  		}
   547  		missing = append(missing, r.Name)
   548  	}
   549  
   550  	if len(missing) > 0 {
   551  		return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", "))
   552  	}
   553  	return nil
   554  }
   555  
   556  // LocateChart looks for a chart directory in known places, and returns either the full path or an error.
   557  //
   558  // This does not ensure that the chart is well-formed; only that the requested filename exists.
   559  //
   560  // Order of resolution:
   561  // - relative to current working directory
   562  // - if path is absolute or begins with '.', error out here
   563  // - URL
   564  //
   565  // If 'verify' was set on ChartPathOptions, this will attempt to also verify the chart.
   566  func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (string, error) {
   567  	name = strings.TrimSpace(name)
   568  	version := strings.TrimSpace(c.Version)
   569  
   570  	if _, err := os.Stat(name); err == nil {
   571  		abs, err := filepath.Abs(name)
   572  		if err != nil {
   573  			return abs, err
   574  		}
   575  		if c.Verify {
   576  			if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil {
   577  				return "", err
   578  			}
   579  		}
   580  		return abs, nil
   581  	}
   582  	if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
   583  		return name, errors.Errorf("path %q not found", name)
   584  	}
   585  
   586  	dl := downloader.ChartDownloader{
   587  		Out:     os.Stdout,
   588  		Keyring: c.Keyring,
   589  		Getters: getter.All(settings),
   590  		Options: []getter.Option{
   591  			getter.WithBasicAuth(c.Username, c.Password),
   592  		},
   593  		RepositoryConfig: settings.RepositoryConfig,
   594  		RepositoryCache:  settings.RepositoryCache,
   595  	}
   596  	if c.Verify {
   597  		dl.Verify = downloader.VerifyAlways
   598  	}
   599  	if c.RepoURL != "" {
   600  		chartURL, err := repo.FindChartInAuthRepoURL(c.RepoURL, c.Username, c.Password, name, version,
   601  			c.CertFile, c.KeyFile, c.CaFile, getter.All(settings))
   602  		if err != nil {
   603  			return "", err
   604  		}
   605  		name = chartURL
   606  	}
   607  
   608  	if err := os.MkdirAll(settings.RepositoryCache, 0755); err != nil {
   609  		return "", err
   610  	}
   611  
   612  	filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache)
   613  	if err == nil {
   614  		lname, err := filepath.Abs(filename)
   615  		if err != nil {
   616  			return filename, err
   617  		}
   618  		return lname, nil
   619  	} else if settings.Debug {
   620  		return filename, err
   621  	}
   622  
   623  	return filename, errors.Errorf("failed to download %q (hint: running `helm repo update` may help)", name)
   624  }