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