github.com/latiif/helm@v2.15.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 req, err := chartutil.LoadRequirements(chartRequested); err == nil {
   271  		// If checkDependencies returns an error, we have unfulfilled dependencies.
   272  		// As of Helm 2.4.0, this is treated as a stopping condition:
   273  		// https://github.com/kubernetes/helm/issues/2209
   274  		if err := renderutil.CheckDependencies(chartRequested, req); err != nil {
   275  			if i.depUp {
   276  				man := &downloader.Manager{
   277  					Out:        i.out,
   278  					ChartPath:  i.chartPath,
   279  					HelmHome:   settings.Home,
   280  					Keyring:    defaultKeyring(),
   281  					SkipUpdate: false,
   282  					Getters:    getter.All(settings),
   283  				}
   284  				if err := man.Update(); err != nil {
   285  					return prettyError(err)
   286  				}
   287  
   288  				// Update all dependencies which are present in /charts.
   289  				chartRequested, err = chartutil.Load(i.chartPath)
   290  				if err != nil {
   291  					return prettyError(err)
   292  				}
   293  			} else {
   294  				return prettyError(err)
   295  			}
   296  
   297  		}
   298  	} else if err != chartutil.ErrRequirementsNotFound {
   299  		return fmt.Errorf("cannot load requirements: %v", err)
   300  	}
   301  
   302  	res, err := i.client.InstallReleaseFromChart(
   303  		chartRequested,
   304  		i.namespace,
   305  		helm.ValueOverrides(rawVals),
   306  		helm.ReleaseName(i.name),
   307  		helm.InstallDryRun(i.dryRun),
   308  		helm.InstallReuseName(i.replace),
   309  		helm.InstallDisableHooks(i.disableHooks),
   310  		helm.InstallDisableCRDHook(i.disableCRDHook),
   311  		helm.InstallSubNotes(i.subNotes),
   312  		helm.InstallTimeout(i.timeout),
   313  		helm.InstallWait(i.wait),
   314  		helm.InstallDescription(i.description))
   315  	if err != nil {
   316  		if i.atomic {
   317  			fmt.Fprintf(os.Stdout, "INSTALL FAILED\nPURGING CHART\nError: %v\n", prettyError(err))
   318  			deleteSideEffects := &deleteCmd{
   319  				name:         i.name,
   320  				disableHooks: i.disableHooks,
   321  				purge:        true,
   322  				timeout:      i.timeout,
   323  				description:  "",
   324  				dryRun:       i.dryRun,
   325  				out:          i.out,
   326  				client:       i.client,
   327  			}
   328  			if err := deleteSideEffects.run(); err != nil {
   329  				return err
   330  			}
   331  			fmt.Fprintf(os.Stdout, "Successfully purged a chart!\n")
   332  		}
   333  		return prettyError(err)
   334  	}
   335  
   336  	rel := res.GetRelease()
   337  	if rel == nil {
   338  		return nil
   339  	}
   340  
   341  	if outputFormat(i.output) == outputTable {
   342  		i.printRelease(rel)
   343  	}
   344  
   345  	// If this is a dry run, we can't display status.
   346  	if i.dryRun {
   347  		// This is special casing to avoid breaking backward compatibility:
   348  		if res.Release.Info.Description != "Dry run complete" {
   349  			fmt.Fprintf(os.Stdout, "WARNING: %s\n", res.Release.Info.Description)
   350  		}
   351  		return nil
   352  	}
   353  
   354  	// Print the status like status command does
   355  	status, err := i.client.ReleaseStatus(rel.Name)
   356  	if err != nil {
   357  		return prettyError(err)
   358  	}
   359  
   360  	return write(i.out, &statusWriter{status}, outputFormat(i.output))
   361  }
   362  
   363  // Merges source and destination map, preferring values from the source map
   364  func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
   365  	for k, v := range src {
   366  		// If the key doesn't exist already, then just set the key to that value
   367  		if _, exists := dest[k]; !exists {
   368  			dest[k] = v
   369  			continue
   370  		}
   371  		nextMap, ok := v.(map[string]interface{})
   372  		// If it isn't another map, overwrite the value
   373  		if !ok {
   374  			dest[k] = v
   375  			continue
   376  		}
   377  		// Edge case: If the key exists in the destination, but isn't a map
   378  		destMap, isMap := dest[k].(map[string]interface{})
   379  		// If the source map has a map for this key, prefer it
   380  		if !isMap {
   381  			dest[k] = v
   382  			continue
   383  		}
   384  		// If we got to this point, it is a map in both, so merge them
   385  		dest[k] = mergeValues(destMap, nextMap)
   386  	}
   387  	return dest
   388  }
   389  
   390  // vals merges values from files specified via -f/--values and
   391  // directly via --set or --set-string or --set-file, marshaling them to YAML
   392  func vals(valueFiles valueFiles, values []string, stringValues []string, fileValues []string, CertFile, KeyFile, CAFile string) ([]byte, error) {
   393  	base := map[string]interface{}{}
   394  
   395  	// User specified a values files via -f/--values
   396  	for _, filePath := range valueFiles {
   397  		currentMap := map[string]interface{}{}
   398  
   399  		var bytes []byte
   400  		var err error
   401  		if strings.TrimSpace(filePath) == "-" {
   402  			bytes, err = ioutil.ReadAll(os.Stdin)
   403  		} else {
   404  			bytes, err = readFile(filePath, CertFile, KeyFile, CAFile)
   405  		}
   406  
   407  		if err != nil {
   408  			return []byte{}, err
   409  		}
   410  
   411  		if err := yaml.Unmarshal(bytes, &currentMap); err != nil {
   412  			return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err)
   413  		}
   414  		// Merge with the previous map
   415  		base = mergeValues(base, currentMap)
   416  	}
   417  
   418  	// User specified a value via --set
   419  	for _, value := range values {
   420  		if err := strvals.ParseInto(value, base); err != nil {
   421  			return []byte{}, fmt.Errorf("failed parsing --set data: %s", err)
   422  		}
   423  	}
   424  
   425  	// User specified a value via --set-string
   426  	for _, value := range stringValues {
   427  		if err := strvals.ParseIntoString(value, base); err != nil {
   428  			return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err)
   429  		}
   430  	}
   431  
   432  	// User specified a value via --set-file
   433  	for _, value := range fileValues {
   434  		reader := func(rs []rune) (interface{}, error) {
   435  			bytes, err := readFile(string(rs), CertFile, KeyFile, CAFile)
   436  			return string(bytes), err
   437  		}
   438  		if err := strvals.ParseIntoFile(value, base, reader); err != nil {
   439  			return []byte{}, fmt.Errorf("failed parsing --set-file data: %s", err)
   440  		}
   441  	}
   442  
   443  	return yaml.Marshal(base)
   444  }
   445  
   446  // printRelease prints info about a release if the Debug is true.
   447  func (i *installCmd) printRelease(rel *release.Release) {
   448  	if rel == nil {
   449  		return
   450  	}
   451  	// TODO: Switch to text/template like everything else.
   452  	fmt.Fprintf(i.out, "NAME:   %s\n", rel.Name)
   453  	if settings.Debug {
   454  		printRelease(i.out, rel)
   455  	}
   456  }
   457  
   458  // locateChartPath looks for a chart directory in known places, and returns either the full path or an error.
   459  //
   460  // This does not ensure that the chart is well-formed; only that the requested filename exists.
   461  //
   462  // Order of resolution:
   463  // - current working directory
   464  // - if path is absolute or begins with '.', error out here
   465  // - chart repos in $HELM_HOME
   466  // - URL
   467  //
   468  // If 'verify' is true, this will attempt to also verify the chart.
   469  func locateChartPath(repoURL, username, password, name, version string, verify bool, keyring,
   470  	certFile, keyFile, caFile string) (string, error) {
   471  	name = strings.TrimSpace(name)
   472  	version = strings.TrimSpace(version)
   473  	if fi, err := os.Stat(name); err == nil {
   474  		abs, err := filepath.Abs(name)
   475  		if err != nil {
   476  			return abs, err
   477  		}
   478  		if verify {
   479  			if fi.IsDir() {
   480  				return "", errors.New("cannot verify a directory")
   481  			}
   482  			if _, err := downloader.VerifyChart(abs, keyring); err != nil {
   483  				return "", err
   484  			}
   485  		}
   486  		return abs, nil
   487  	}
   488  	if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
   489  		return name, fmt.Errorf("path %q not found", name)
   490  	}
   491  
   492  	crepo := filepath.Join(settings.Home.Repository(), name)
   493  	if _, err := os.Stat(crepo); err == nil {
   494  		return filepath.Abs(crepo)
   495  	}
   496  
   497  	dl := downloader.ChartDownloader{
   498  		HelmHome: settings.Home,
   499  		Out:      os.Stdout,
   500  		Keyring:  keyring,
   501  		Getters:  getter.All(settings),
   502  		Username: username,
   503  		Password: password,
   504  	}
   505  	if verify {
   506  		dl.Verify = downloader.VerifyAlways
   507  	}
   508  	if repoURL != "" {
   509  		chartURL, err := repo.FindChartInAuthRepoURL(repoURL, username, password, name, version,
   510  			certFile, keyFile, caFile, getter.All(settings))
   511  		if err != nil {
   512  			return "", err
   513  		}
   514  		name = chartURL
   515  	}
   516  
   517  	if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) {
   518  		os.MkdirAll(settings.Home.Archive(), 0744)
   519  	}
   520  
   521  	filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive())
   522  	if err == nil {
   523  		lname, err := filepath.Abs(filename)
   524  		if err != nil {
   525  			return filename, err
   526  		}
   527  		debug("Fetched %s to %s\n", name, filename)
   528  		return lname, nil
   529  	} else if settings.Debug {
   530  		return filename, err
   531  	}
   532  
   533  	return filename, fmt.Errorf("failed to download %q (hint: running `helm repo update` may help)", name)
   534  }
   535  
   536  func generateName(nameTemplate string) (string, error) {
   537  	t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
   538  	if err != nil {
   539  		return "", err
   540  	}
   541  	var b bytes.Buffer
   542  	err = t.Execute(&b, nil)
   543  	if err != nil {
   544  		return "", err
   545  	}
   546  	return b.String(), nil
   547  }
   548  
   549  func defaultNamespace() string {
   550  	if ns, _, err := kube.GetConfig(settings.KubeContext, settings.KubeConfig).Namespace(); err == nil {
   551  		return ns
   552  	}
   553  	return "default"
   554  }
   555  
   556  //readFile load a file from the local directory or a remote file with a url.
   557  func readFile(filePath, CertFile, KeyFile, CAFile string) ([]byte, error) {
   558  	u, _ := url.Parse(filePath)
   559  	p := getter.All(settings)
   560  
   561  	// FIXME: maybe someone handle other protocols like ftp.
   562  	getterConstructor, err := p.ByScheme(u.Scheme)
   563  
   564  	if err != nil {
   565  		return ioutil.ReadFile(filePath)
   566  	}
   567  
   568  	getter, err := getterConstructor(filePath, CertFile, KeyFile, CAFile)
   569  	if err != nil {
   570  		return []byte{}, err
   571  	}
   572  	data, err := getter.Get(filePath)
   573  	return data.Bytes(), err
   574  }