github.com/azure-devops-engineer/helm@v3.0.0-alpha.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"
    23  	"io/ioutil"
    24  	"net/url"
    25  	"os"
    26  	"path"
    27  	"path/filepath"
    28  	"sort"
    29  	"strings"
    30  	"text/template"
    31  	"time"
    32  
    33  	"github.com/Masterminds/sprig"
    34  	"github.com/pkg/errors"
    35  	"sigs.k8s.io/yaml"
    36  
    37  	"helm.sh/helm/pkg/chart"
    38  	"helm.sh/helm/pkg/chartutil"
    39  	"helm.sh/helm/pkg/cli"
    40  	"helm.sh/helm/pkg/downloader"
    41  	"helm.sh/helm/pkg/engine"
    42  	"helm.sh/helm/pkg/getter"
    43  	"helm.sh/helm/pkg/hooks"
    44  	kubefake "helm.sh/helm/pkg/kube/fake"
    45  	"helm.sh/helm/pkg/release"
    46  	"helm.sh/helm/pkg/releaseutil"
    47  	"helm.sh/helm/pkg/repo"
    48  	"helm.sh/helm/pkg/storage"
    49  	"helm.sh/helm/pkg/storage/driver"
    50  	"helm.sh/helm/pkg/strvals"
    51  	"helm.sh/helm/pkg/version"
    52  )
    53  
    54  // releaseNameMaxLen is the maximum length of a release name.
    55  //
    56  // As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for
    57  // charts to add data. Effectively, that gives us 53 chars.
    58  // See https://github.com/helm/helm/issues/1528
    59  const releaseNameMaxLen = 53
    60  
    61  // NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine
    62  // but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually
    63  // wants to see this file after rendering in the status command. However, it must be a suffix
    64  // since there can be filepath in front of it.
    65  const notesFileSuffix = "NOTES.txt"
    66  
    67  const defaultDirectoryPermission = 0755
    68  
    69  // Install performs an installation operation.
    70  type Install struct {
    71  	cfg *Configuration
    72  
    73  	ChartPathOptions
    74  	ValueOptions
    75  
    76  	ClientOnly       bool
    77  	DryRun           bool
    78  	DisableHooks     bool
    79  	Replace          bool
    80  	Wait             bool
    81  	Devel            bool
    82  	DependencyUpdate bool
    83  	Timeout          time.Duration
    84  	Namespace        string
    85  	ReleaseName      string
    86  	GenerateName     bool
    87  	NameTemplate     string
    88  	OutputDir        string
    89  	Atomic           bool
    90  }
    91  
    92  type ValueOptions struct {
    93  	ValueFiles   []string
    94  	StringValues []string
    95  	Values       []string
    96  	rawValues    map[string]interface{}
    97  }
    98  
    99  type ChartPathOptions struct {
   100  	CaFile   string // --ca-file
   101  	CertFile string // --cert-file
   102  	KeyFile  string // --key-file
   103  	Keyring  string // --keyring
   104  	Password string // --password
   105  	RepoURL  string // --repo
   106  	Username string // --username
   107  	Verify   bool   // --verify
   108  	Version  string // --version
   109  }
   110  
   111  // NewInstall creates a new Install object with the given configuration.
   112  func NewInstall(cfg *Configuration) *Install {
   113  	return &Install{
   114  		cfg: cfg,
   115  	}
   116  }
   117  
   118  // Run executes the installation
   119  //
   120  // If DryRun is set to true, this will prepare the release, but not install it
   121  func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) {
   122  	if err := i.availableName(); err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	if i.ClientOnly {
   127  		// Add mock objects in here so it doesn't use Kube API server
   128  		// NOTE(bacongobbler): used for `helm template`
   129  		i.cfg.Capabilities = chartutil.DefaultCapabilities
   130  		i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}
   131  		i.cfg.Releases = storage.Init(driver.NewMemory())
   132  	}
   133  
   134  	// Make sure if Atomic is set, that wait is set as well. This makes it so
   135  	// the user doesn't have to specify both
   136  	i.Wait = i.Wait || i.Atomic
   137  
   138  	caps, err := i.cfg.getCapabilities()
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	options := chartutil.ReleaseOptions{
   144  		Name:      i.ReleaseName,
   145  		Namespace: i.Namespace,
   146  		IsInstall: true,
   147  	}
   148  	valuesToRender, err := chartutil.ToRenderValues(chrt, i.rawValues, options, caps)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	rel := i.createRelease(chrt, i.rawValues)
   154  	var manifestDoc *bytes.Buffer
   155  	rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.OutputDir)
   156  	// Even for errors, attach this if available
   157  	if manifestDoc != nil {
   158  		rel.Manifest = manifestDoc.String()
   159  	}
   160  	// Check error from render
   161  	if err != nil {
   162  		rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
   163  		// Return a release with partial data so that the client can show debugging information.
   164  		return rel, err
   165  	}
   166  
   167  	// Mark this release as in-progress
   168  	rel.SetStatus(release.StatusPendingInstall, "Initial install underway")
   169  	if err := i.validateManifest(manifestDoc); err != nil {
   170  		return rel, err
   171  	}
   172  
   173  	// Bail out here if it is a dry run
   174  	if i.DryRun {
   175  		rel.Info.Description = "Dry run complete"
   176  		return rel, nil
   177  	}
   178  
   179  	// If Replace is true, we need to supersede the last release.
   180  	if i.Replace {
   181  		if err := i.replaceRelease(rel); err != nil {
   182  			return nil, err
   183  		}
   184  	}
   185  
   186  	// Store the release in history before continuing (new in Helm 3). We always know
   187  	// that this is a create operation.
   188  	if err := i.cfg.Releases.Create(rel); err != nil {
   189  		// We could try to recover gracefully here, but since nothing has been installed
   190  		// yet, this is probably safer than trying to continue when we know storage is
   191  		// not working.
   192  		return rel, err
   193  	}
   194  
   195  	// pre-install hooks
   196  	if !i.DisableHooks {
   197  		if err := i.execHook(rel.Hooks, hooks.PreInstall); err != nil {
   198  			return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err))
   199  		}
   200  	}
   201  
   202  	// At this point, we can do the install. Note that before we were detecting whether to
   203  	// do an update, but it's not clear whether we WANT to do an update if the re-use is set
   204  	// to true, since that is basically an upgrade operation.
   205  	buf := bytes.NewBufferString(rel.Manifest)
   206  	if err := i.cfg.KubeClient.Create(buf); err != nil {
   207  		return i.failRelease(rel, err)
   208  	}
   209  
   210  	if i.Wait {
   211  		buf := bytes.NewBufferString(rel.Manifest)
   212  		if err := i.cfg.KubeClient.Wait(buf, i.Timeout); err != nil {
   213  			return i.failRelease(rel, err)
   214  		}
   215  
   216  	}
   217  
   218  	if !i.DisableHooks {
   219  		if err := i.execHook(rel.Hooks, hooks.PostInstall); err != nil {
   220  			return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err))
   221  		}
   222  	}
   223  
   224  	rel.SetStatus(release.StatusDeployed, "Install complete")
   225  
   226  	// This is a tricky case. The release has been created, but the result
   227  	// cannot be recorded. The truest thing to tell the user is that the
   228  	// release was created. However, the user will not be able to do anything
   229  	// further with this release.
   230  	//
   231  	// One possible strategy would be to do a timed retry to see if we can get
   232  	// this stored in the future.
   233  	i.recordRelease(rel)
   234  
   235  	return rel, nil
   236  }
   237  
   238  func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
   239  	rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
   240  	if i.Atomic {
   241  		i.cfg.Log("Install failed and atomic is set, uninstalling release")
   242  		uninstall := NewUninstall(i.cfg)
   243  		uninstall.DisableHooks = i.DisableHooks
   244  		uninstall.KeepHistory = false
   245  		uninstall.Timeout = i.Timeout
   246  		if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil {
   247  			return rel, errors.Wrapf(uninstallErr, "an error occurred while uninstalling the release. original install error: %s", err)
   248  		}
   249  		return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled due to atomic being set", i.ReleaseName)
   250  	}
   251  	i.recordRelease(rel) // Ignore the error, since we have another error to deal with.
   252  	return rel, err
   253  }
   254  
   255  // availableName tests whether a name is available
   256  //
   257  // Roughly, this will return an error if name is
   258  //
   259  //	- empty
   260  //	- too long
   261  // 	- already in use, and not deleted
   262  //	- used by a deleted release, and i.Replace is false
   263  func (i *Install) availableName() error {
   264  	start := i.ReleaseName
   265  	if start == "" {
   266  		return errors.New("name is required")
   267  	}
   268  
   269  	if len(start) > releaseNameMaxLen {
   270  		return errors.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen)
   271  	}
   272  
   273  	h, err := i.cfg.Releases.History(start)
   274  	if err != nil || len(h) < 1 {
   275  		return nil
   276  	}
   277  	releaseutil.Reverse(h, releaseutil.SortByRevision)
   278  	rel := h[0]
   279  
   280  	if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) {
   281  		return nil
   282  	}
   283  	return errors.New("cannot re-use a name that is still in use")
   284  }
   285  
   286  // createRelease creates a new release object
   287  func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}) *release.Release {
   288  	ts := i.cfg.Now()
   289  	return &release.Release{
   290  		Name:      i.ReleaseName,
   291  		Namespace: i.Namespace,
   292  		Chart:     chrt,
   293  		Config:    rawVals,
   294  		Info: &release.Info{
   295  			FirstDeployed: ts,
   296  			LastDeployed:  ts,
   297  			Status:        release.StatusUnknown,
   298  		},
   299  		Version: 1,
   300  	}
   301  }
   302  
   303  // recordRelease with an update operation in case reuse has been set.
   304  func (i *Install) recordRelease(r *release.Release) error {
   305  	// This is a legacy function which has been reduced to a oneliner. Could probably
   306  	// refactor it out.
   307  	return i.cfg.Releases.Update(r)
   308  }
   309  
   310  // replaceRelease replaces an older release with this one
   311  //
   312  // This allows us to re-use names by superseding an existing release with a new one
   313  func (i *Install) replaceRelease(rel *release.Release) error {
   314  	hist, err := i.cfg.Releases.History(rel.Name)
   315  	if err != nil || len(hist) == 0 {
   316  		// No releases exist for this name, so we can return early
   317  		return nil
   318  	}
   319  
   320  	releaseutil.Reverse(hist, releaseutil.SortByRevision)
   321  	last := hist[0]
   322  
   323  	// Update version to the next available
   324  	rel.Version = last.Version + 1
   325  
   326  	// Do not change the status of a failed release.
   327  	if last.Info.Status == release.StatusFailed {
   328  		return nil
   329  	}
   330  
   331  	// For any other status, mark it as superseded and store the old record
   332  	last.SetStatus(release.StatusSuperseded, "superseded by new release")
   333  	return i.recordRelease(last)
   334  }
   335  
   336  // renderResources renders the templates in a chart
   337  func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, outputDir string) ([]*release.Hook, *bytes.Buffer, string, error) {
   338  	hs := []*release.Hook{}
   339  	b := bytes.NewBuffer(nil)
   340  
   341  	caps, err := c.getCapabilities()
   342  	if err != nil {
   343  		return hs, b, "", err
   344  	}
   345  
   346  	if ch.Metadata.KubeVersion != "" {
   347  		if !version.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
   348  			return hs, b, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
   349  		}
   350  	}
   351  
   352  	files, err := engine.Render(ch, values)
   353  	if err != nil {
   354  		return hs, b, "", err
   355  	}
   356  
   357  	// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
   358  	// pull it out of here into a separate file so that we can actually use the output of the rendered
   359  	// text file. We have to spin through this map because the file contains path information, so we
   360  	// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
   361  	// it in the sortHooks.
   362  	notes := ""
   363  	for k, v := range files {
   364  		if strings.HasSuffix(k, notesFileSuffix) {
   365  			// Only apply the notes if it belongs to the parent chart
   366  			// Note: Do not use filePath.Join since it creates a path with \ which is not expected
   367  			if k == path.Join(ch.Name(), "templates", notesFileSuffix) {
   368  				notes = v
   369  			}
   370  			delete(files, k)
   371  		}
   372  	}
   373  
   374  	// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
   375  	// as partials are not used after renderer.Render. Empty manifests are also
   376  	// removed here.
   377  	hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder)
   378  	if err != nil {
   379  		// By catching parse errors here, we can prevent bogus releases from going
   380  		// to Kubernetes.
   381  		//
   382  		// We return the files as a big blob of data to help the user debug parser
   383  		// errors.
   384  		for name, content := range files {
   385  			if strings.TrimSpace(content) == "" {
   386  				continue
   387  			}
   388  			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
   389  		}
   390  		return hs, b, "", err
   391  	}
   392  
   393  	// Aggregate all valid manifests into one big doc.
   394  	fileWritten := make(map[string]bool)
   395  	for _, m := range manifests {
   396  		if outputDir == "" {
   397  			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
   398  		} else {
   399  			err = writeToFile(outputDir, m.Name, m.Content, fileWritten[m.Name])
   400  			if err != nil {
   401  				return hs, b, "", err
   402  			}
   403  			fileWritten[m.Name] = true
   404  		}
   405  	}
   406  
   407  	return hs, b, notes, nil
   408  }
   409  
   410  // write the <data> to <output-dir>/<name>. <append> controls if the file is created or content will be appended
   411  func writeToFile(outputDir string, name string, data string, append bool) error {
   412  	outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator))
   413  
   414  	err := ensureDirectoryForFile(outfileName)
   415  	if err != nil {
   416  		return err
   417  	}
   418  
   419  	f, err := createOrOpenFile(outfileName, append)
   420  	if err != nil {
   421  		return err
   422  	}
   423  
   424  	defer f.Close()
   425  
   426  	_, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data))
   427  
   428  	if err != nil {
   429  		return err
   430  	}
   431  
   432  	fmt.Printf("wrote %s\n", outfileName)
   433  	return nil
   434  }
   435  
   436  func createOrOpenFile(filename string, append bool) (*os.File, error) {
   437  	if append {
   438  		return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600)
   439  	}
   440  	return os.Create(filename)
   441  }
   442  
   443  // check if the directory exists to create file. creates if don't exists
   444  func ensureDirectoryForFile(file string) error {
   445  	baseDir := path.Dir(file)
   446  	_, err := os.Stat(baseDir)
   447  	if err != nil && !os.IsNotExist(err) {
   448  		return err
   449  	}
   450  
   451  	return os.MkdirAll(baseDir, defaultDirectoryPermission)
   452  }
   453  
   454  // validateManifest checks to see whether the given manifest is valid for the current Kubernetes
   455  func (i *Install) validateManifest(manifest io.Reader) error {
   456  	_, err := i.cfg.KubeClient.BuildUnstructured(manifest)
   457  	return err
   458  }
   459  
   460  // execHook executes all of the hooks for the given hook event.
   461  func (i *Install) execHook(hs []*release.Hook, hook string) error {
   462  	executingHooks := []*release.Hook{}
   463  
   464  	for _, h := range hs {
   465  		for _, e := range h.Events {
   466  			if string(e) == hook {
   467  				executingHooks = append(executingHooks, h)
   468  			}
   469  		}
   470  	}
   471  
   472  	sort.Sort(hookByWeight(executingHooks))
   473  
   474  	for _, h := range executingHooks {
   475  		if err := deleteHookByPolicy(i.cfg, h, hooks.BeforeHookCreation); err != nil {
   476  			return err
   477  		}
   478  
   479  		b := bytes.NewBufferString(h.Manifest)
   480  		if err := i.cfg.KubeClient.Create(b); err != nil {
   481  			return errors.Wrapf(err, "warning: Release %s %s %s failed", i.ReleaseName, hook, h.Path)
   482  		}
   483  		b.Reset()
   484  		b.WriteString(h.Manifest)
   485  
   486  		if err := i.cfg.KubeClient.WatchUntilReady(b, i.Timeout); err != nil {
   487  			// If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
   488  			// under failed condition. If so, then clear the corresponding resource object in the hook
   489  			if err := deleteHookByPolicy(i.cfg, h, hooks.HookFailed); err != nil {
   490  				return err
   491  			}
   492  			return err
   493  		}
   494  	}
   495  
   496  	// If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted
   497  	// under succeeded condition. If so, then clear the corresponding resource object in each hook
   498  	for _, h := range executingHooks {
   499  		if err := deleteHookByPolicy(i.cfg, h, hooks.HookSucceeded); err != nil {
   500  			return err
   501  		}
   502  		h.LastRun = time.Now()
   503  	}
   504  
   505  	return nil
   506  }
   507  
   508  // deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning
   509  // FIXME: Can we refactor this out?
   510  var deletePolices = map[string]release.HookDeletePolicy{
   511  	hooks.HookSucceeded:      release.HookSucceeded,
   512  	hooks.HookFailed:         release.HookFailed,
   513  	hooks.BeforeHookCreation: release.HookBeforeHookCreation,
   514  }
   515  
   516  // hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices
   517  // supported by helm. If so, mark the hook as one should be deleted.
   518  func hookHasDeletePolicy(h *release.Hook, policy string) bool {
   519  	dp, ok := deletePolices[policy]
   520  	if !ok {
   521  		return false
   522  	}
   523  	for _, v := range h.DeletePolicies {
   524  		if dp == v {
   525  			return true
   526  		}
   527  	}
   528  	return false
   529  }
   530  
   531  // hookByWeight is a sorter for hooks
   532  type hookByWeight []*release.Hook
   533  
   534  func (x hookByWeight) Len() int      { return len(x) }
   535  func (x hookByWeight) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
   536  func (x hookByWeight) Less(i, j int) bool {
   537  	if x[i].Weight == x[j].Weight {
   538  		return x[i].Name < x[j].Name
   539  	}
   540  	return x[i].Weight < x[j].Weight
   541  }
   542  
   543  // NameAndChart returns the name and chart that should be used.
   544  //
   545  // This will read the flags and handle name generation if necessary.
   546  func (i *Install) NameAndChart(args []string) (string, string, error) {
   547  	flagsNotSet := func() error {
   548  		if i.GenerateName {
   549  			return errors.New("cannot set --generate-name and also specify a name")
   550  		}
   551  		if i.NameTemplate != "" {
   552  			return errors.New("cannot set --name-template and also specify a name")
   553  		}
   554  		return nil
   555  	}
   556  
   557  	if len(args) == 2 {
   558  		return args[0], args[1], flagsNotSet()
   559  	}
   560  
   561  	if i.NameTemplate != "" {
   562  		name, err := TemplateName(i.NameTemplate)
   563  		return name, args[0], err
   564  	}
   565  
   566  	if i.ReleaseName != "" {
   567  		return i.ReleaseName, args[0], nil
   568  	}
   569  
   570  	if !i.GenerateName {
   571  		return "", args[0], errors.New("must either provide a name or specify --generate-name")
   572  	}
   573  
   574  	base := filepath.Base(args[0])
   575  	if base == "." || base == "" {
   576  		base = "chart"
   577  	}
   578  
   579  	return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil
   580  }
   581  
   582  func TemplateName(nameTemplate string) (string, error) {
   583  	if nameTemplate == "" {
   584  		return "", nil
   585  	}
   586  
   587  	t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
   588  	if err != nil {
   589  		return "", err
   590  	}
   591  	var b bytes.Buffer
   592  	if err := t.Execute(&b, nil); err != nil {
   593  		return "", err
   594  	}
   595  
   596  	return b.String(), nil
   597  }
   598  
   599  func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error {
   600  	var missing []string
   601  
   602  OUTER:
   603  	for _, r := range reqs {
   604  		for _, d := range ch.Dependencies() {
   605  			if d.Name() == r.Name {
   606  				continue OUTER
   607  			}
   608  		}
   609  		missing = append(missing, r.Name)
   610  	}
   611  
   612  	if len(missing) > 0 {
   613  		return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", "))
   614  	}
   615  	return nil
   616  }
   617  
   618  // LocateChart looks for a chart directory in known places, and returns either the full path or an error.
   619  //
   620  // This does not ensure that the chart is well-formed; only that the requested filename exists.
   621  //
   622  // Order of resolution:
   623  // - relative to current working directory
   624  // - if path is absolute or begins with '.', error out here
   625  // - chart repos in $HELM_HOME
   626  // - URL
   627  //
   628  // If 'verify' is true, this will attempt to also verify the chart.
   629  func (c *ChartPathOptions) LocateChart(name string, settings cli.EnvSettings) (string, error) {
   630  	name = strings.TrimSpace(name)
   631  	version := strings.TrimSpace(c.Version)
   632  
   633  	if _, err := os.Stat(name); err == nil {
   634  		abs, err := filepath.Abs(name)
   635  		if err != nil {
   636  			return abs, err
   637  		}
   638  		if c.Verify {
   639  			if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil {
   640  				return "", err
   641  			}
   642  		}
   643  		return abs, nil
   644  	}
   645  	if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
   646  		return name, errors.Errorf("path %q not found", name)
   647  	}
   648  
   649  	crepo := filepath.Join(settings.Home.Repository(), name)
   650  	if _, err := os.Stat(crepo); err == nil {
   651  		return filepath.Abs(crepo)
   652  	}
   653  
   654  	dl := downloader.ChartDownloader{
   655  		HelmHome: settings.Home,
   656  		Out:      os.Stdout,
   657  		Keyring:  c.Keyring,
   658  		Getters:  getter.All(settings),
   659  		Options: []getter.Option{
   660  			getter.WithBasicAuth(c.Username, c.Password),
   661  		},
   662  	}
   663  	if c.Verify {
   664  		dl.Verify = downloader.VerifyAlways
   665  	}
   666  	if c.RepoURL != "" {
   667  		chartURL, err := repo.FindChartInAuthRepoURL(c.RepoURL, c.Username, c.Password, name, version,
   668  			c.CertFile, c.KeyFile, c.CaFile, getter.All(settings))
   669  		if err != nil {
   670  			return "", err
   671  		}
   672  		name = chartURL
   673  	}
   674  
   675  	if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) {
   676  		os.MkdirAll(settings.Home.Archive(), 0744)
   677  	}
   678  
   679  	filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive())
   680  	if err == nil {
   681  		lname, err := filepath.Abs(filename)
   682  		if err != nil {
   683  			return filename, err
   684  		}
   685  		return lname, nil
   686  	} else if settings.Debug {
   687  		return filename, err
   688  	}
   689  
   690  	return filename, errors.Errorf("failed to download %q (hint: running `helm repo update` may help)", name)
   691  }
   692  
   693  // MergeValues merges values from files specified via -f/--values and
   694  // directly via --set or --set-string, marshaling them to YAML
   695  func (v *ValueOptions) MergeValues(settings cli.EnvSettings) error {
   696  	base := map[string]interface{}{}
   697  
   698  	// User specified a values files via -f/--values
   699  	for _, filePath := range v.ValueFiles {
   700  		currentMap := map[string]interface{}{}
   701  
   702  		bytes, err := readFile(filePath, settings)
   703  		if err != nil {
   704  			return err
   705  		}
   706  
   707  		if err := yaml.Unmarshal(bytes, &currentMap); err != nil {
   708  			return errors.Wrapf(err, "failed to parse %s", filePath)
   709  		}
   710  		// Merge with the previous map
   711  		base = mergeMaps(base, currentMap)
   712  	}
   713  
   714  	// User specified a value via --set
   715  	for _, value := range v.Values {
   716  		if err := strvals.ParseInto(value, base); err != nil {
   717  			return errors.Wrap(err, "failed parsing --set data")
   718  		}
   719  	}
   720  
   721  	// User specified a value via --set-string
   722  	for _, value := range v.StringValues {
   723  		if err := strvals.ParseIntoString(value, base); err != nil {
   724  			return errors.Wrap(err, "failed parsing --set-string data")
   725  		}
   726  	}
   727  
   728  	v.rawValues = base
   729  	return nil
   730  }
   731  
   732  func NewValueOptions(values map[string]interface{}) ValueOptions {
   733  	return ValueOptions{
   734  		rawValues: values,
   735  	}
   736  }
   737  
   738  // mergeValues merges source and destination map, preferring values from the source map
   739  func mergeValues(dest, src map[string]interface{}) map[string]interface{} {
   740  	out := make(map[string]interface{})
   741  	for k, v := range dest {
   742  		out[k] = v
   743  	}
   744  	for k, v := range src {
   745  		if _, ok := out[k]; !ok {
   746  			// If the key doesn't exist already, then just set the key to that value
   747  		} else if nextMap, ok := v.(map[string]interface{}); !ok {
   748  			// If it isn't another map, overwrite the value
   749  		} else if destMap, isMap := out[k].(map[string]interface{}); !isMap {
   750  			// Edge case: If the key exists in the destination, but isn't a map
   751  			// If the source map has a map for this key, prefer it
   752  		} else {
   753  			// If we got to this point, it is a map in both, so merge them
   754  			out[k] = mergeValues(destMap, nextMap)
   755  			continue
   756  		}
   757  		out[k] = v
   758  	}
   759  	return out
   760  }
   761  
   762  func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
   763  	out := make(map[string]interface{}, len(a))
   764  	for k, v := range a {
   765  		out[k] = v
   766  	}
   767  	for k, v := range b {
   768  		if v, ok := v.(map[string]interface{}); ok {
   769  			if bv, ok := out[k]; ok {
   770  				if bv, ok := bv.(map[string]interface{}); ok {
   771  					out[k] = mergeMaps(bv, v)
   772  					continue
   773  				}
   774  			}
   775  		}
   776  		out[k] = v
   777  	}
   778  	return out
   779  }
   780  
   781  // readFile load a file from stdin, the local directory, or a remote file with a url.
   782  func readFile(filePath string, settings cli.EnvSettings) ([]byte, error) {
   783  	if strings.TrimSpace(filePath) == "-" {
   784  		return ioutil.ReadAll(os.Stdin)
   785  	}
   786  	u, _ := url.Parse(filePath)
   787  	p := getter.All(settings)
   788  
   789  	// FIXME: maybe someone handle other protocols like ftp.
   790  	getterConstructor, err := p.ByScheme(u.Scheme)
   791  
   792  	if err != nil {
   793  		return ioutil.ReadFile(filePath)
   794  	}
   795  
   796  	getter, err := getterConstructor(getter.WithURL(filePath))
   797  	if err != nil {
   798  		return []byte{}, err
   799  	}
   800  	data, err := getter.Get(filePath)
   801  	return data.Bytes(), err
   802  }