github.com/azlyth/helm@v2.8.2+incompatible/cmd/helm/install.go (about)

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