github.com/koderover/helm@v2.17.0+incompatible/cmd/helm/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 main
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/url"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  	"text/template"
    30  
    31  	"github.com/Masterminds/sprig"
    32  	"github.com/ghodss/yaml"
    33  	"github.com/spf13/cobra"
    34  
    35  	"k8s.io/apimachinery/pkg/util/validation"
    36  	"k8s.io/helm/pkg/chartutil"
    37  	"k8s.io/helm/pkg/downloader"
    38  	"k8s.io/helm/pkg/getter"
    39  	"k8s.io/helm/pkg/helm"
    40  	"k8s.io/helm/pkg/kube"
    41  	"k8s.io/helm/pkg/proto/hapi/release"
    42  	"k8s.io/helm/pkg/renderutil"
    43  	"k8s.io/helm/pkg/repo"
    44  	"k8s.io/helm/pkg/strvals"
    45  )
    46  
    47  const installDesc = `
    48  This command installs a chart archive.
    49  
    50  The install argument must be a chart reference, a path to a packaged chart,
    51  a path to an unpacked chart directory or a URL.
    52  
    53  To override values in a chart, use either the '--values' flag and pass in a file
    54  or use the '--set' flag and pass configuration from the command line.  To force string
    55  values in '--set', use '--set-string' instead. In case a value is large and therefore
    56  you want not to use neither '--values' nor '--set', use '--set-file' to read the
    57  single large value from file.
    58  
    59  	$ helm install -f myvalues.yaml ./redis
    60  
    61  or
    62  
    63  	$ helm install --set name=prod ./redis
    64  
    65  or
    66  
    67  	$ helm install --set-string long_int=1234567890 ./redis
    68  
    69  or
    70      $ helm install --set-file multiline_text=path/to/textfile
    71  
    72  You can specify the '--values'/'-f' flag multiple times. The priority will be given to the
    73  last (right-most) file specified. For example, if both myvalues.yaml and override.yaml
    74  contained a key called 'Test', the value set in override.yaml would take precedence:
    75  
    76  	$ helm install -f myvalues.yaml -f override.yaml ./redis
    77  
    78  You can specify the '--set' flag multiple times. The priority will be given to the
    79  last (right-most) set specified. For example, if both 'bar' and 'newbar' values are
    80  set for a key called 'foo', the 'newbar' value would take precedence:
    81  
    82  	$ helm install --set foo=bar --set foo=newbar ./redis
    83  
    84  
    85  To check the generated manifests of a release without installing the chart,
    86  the '--debug' and '--dry-run' flags can be combined. This will still require a
    87  round-trip to the Tiller server.
    88  
    89  If --verify is set, the chart MUST have a provenance file, and the provenance
    90  file MUST pass all verification steps.
    91  
    92  There are five different ways you can express the chart you want to install:
    93  
    94  1. By chart reference: helm install stable/mariadb
    95  2. By path to a packaged chart: helm install ./nginx-1.2.3.tgz
    96  3. By path to an unpacked chart directory: helm install ./nginx
    97  4. By absolute URL: helm install https://example.com/charts/nginx-1.2.3.tgz
    98  5. By chart reference and repo url: helm install --repo https://example.com/charts/ nginx
    99  
   100  CHART REFERENCES
   101  
   102  A chart reference is a convenient way of reference a chart in a chart repository.
   103  
   104  When you use a chart reference with a repo prefix ('stable/mariadb'), Helm will look in the local
   105  configuration for a chart repository named 'stable', and will then look for a
   106  chart in that repository whose name is 'mariadb'. It will install the latest
   107  version of that chart unless you also supply a version number with the
   108  '--version' flag.
   109  
   110  To see the list of chart repositories, use 'helm repo list'. To search for
   111  charts in a repository, use 'helm search'.
   112  `
   113  
   114  type installCmd struct {
   115  	name           string
   116  	namespace      string
   117  	valueFiles     valueFiles
   118  	chartPath      string
   119  	dryRun         bool
   120  	disableHooks   bool
   121  	disableCRDHook bool
   122  	replace        bool
   123  	verify         bool
   124  	keyring        string
   125  	out            io.Writer
   126  	client         helm.Interface
   127  	values         []string
   128  	stringValues   []string
   129  	fileValues     []string
   130  	nameTemplate   string
   131  	version        string
   132  	timeout        int64
   133  	wait           bool
   134  	atomic         bool
   135  	repoURL        string
   136  	username       string
   137  	password       string
   138  	devel          bool
   139  	depUp          bool
   140  	subNotes       bool
   141  	description    string
   142  
   143  	certFile string
   144  	keyFile  string
   145  	caFile   string
   146  	output   string
   147  }
   148  
   149  type valueFiles []string
   150  
   151  func (v *valueFiles) String() string {
   152  	return fmt.Sprint(*v)
   153  }
   154  
   155  func (v *valueFiles) Type() string {
   156  	return "valueFiles"
   157  }
   158  
   159  func (v *valueFiles) Set(value string) error {
   160  	for _, filePath := range strings.Split(value, ",") {
   161  		*v = append(*v, filePath)
   162  	}
   163  	return nil
   164  }
   165  
   166  func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
   167  	inst := &installCmd{
   168  		out:    out,
   169  		client: c,
   170  	}
   171  
   172  	cmd := &cobra.Command{
   173  		Use:     "install [CHART]",
   174  		Short:   "Install a chart archive",
   175  		Long:    installDesc,
   176  		PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() },
   177  		RunE: func(cmd *cobra.Command, args []string) error {
   178  			if err := checkArgsLength(len(args), "chart name"); err != nil {
   179  				return err
   180  			}
   181  
   182  			debug("Original chart version: %q", inst.version)
   183  			if inst.version == "" && inst.devel {
   184  				debug("setting version to >0.0.0-0")
   185  				inst.version = ">0.0.0-0"
   186  			}
   187  
   188  			cp, err := locateChartPath(inst.repoURL, inst.username, inst.password, args[0], inst.version, inst.verify, inst.keyring,
   189  				inst.certFile, inst.keyFile, inst.caFile)
   190  			if err != nil {
   191  				return err
   192  			}
   193  			inst.chartPath = cp
   194  			inst.client = ensureHelmClient(inst.client)
   195  			inst.wait = inst.wait || inst.atomic
   196  
   197  			return inst.run()
   198  		},
   199  	}
   200  
   201  	f := cmd.Flags()
   202  	settings.AddFlagsTLS(f)
   203  	f.VarP(&inst.valueFiles, "values", "f", "Specify values in a YAML file or a URL(can specify multiple)")
   204  	f.StringVarP(&inst.name, "name", "n", "", "The release name. If unspecified, it will autogenerate one for you")
   205  	f.StringVar(&inst.namespace, "namespace", "", "Namespace to install the release into. Defaults to the current kube config namespace.")
   206  	f.BoolVar(&inst.dryRun, "dry-run", false, "Simulate an install")
   207  	f.BoolVar(&inst.disableHooks, "no-hooks", false, "Prevent hooks from running during install")
   208  	f.BoolVar(&inst.disableCRDHook, "no-crd-hook", false, "Prevent CRD hooks from running, but run other hooks")
   209  	f.BoolVar(&inst.replace, "replace", false, "Re-use the given name, even if that name is already used. This is unsafe in production")
   210  	f.StringArrayVar(&inst.values, "set", []string{}, "Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
   211  	f.StringArrayVar(&inst.stringValues, "set-string", []string{}, "Set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
   212  	f.StringArrayVar(&inst.fileValues, "set-file", []string{}, "Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
   213  	f.StringVar(&inst.nameTemplate, "name-template", "", "Specify template used to name the release")
   214  	f.BoolVar(&inst.verify, "verify", false, "Verify the package before installing it")
   215  	f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "Location of public keys used for verification")
   216  	f.StringVar(&inst.version, "version", "", "Specify the exact chart version to install. If this is not specified, the latest version is installed")
   217  	f.Int64Var(&inst.timeout, "timeout", 300, "Time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)")
   218  	f.BoolVar(&inst.wait, "wait", false, "If set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout")
   219  	f.BoolVar(&inst.atomic, "atomic", false, "If set, installation process purges chart on fail, also sets --wait flag")
   220  	f.StringVar(&inst.repoURL, "repo", "", "Chart repository url where to locate the requested chart")
   221  	f.StringVar(&inst.username, "username", "", "Chart repository username where to locate the requested chart")
   222  	f.StringVar(&inst.password, "password", "", "Chart repository password where to locate the requested chart")
   223  	f.StringVar(&inst.certFile, "cert-file", "", "Identify HTTPS client using this SSL certificate file")
   224  	f.StringVar(&inst.keyFile, "key-file", "", "Identify HTTPS client using this SSL key file")
   225  	f.StringVar(&inst.caFile, "ca-file", "", "Verify certificates of HTTPS-enabled servers using this CA bundle")
   226  	f.BoolVar(&inst.devel, "devel", false, "Use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.")
   227  	f.BoolVar(&inst.depUp, "dep-up", false, "Run helm dependency update before installing the chart")
   228  	f.BoolVar(&inst.subNotes, "render-subchart-notes", false, "Render subchart notes along with the parent")
   229  	f.StringVar(&inst.description, "description", "", "Specify a description for the release")
   230  	bindOutputFlag(cmd, &inst.output)
   231  
   232  	// set defaults from environment
   233  	settings.InitTLS(f)
   234  
   235  	return cmd
   236  }
   237  
   238  func (i *installCmd) run() error {
   239  	debug("CHART PATH: %s\n", i.chartPath)
   240  
   241  	if i.namespace == "" {
   242  		i.namespace = defaultNamespace()
   243  	}
   244  
   245  	rawVals, err := vals(i.valueFiles, i.values, i.stringValues, i.fileValues, i.certFile, i.keyFile, i.caFile)
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	// If template is specified, try to run the template.
   251  	if i.nameTemplate != "" {
   252  		i.name, err = generateName(i.nameTemplate)
   253  		if err != nil {
   254  			return err
   255  		}
   256  		// Print the final name so the user knows what the final name of the release is.
   257  		fmt.Printf("FINAL NAME: %s\n", i.name)
   258  	}
   259  
   260  	if msgs := validation.IsDNS1123Subdomain(i.name); i.name != "" && len(msgs) > 0 {
   261  		return fmt.Errorf("release name %s is invalid: %s", i.name, strings.Join(msgs, ";"))
   262  	}
   263  
   264  	// Check chart requirements to make sure all dependencies are present in /charts
   265  	chartRequested, err := chartutil.Load(i.chartPath)
   266  	if err != nil {
   267  		return prettyError(err)
   268  	}
   269  
   270  	if chartRequested.Metadata.Deprecated {
   271  		fmt.Fprintln(os.Stderr, "WARNING: This chart is deprecated")
   272  	}
   273  
   274  	if req, err := chartutil.LoadRequirements(chartRequested); err == nil {
   275  		// If checkDependencies returns an error, we have unfulfilled dependencies.
   276  		// As of Helm 2.4.0, this is treated as a stopping condition:
   277  		// https://github.com/kubernetes/helm/issues/2209
   278  		if err := renderutil.CheckDependencies(chartRequested, req); err != nil {
   279  			if i.depUp {
   280  				man := &downloader.Manager{
   281  					Out:        i.out,
   282  					ChartPath:  i.chartPath,
   283  					HelmHome:   settings.Home,
   284  					Keyring:    defaultKeyring(),
   285  					SkipUpdate: false,
   286  					Getters:    getter.All(settings),
   287  				}
   288  				if err := man.Update(); err != nil {
   289  					return prettyError(err)
   290  				}
   291  
   292  				// Update all dependencies which are present in /charts.
   293  				chartRequested, err = chartutil.Load(i.chartPath)
   294  				if err != nil {
   295  					return prettyError(err)
   296  				}
   297  			} else {
   298  				return prettyError(err)
   299  			}
   300  
   301  		}
   302  	} else if err != chartutil.ErrRequirementsNotFound {
   303  		return fmt.Errorf("cannot load requirements: %v", err)
   304  	}
   305  
   306  	res, err := i.client.InstallReleaseFromChart(
   307  		chartRequested,
   308  		i.namespace,
   309  		helm.ValueOverrides(rawVals),
   310  		helm.ReleaseName(i.name),
   311  		helm.InstallDryRun(i.dryRun),
   312  		helm.InstallReuseName(i.replace),
   313  		helm.InstallDisableHooks(i.disableHooks),
   314  		helm.InstallDisableCRDHook(i.disableCRDHook),
   315  		helm.InstallSubNotes(i.subNotes),
   316  		helm.InstallTimeout(i.timeout),
   317  		helm.InstallWait(i.wait),
   318  		helm.InstallDescription(i.description))
   319  	if err != nil {
   320  		if i.atomic {
   321  			fmt.Fprintf(os.Stdout, "INSTALL FAILED\nPURGING CHART\nError: %v\n", prettyError(err))
   322  			deleteSideEffects := &deleteCmd{
   323  				name:         i.name,
   324  				disableHooks: i.disableHooks,
   325  				purge:        true,
   326  				timeout:      i.timeout,
   327  				description:  "",
   328  				dryRun:       i.dryRun,
   329  				out:          i.out,
   330  				client:       i.client,
   331  			}
   332  			if err := deleteSideEffects.run(); err != nil {
   333  				return err
   334  			}
   335  			fmt.Fprintf(os.Stdout, "Successfully purged a chart!\n")
   336  		}
   337  		return prettyError(err)
   338  	}
   339  
   340  	rel := res.GetRelease()
   341  	if rel == nil {
   342  		return nil
   343  	}
   344  
   345  	if outputFormat(i.output) == outputTable {
   346  		i.printRelease(rel)
   347  	}
   348  
   349  	// If this is a dry run, we can't display status.
   350  	if i.dryRun {
   351  		// This is special casing to avoid breaking backward compatibility:
   352  		if res.Release.Info.Description != "Dry run complete" {
   353  			fmt.Fprintf(os.Stdout, "WARNING: %s\n", res.Release.Info.Description)
   354  		}
   355  		return nil
   356  	}
   357  
   358  	// Print the status like status command does
   359  	status, err := i.client.ReleaseStatus(rel.Name)
   360  	if err != nil {
   361  		return prettyError(err)
   362  	}
   363  
   364  	return write(i.out, &statusWriter{status}, outputFormat(i.output))
   365  }
   366  
   367  // Merges source and destination map, preferring values from the source map
   368  func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
   369  	for k, v := range src {
   370  		// If the key doesn't exist already, then just set the key to that value
   371  		if _, exists := dest[k]; !exists {
   372  			dest[k] = v
   373  			continue
   374  		}
   375  		nextMap, ok := v.(map[string]interface{})
   376  		// If it isn't another map, overwrite the value
   377  		if !ok {
   378  			dest[k] = v
   379  			continue
   380  		}
   381  		// Edge case: If the key exists in the destination, but isn't a map
   382  		destMap, isMap := dest[k].(map[string]interface{})
   383  		// If the source map has a map for this key, prefer it
   384  		if !isMap {
   385  			dest[k] = v
   386  			continue
   387  		}
   388  		// If we got to this point, it is a map in both, so merge them
   389  		dest[k] = mergeValues(destMap, nextMap)
   390  	}
   391  	return dest
   392  }
   393  
   394  // vals merges values from files specified via -f/--values and
   395  // directly via --set or --set-string or --set-file, marshaling them to YAML
   396  func vals(valueFiles valueFiles, values []string, stringValues []string, fileValues []string, CertFile, KeyFile, CAFile string) ([]byte, error) {
   397  	base := map[string]interface{}{}
   398  
   399  	// User specified a values files via -f/--values
   400  	for _, filePath := range valueFiles {
   401  		currentMap := map[string]interface{}{}
   402  
   403  		var bytes []byte
   404  		var err error
   405  		if strings.TrimSpace(filePath) == "-" {
   406  			bytes, err = ioutil.ReadAll(os.Stdin)
   407  		} else {
   408  			bytes, err = readFile(filePath, CertFile, KeyFile, CAFile)
   409  		}
   410  
   411  		if err != nil {
   412  			return []byte{}, err
   413  		}
   414  
   415  		if err := yaml.Unmarshal(bytes, &currentMap); err != nil {
   416  			return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err)
   417  		}
   418  		// Merge with the previous map
   419  		base = mergeValues(base, currentMap)
   420  	}
   421  
   422  	// User specified a value via --set
   423  	for _, value := range values {
   424  		if err := strvals.ParseInto(value, base); err != nil {
   425  			return []byte{}, fmt.Errorf("failed parsing --set data: %s", err)
   426  		}
   427  	}
   428  
   429  	// User specified a value via --set-string
   430  	for _, value := range stringValues {
   431  		if err := strvals.ParseIntoString(value, base); err != nil {
   432  			return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err)
   433  		}
   434  	}
   435  
   436  	// User specified a value via --set-file
   437  	for _, value := range fileValues {
   438  		reader := func(rs []rune) (interface{}, error) {
   439  			bytes, err := readFile(string(rs), CertFile, KeyFile, CAFile)
   440  			return string(bytes), err
   441  		}
   442  		if err := strvals.ParseIntoFile(value, base, reader); err != nil {
   443  			return []byte{}, fmt.Errorf("failed parsing --set-file data: %s", err)
   444  		}
   445  	}
   446  
   447  	return yaml.Marshal(base)
   448  }
   449  
   450  // printRelease prints info about a release if the Debug is true.
   451  func (i *installCmd) printRelease(rel *release.Release) {
   452  	if rel == nil {
   453  		return
   454  	}
   455  	// TODO: Switch to text/template like everything else.
   456  	fmt.Fprintf(i.out, "NAME:   %s\n", rel.Name)
   457  	if settings.Debug {
   458  		printRelease(i.out, rel)
   459  	}
   460  }
   461  
   462  // locateChartPath looks for a chart directory in known places, and returns either the full path or an error.
   463  //
   464  // This does not ensure that the chart is well-formed; only that the requested filename exists.
   465  //
   466  // Order of resolution:
   467  // - current working directory
   468  // - if path is absolute or begins with '.', error out here
   469  // - chart repos in $HELM_HOME
   470  // - URL
   471  //
   472  // If 'verify' is true, this will attempt to also verify the chart.
   473  func locateChartPath(repoURL, username, password, name, version string, verify bool, keyring,
   474  	certFile, keyFile, caFile string) (string, error) {
   475  	name = strings.TrimSpace(name)
   476  	version = strings.TrimSpace(version)
   477  	if fi, err := os.Stat(name); err == nil {
   478  		abs, err := filepath.Abs(name)
   479  		if err != nil {
   480  			return abs, err
   481  		}
   482  		if verify {
   483  			if fi.IsDir() {
   484  				return "", errors.New("cannot verify a directory")
   485  			}
   486  			if _, err := downloader.VerifyChart(abs, keyring); err != nil {
   487  				return "", err
   488  			}
   489  		}
   490  		return abs, nil
   491  	}
   492  	if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
   493  		return name, fmt.Errorf("path %q not found", name)
   494  	}
   495  
   496  	crepo := filepath.Join(settings.Home.Repository(), name)
   497  	if _, err := os.Stat(crepo); err == nil {
   498  		return filepath.Abs(crepo)
   499  	}
   500  
   501  	dl := downloader.ChartDownloader{
   502  		HelmHome: settings.Home,
   503  		Out:      os.Stdout,
   504  		Keyring:  keyring,
   505  		Getters:  getter.All(settings),
   506  		Username: username,
   507  		Password: password,
   508  	}
   509  	if verify {
   510  		dl.Verify = downloader.VerifyAlways
   511  	}
   512  	if repoURL != "" {
   513  		chartURL, err := repo.FindChartInAuthRepoURL(repoURL, username, password, name, version,
   514  			certFile, keyFile, caFile, getter.All(settings))
   515  		if err != nil {
   516  			return "", err
   517  		}
   518  		name = chartURL
   519  	}
   520  
   521  	if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) {
   522  		os.MkdirAll(settings.Home.Archive(), 0744)
   523  	}
   524  
   525  	filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive())
   526  	if err == nil {
   527  		lname, err := filepath.Abs(filename)
   528  		if err != nil {
   529  			return filename, err
   530  		}
   531  		debug("Fetched %s to %s\n", name, filename)
   532  		return lname, nil
   533  	} else if settings.Debug {
   534  		return filename, err
   535  	}
   536  
   537  	return filename, fmt.Errorf("failed to download %q (hint: running `helm repo update` may help)", name)
   538  }
   539  
   540  func generateName(nameTemplate string) (string, error) {
   541  	t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
   542  	if err != nil {
   543  		return "", err
   544  	}
   545  	var b bytes.Buffer
   546  	err = t.Execute(&b, nil)
   547  	if err != nil {
   548  		return "", err
   549  	}
   550  	return b.String(), nil
   551  }
   552  
   553  func defaultNamespace() string {
   554  	if ns, _, err := kube.GetConfig(settings.KubeContext, settings.KubeConfig).Namespace(); err == nil {
   555  		return ns
   556  	}
   557  	return "default"
   558  }
   559  
   560  //readFile load a file from the local directory or a remote file with a url.
   561  func readFile(filePath, CertFile, KeyFile, CAFile string) ([]byte, error) {
   562  	u, _ := url.Parse(filePath)
   563  	p := getter.All(settings)
   564  
   565  	// FIXME: maybe someone handle other protocols like ftp.
   566  	getterConstructor, err := p.ByScheme(u.Scheme)
   567  
   568  	if err != nil {
   569  		return ioutil.ReadFile(filePath)
   570  	}
   571  
   572  	getter, err := getterConstructor(filePath, CertFile, KeyFile, CAFile)
   573  	if err != nil {
   574  		return []byte{}, err
   575  	}
   576  	data, err := getter.Get(filePath)
   577  	return data.Bytes(), err
   578  }