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