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