github.com/canthefason/helm@v2.2.1-0.20170221172616-16b043b8d505+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/cmd/helm/helmpath"
    36  	"k8s.io/helm/cmd/helm/strvals"
    37  	"k8s.io/helm/pkg/chartutil"
    38  	"k8s.io/helm/pkg/downloader"
    39  	"k8s.io/helm/pkg/helm"
    40  	"k8s.io/helm/pkg/kube"
    41  	"k8s.io/helm/pkg/proto/hapi/chart"
    42  	"k8s.io/helm/pkg/proto/hapi/release"
    43  )
    44  
    45  const installDesc = `
    46  This command installs a chart archive.
    47  
    48  The install argument must be either a relative path to a chart directory or the
    49  name of a chart in the current working directory.
    50  
    51  To override values in a chart, use either the '--values' flag and pass in a file
    52  or use the '--set' flag and pass configuration from the command line.
    53  
    54  	$ helm install -f myvalues.yaml ./redis
    55  
    56  or
    57  
    58  	$ helm install --set name=prod ./redis
    59  
    60  You can specify the '--values'/'-f' flag multiple times. The priority will be given to the
    61  last (right-most) file specified. For example, if both myvalues.yaml and override.yaml
    62  contained a key called 'Test', the value set in override.yaml would take precedence:
    63  
    64  	$ helm install -f myvalues.yaml -f override.yaml ./redis
    65  
    66  You can specify the '--set' flag multiple times. The priority will be given to the
    67  last (right-most) set specified. For example, if both 'bar' and 'newbar' values are
    68  set for a key called 'foo', the 'newbar' value would take precedence:
    69  
    70  	$ helm install --set foo=bar --set foo=newbar ./redis
    71  
    72  
    73  To check the generated manifests of a release without installing the chart,
    74  the '--debug' and '--dry-run' flags can be combined. This will still require a
    75  round-trip to the Tiller server.
    76  
    77  If --verify is set, the chart MUST have a provenance file, and the provenenace
    78  fall MUST pass all verification steps.
    79  
    80  There are four different ways you can express the chart you want to install:
    81  
    82  1. By chart reference: helm install stable/mariadb
    83  2. By path to a packaged chart: helm install ./nginx-1.2.3.tgz
    84  3. By path to an unpacked chart directory: helm install ./nginx
    85  4. By absolute URL: helm install https://example.com/charts/nginx-1.2.3.tgz
    86  
    87  CHART REFERENCES
    88  
    89  A chart reference is a convenient way of reference a chart in a chart repository.
    90  
    91  When you use a chart reference ('stable/mariadb'), Helm will look in the local
    92  configuration for a chart repository named 'stable', and will then look for a
    93  chart in that repository whose name is 'mariadb'. It will install the latest
    94  version of that chart unless you also supply a version number with the
    95  '--version' flag.
    96  
    97  To see the list of chart repositories, use 'helm repo list'. To search for
    98  charts in a repository, use 'helm search'.
    99  `
   100  
   101  type installCmd struct {
   102  	name         string
   103  	namespace    string
   104  	valueFiles   valueFiles
   105  	chartPath    string
   106  	dryRun       bool
   107  	disableHooks bool
   108  	replace      bool
   109  	verify       bool
   110  	keyring      string
   111  	out          io.Writer
   112  	client       helm.Interface
   113  	values       []string
   114  	nameTemplate string
   115  	version      string
   116  	timeout      int64
   117  	wait         bool
   118  }
   119  
   120  type valueFiles []string
   121  
   122  func (v *valueFiles) String() string {
   123  	return fmt.Sprint(*v)
   124  }
   125  
   126  func (v *valueFiles) Type() string {
   127  	return "valueFiles"
   128  }
   129  
   130  func (v *valueFiles) Set(value string) error {
   131  	for _, filePath := range strings.Split(value, ",") {
   132  		*v = append(*v, filePath)
   133  	}
   134  	return nil
   135  }
   136  
   137  func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
   138  	inst := &installCmd{
   139  		out:    out,
   140  		client: c,
   141  	}
   142  
   143  	cmd := &cobra.Command{
   144  		Use:               "install [CHART]",
   145  		Short:             "install a chart archive",
   146  		Long:              installDesc,
   147  		PersistentPreRunE: setupConnection,
   148  		RunE: func(cmd *cobra.Command, args []string) error {
   149  			if err := checkArgsLength(len(args), "chart name"); err != nil {
   150  				return err
   151  			}
   152  			cp, err := locateChartPath(args[0], inst.version, inst.verify, inst.keyring)
   153  			if err != nil {
   154  				return err
   155  			}
   156  			inst.chartPath = cp
   157  			inst.client = ensureHelmClient(inst.client)
   158  			return inst.run()
   159  		},
   160  	}
   161  
   162  	f := cmd.Flags()
   163  	f.VarP(&inst.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)")
   164  	f.StringVarP(&inst.name, "name", "n", "", "release name. If unspecified, it will autogenerate one for you")
   165  	f.StringVar(&inst.namespace, "namespace", "", "namespace to install the release into")
   166  	f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install")
   167  	f.BoolVar(&inst.disableHooks, "no-hooks", false, "prevent hooks from running during install")
   168  	f.BoolVar(&inst.replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production")
   169  	f.StringArrayVar(&inst.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
   170  	f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release")
   171  	f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it")
   172  	f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification")
   173  	f.StringVar(&inst.version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed")
   174  	f.Int64Var(&inst.timeout, "timeout", 300, "time in seconds to wait for any individual kubernetes operation (like Jobs for hooks)")
   175  	f.BoolVar(&inst.wait, "wait", false, "if set, will wait until all Pods, PVCs, and Services are in a ready state before marking the release as successful. It will wait for as long as --timeout")
   176  
   177  	return cmd
   178  }
   179  
   180  func (i *installCmd) run() error {
   181  	if flagDebug {
   182  		fmt.Fprintf(i.out, "CHART PATH: %s\n", i.chartPath)
   183  	}
   184  
   185  	if i.namespace == "" {
   186  		i.namespace = defaultNamespace()
   187  	}
   188  
   189  	rawVals, err := i.vals()
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	// If template is specified, try to run the template.
   195  	if i.nameTemplate != "" {
   196  		i.name, err = generateName(i.nameTemplate)
   197  		if err != nil {
   198  			return err
   199  		}
   200  		// Print the final name so the user knows what the final name of the release is.
   201  		fmt.Printf("FINAL NAME: %s\n", i.name)
   202  	}
   203  
   204  	// Check chart requirements to make sure all dependencies are present in /charts
   205  	if c, err := chartutil.Load(i.chartPath); err == nil {
   206  		if req, err := chartutil.LoadRequirements(c); err == nil {
   207  			checkDependencies(c, req, i.out)
   208  		}
   209  	}
   210  
   211  	res, err := i.client.InstallRelease(
   212  		i.chartPath,
   213  		i.namespace,
   214  		helm.ValueOverrides(rawVals),
   215  		helm.ReleaseName(i.name),
   216  		helm.InstallDryRun(i.dryRun),
   217  		helm.InstallReuseName(i.replace),
   218  		helm.InstallDisableHooks(i.disableHooks),
   219  		helm.InstallTimeout(i.timeout),
   220  		helm.InstallWait(i.wait))
   221  	if err != nil {
   222  		return prettyError(err)
   223  	}
   224  
   225  	rel := res.GetRelease()
   226  	if rel == nil {
   227  		return nil
   228  	}
   229  	i.printRelease(rel)
   230  
   231  	// If this is a dry run, we can't display status.
   232  	if i.dryRun {
   233  		return nil
   234  	}
   235  
   236  	// Print the status like status command does
   237  	status, err := i.client.ReleaseStatus(rel.Name)
   238  	if err != nil {
   239  		return prettyError(err)
   240  	}
   241  	PrintStatus(i.out, status)
   242  	return nil
   243  }
   244  
   245  // Merges source and destination map, preferring values from the source map
   246  func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
   247  	for k, v := range src {
   248  		// If the key doesn't exist already, then just set the key to that value
   249  		if _, exists := dest[k]; !exists {
   250  			dest[k] = v
   251  			continue
   252  		}
   253  		nextMap, ok := v.(map[string]interface{})
   254  		// If it isn't another map, overwrite the value
   255  		if !ok {
   256  			dest[k] = v
   257  			continue
   258  		}
   259  		// If the key doesn't exist already, then just set the key to that value
   260  		if _, exists := dest[k]; !exists {
   261  			dest[k] = nextMap
   262  			continue
   263  		}
   264  		// Edge case: If the key exists in the destination, but isn't a map
   265  		destMap, isMap := dest[k].(map[string]interface{})
   266  		// If the source map has a map for this key, prefer it
   267  		if !isMap {
   268  			dest[k] = v
   269  			continue
   270  		}
   271  		// If we got to this point, it is a map in both, so merge them
   272  		dest[k] = mergeValues(destMap, nextMap)
   273  	}
   274  	return dest
   275  }
   276  
   277  func (i *installCmd) vals() ([]byte, error) {
   278  	base := map[string]interface{}{}
   279  
   280  	// User specified a values files via -f/--values
   281  	for _, filePath := range i.valueFiles {
   282  		currentMap := map[string]interface{}{}
   283  		bytes, err := ioutil.ReadFile(filePath)
   284  		if err != nil {
   285  			return []byte{}, err
   286  		}
   287  
   288  		if err := yaml.Unmarshal(bytes, &currentMap); err != nil {
   289  			return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err)
   290  		}
   291  		// Merge with the previous map
   292  		base = mergeValues(base, currentMap)
   293  	}
   294  
   295  	// User specified a value via --set
   296  	for _, value := range i.values {
   297  		if err := strvals.ParseInto(value, base); err != nil {
   298  			return []byte{}, fmt.Errorf("failed parsing --set data: %s", err)
   299  		}
   300  	}
   301  
   302  	return yaml.Marshal(base)
   303  }
   304  
   305  // printRelease prints info about a release if the flagDebug is true.
   306  func (i *installCmd) printRelease(rel *release.Release) {
   307  	if rel == nil {
   308  		return
   309  	}
   310  	// TODO: Switch to text/template like everything else.
   311  	fmt.Fprintf(i.out, "NAME:   %s\n", rel.Name)
   312  	if flagDebug {
   313  		printRelease(i.out, rel)
   314  	}
   315  }
   316  
   317  // locateChartPath looks for a chart directory in known places, and returns either the full path or an error.
   318  //
   319  // This does not ensure that the chart is well-formed; only that the requested filename exists.
   320  //
   321  // Order of resolution:
   322  // - current working directory
   323  // - if path is absolute or begins with '.', error out here
   324  // - chart repos in $HELM_HOME
   325  // - URL
   326  //
   327  // If 'verify' is true, this will attempt to also verify the chart.
   328  func locateChartPath(name, version string, verify bool, keyring string) (string, error) {
   329  	name = strings.TrimSpace(name)
   330  	version = strings.TrimSpace(version)
   331  	if fi, err := os.Stat(name); err == nil {
   332  		abs, err := filepath.Abs(name)
   333  		if err != nil {
   334  			return abs, err
   335  		}
   336  		if verify {
   337  			if fi.IsDir() {
   338  				return "", errors.New("cannot verify a directory")
   339  			}
   340  			if _, err := downloader.VerifyChart(abs, keyring); err != nil {
   341  				return "", err
   342  			}
   343  		}
   344  		return abs, nil
   345  	}
   346  	if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
   347  		return name, fmt.Errorf("path %q not found", name)
   348  	}
   349  
   350  	crepo := filepath.Join(helmpath.Home(homePath()).Repository(), name)
   351  	if _, err := os.Stat(crepo); err == nil {
   352  		return filepath.Abs(crepo)
   353  	}
   354  
   355  	dl := downloader.ChartDownloader{
   356  		HelmHome: helmpath.Home(homePath()),
   357  		Out:      os.Stdout,
   358  		Keyring:  keyring,
   359  	}
   360  	if verify {
   361  		dl.Verify = downloader.VerifyAlways
   362  	}
   363  
   364  	filename, _, err := dl.DownloadTo(name, version, ".")
   365  	if err == nil {
   366  		lname, err := filepath.Abs(filename)
   367  		if err != nil {
   368  			return filename, err
   369  		}
   370  		if flagDebug {
   371  			fmt.Printf("Fetched %s to %s\n", name, filename)
   372  		}
   373  		return lname, nil
   374  	} else if flagDebug {
   375  		return filename, err
   376  	}
   377  
   378  	return filename, fmt.Errorf("file %q not found", name)
   379  }
   380  
   381  func generateName(nameTemplate string) (string, error) {
   382  	t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
   383  	if err != nil {
   384  		return "", err
   385  	}
   386  	var b bytes.Buffer
   387  	err = t.Execute(&b, nil)
   388  	if err != nil {
   389  		return "", err
   390  	}
   391  	return b.String(), nil
   392  }
   393  
   394  func defaultNamespace() string {
   395  	if ns, _, err := kube.GetConfig(kubeContext).Namespace(); err == nil {
   396  		return ns
   397  	}
   398  	return "default"
   399  }
   400  
   401  func checkDependencies(ch *chart.Chart, reqs *chartutil.Requirements, out io.Writer) {
   402  	deps := ch.GetDependencies()
   403  	for _, r := range reqs.Dependencies {
   404  		found := false
   405  		for _, d := range deps {
   406  			if d.Metadata.Name == r.Name {
   407  				found = true
   408  				break
   409  			}
   410  		}
   411  		if !found {
   412  			fmt.Fprintf(out, "Warning: %s is in requirements.yaml but not in the charts/ directory!\n", r.Name)
   413  		}
   414  	}
   415  }