github.com/Beeketing/helm@v2.12.1+incompatible/cmd/helm/init.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  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  	"time"
    27  
    28  	"github.com/spf13/cobra"
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	"k8s.io/client-go/kubernetes"
    31  
    32  	"k8s.io/apimachinery/pkg/util/yaml"
    33  	"k8s.io/helm/cmd/helm/installer"
    34  	"k8s.io/helm/pkg/getter"
    35  	"k8s.io/helm/pkg/helm"
    36  	"k8s.io/helm/pkg/helm/helmpath"
    37  	"k8s.io/helm/pkg/helm/portforwarder"
    38  	"k8s.io/helm/pkg/repo"
    39  )
    40  
    41  const initDesc = `
    42  This command installs Tiller (the Helm server-side component) onto your
    43  Kubernetes Cluster and sets up local configuration in $HELM_HOME (default ~/.helm/).
    44  
    45  As with the rest of the Helm commands, 'helm init' discovers Kubernetes clusters
    46  by reading $KUBECONFIG (default '~/.kube/config') and using the default context.
    47  
    48  To set up just a local environment, use '--client-only'. That will configure
    49  $HELM_HOME, but not attempt to connect to a Kubernetes cluster and install the Tiller
    50  deployment.
    51  
    52  When installing Tiller, 'helm init' will attempt to install the latest released
    53  version. You can specify an alternative image with '--tiller-image'. For those
    54  frequently working on the latest code, the flag '--canary-image' will install
    55  the latest pre-release version of Tiller (e.g. the HEAD commit in the GitHub
    56  repository on the master branch).
    57  
    58  To dump a manifest containing the Tiller deployment YAML, combine the
    59  '--dry-run' and '--debug' flags.
    60  `
    61  
    62  const (
    63  	stableRepository         = "stable"
    64  	localRepository          = "local"
    65  	localRepositoryIndexFile = "index.yaml"
    66  )
    67  
    68  var (
    69  	stableRepositoryURL = "https://kubernetes-charts.storage.googleapis.com"
    70  	// This is the IPv4 loopback, not localhost, because we have to force IPv4
    71  	// for Dockerized Helm: https://github.com/kubernetes/helm/issues/1410
    72  	localRepositoryURL = "http://127.0.0.1:8879/charts"
    73  	tlsServerName      string // overrides the server name used to verify the hostname on the returned certificates from the server.
    74  	tlsCaCertFile      string // path to TLS CA certificate file
    75  	tlsCertFile        string // path to TLS certificate file
    76  	tlsKeyFile         string // path to TLS key file
    77  	tlsVerify          bool   // enable TLS and verify remote certificates
    78  	tlsEnable          bool   // enable TLS
    79  )
    80  
    81  type initCmd struct {
    82  	image          string
    83  	clientOnly     bool
    84  	canary         bool
    85  	upgrade        bool
    86  	namespace      string
    87  	dryRun         bool
    88  	forceUpgrade   bool
    89  	skipRefresh    bool
    90  	out            io.Writer
    91  	client         helm.Interface
    92  	home           helmpath.Home
    93  	opts           installer.Options
    94  	kubeClient     kubernetes.Interface
    95  	serviceAccount string
    96  	maxHistory     int
    97  	replicas       int
    98  	wait           bool
    99  }
   100  
   101  func newInitCmd(out io.Writer) *cobra.Command {
   102  	i := &initCmd{out: out}
   103  
   104  	cmd := &cobra.Command{
   105  		Use:   "init",
   106  		Short: "initialize Helm on both client and server",
   107  		Long:  initDesc,
   108  		RunE: func(cmd *cobra.Command, args []string) error {
   109  			if len(args) != 0 {
   110  				return errors.New("This command does not accept arguments")
   111  			}
   112  			i.namespace = settings.TillerNamespace
   113  			i.home = settings.Home
   114  			i.client = ensureHelmClient(i.client)
   115  
   116  			return i.run()
   117  		},
   118  	}
   119  
   120  	f := cmd.Flags()
   121  	f.StringVarP(&i.image, "tiller-image", "i", "", "override Tiller image")
   122  	f.BoolVar(&i.canary, "canary-image", false, "use the canary Tiller image")
   123  	f.BoolVar(&i.upgrade, "upgrade", false, "upgrade if Tiller is already installed")
   124  	f.BoolVar(&i.forceUpgrade, "force-upgrade", false, "force upgrade of Tiller to the current helm version")
   125  	f.BoolVarP(&i.clientOnly, "client-only", "c", false, "if set does not install Tiller")
   126  	f.BoolVar(&i.dryRun, "dry-run", false, "do not install local or remote")
   127  	f.BoolVar(&i.skipRefresh, "skip-refresh", false, "do not refresh (download) the local repository cache")
   128  	f.BoolVar(&i.wait, "wait", false, "block until Tiller is running and ready to receive requests")
   129  
   130  	// TODO: replace TLS flags with pkg/helm/environment.AddFlagsTLS() in Helm 3
   131  	//
   132  	// NOTE (bacongobbler): we can't do this in Helm 2 because the flag names differ, and `helm init --tls-ca-cert`
   133  	// doesn't conform with the rest of the TLS flag names (should be --tiller-tls-ca-cert in Helm 3)
   134  	f.BoolVar(&tlsEnable, "tiller-tls", false, "install Tiller with TLS enabled")
   135  	f.BoolVar(&tlsVerify, "tiller-tls-verify", false, "install Tiller with TLS enabled and to verify remote certificates")
   136  	f.StringVar(&tlsKeyFile, "tiller-tls-key", "", "path to TLS key file to install with Tiller")
   137  	f.StringVar(&tlsCertFile, "tiller-tls-cert", "", "path to TLS certificate file to install with Tiller")
   138  	f.StringVar(&tlsCaCertFile, "tls-ca-cert", "", "path to CA root certificate")
   139  	f.StringVar(&tlsServerName, "tiller-tls-hostname", settings.TillerHost, "the server name used to verify the hostname on the returned certificates from Tiller")
   140  
   141  	f.StringVar(&stableRepositoryURL, "stable-repo-url", stableRepositoryURL, "URL for stable repository")
   142  	f.StringVar(&localRepositoryURL, "local-repo-url", localRepositoryURL, "URL for local repository")
   143  
   144  	f.BoolVar(&i.opts.EnableHostNetwork, "net-host", false, "install Tiller with net=host")
   145  	f.StringVar(&i.serviceAccount, "service-account", "", "name of service account")
   146  	f.IntVar(&i.maxHistory, "history-max", 0, "limit the maximum number of revisions saved per release. Use 0 for no limit.")
   147  	f.IntVar(&i.replicas, "replicas", 1, "amount of tiller instances to run on the cluster")
   148  
   149  	f.StringVar(&i.opts.NodeSelectors, "node-selectors", "", "labels to specify the node on which Tiller is installed (app=tiller,helm=rocks)")
   150  	f.VarP(&i.opts.Output, "output", "o", "skip installation and output Tiller's manifest in specified format (json or yaml)")
   151  	f.StringArrayVar(&i.opts.Values, "override", []string{}, "override values for the Tiller Deployment manifest (can specify multiple or separate values with commas: key1=val1,key2=val2)")
   152  	f.BoolVar(&i.opts.AutoMountServiceAccountToken, "automount-service-account-token", true, "auto-mount the given service account to tiller")
   153  
   154  	return cmd
   155  }
   156  
   157  // tlsOptions sanitizes the tls flags as well as checks for the existence of required
   158  // tls files indicated by those flags, if any.
   159  func (i *initCmd) tlsOptions() error {
   160  	i.opts.EnableTLS = tlsEnable || tlsVerify
   161  	i.opts.VerifyTLS = tlsVerify
   162  
   163  	if i.opts.EnableTLS {
   164  		missing := func(file string) bool {
   165  			_, err := os.Stat(file)
   166  			return os.IsNotExist(err)
   167  		}
   168  		if i.opts.TLSKeyFile = tlsKeyFile; i.opts.TLSKeyFile == "" || missing(i.opts.TLSKeyFile) {
   169  			return errors.New("missing required TLS key file")
   170  		}
   171  		if i.opts.TLSCertFile = tlsCertFile; i.opts.TLSCertFile == "" || missing(i.opts.TLSCertFile) {
   172  			return errors.New("missing required TLS certificate file")
   173  		}
   174  		if i.opts.VerifyTLS {
   175  			if i.opts.TLSCaCertFile = tlsCaCertFile; i.opts.TLSCaCertFile == "" || missing(i.opts.TLSCaCertFile) {
   176  				return errors.New("missing required TLS CA file")
   177  			}
   178  		}
   179  
   180  		// FIXME: remove once we use pkg/helm/environment.AddFlagsTLS() in Helm 3
   181  		settings.TLSEnable = tlsEnable
   182  		settings.TLSVerify = tlsVerify
   183  		settings.TLSServerName = tlsServerName
   184  		settings.TLSCaCertFile = tlsCaCertFile
   185  		settings.TLSCertFile = tlsCertFile
   186  		settings.TLSKeyFile = tlsKeyFile
   187  	}
   188  	return nil
   189  }
   190  
   191  // run initializes local config and installs Tiller to Kubernetes cluster.
   192  func (i *initCmd) run() error {
   193  	if err := i.tlsOptions(); err != nil {
   194  		return err
   195  	}
   196  	i.opts.Namespace = i.namespace
   197  	i.opts.UseCanary = i.canary
   198  	i.opts.ImageSpec = i.image
   199  	i.opts.ForceUpgrade = i.forceUpgrade
   200  	i.opts.ServiceAccount = i.serviceAccount
   201  	i.opts.MaxHistory = i.maxHistory
   202  	i.opts.Replicas = i.replicas
   203  
   204  	writeYAMLManifests := func(manifests []string) error {
   205  		w := i.out
   206  		for _, manifest := range manifests {
   207  			if _, err := fmt.Fprintln(w, "---"); err != nil {
   208  				return err
   209  			}
   210  
   211  			if _, err := fmt.Fprintln(w, manifest); err != nil {
   212  				return err
   213  			}
   214  		}
   215  
   216  		// YAML ending document boundary marker
   217  		_, err := fmt.Fprintln(w, "...")
   218  		return err
   219  	}
   220  	if len(i.opts.Output) > 0 {
   221  		var manifests []string
   222  		var err error
   223  		if manifests, err = installer.TillerManifests(&i.opts); err != nil {
   224  			return err
   225  		}
   226  		switch i.opts.Output.String() {
   227  		case "json":
   228  			for _, manifest := range manifests {
   229  				var out bytes.Buffer
   230  				jsonb, err := yaml.ToJSON([]byte(manifest))
   231  				if err != nil {
   232  					return err
   233  				}
   234  				buf := bytes.NewBuffer(jsonb)
   235  				if err := json.Indent(&out, buf.Bytes(), "", "    "); err != nil {
   236  					return err
   237  				}
   238  				if _, err = i.out.Write(out.Bytes()); err != nil {
   239  					return err
   240  				}
   241  				fmt.Fprint(i.out, "\n")
   242  			}
   243  			return nil
   244  		case "yaml":
   245  			return writeYAMLManifests(manifests)
   246  		default:
   247  			return fmt.Errorf("unknown output format: %q", i.opts.Output)
   248  		}
   249  	}
   250  	if settings.Debug {
   251  		var manifests []string
   252  		var err error
   253  
   254  		// write Tiller manifests
   255  		if manifests, err = installer.TillerManifests(&i.opts); err != nil {
   256  			return err
   257  		}
   258  
   259  		if err = writeYAMLManifests(manifests); err != nil {
   260  			return err
   261  		}
   262  	}
   263  
   264  	if i.dryRun {
   265  		return nil
   266  	}
   267  
   268  	if err := ensureDirectories(i.home, i.out); err != nil {
   269  		return err
   270  	}
   271  	if err := ensureDefaultRepos(i.home, i.out, i.skipRefresh); err != nil {
   272  		return err
   273  	}
   274  	if err := ensureRepoFileFormat(i.home.RepositoryFile(), i.out); err != nil {
   275  		return err
   276  	}
   277  	fmt.Fprintf(i.out, "$HELM_HOME has been configured at %s.\n", settings.Home)
   278  
   279  	if !i.clientOnly {
   280  		if i.kubeClient == nil {
   281  			_, c, err := getKubeClient(settings.KubeContext, settings.KubeConfig)
   282  			if err != nil {
   283  				return fmt.Errorf("could not get kubernetes client: %s", err)
   284  			}
   285  			i.kubeClient = c
   286  		}
   287  		if err := installer.Install(i.kubeClient, &i.opts); err != nil {
   288  			if !apierrors.IsAlreadyExists(err) {
   289  				return fmt.Errorf("error installing: %s", err)
   290  			}
   291  			if i.upgrade {
   292  				if err := installer.Upgrade(i.kubeClient, &i.opts); err != nil {
   293  					return fmt.Errorf("error when upgrading: %s", err)
   294  				}
   295  				if err := i.ping(i.opts.SelectImage()); err != nil {
   296  					return err
   297  				}
   298  				fmt.Fprintln(i.out, "\nTiller (the Helm server-side component) has been upgraded to the current version.")
   299  			} else {
   300  				fmt.Fprintln(i.out, "Warning: Tiller is already installed in the cluster.\n"+
   301  					"(Use --client-only to suppress this message, or --upgrade to upgrade Tiller to the current version.)")
   302  			}
   303  		} else {
   304  			fmt.Fprintln(i.out, "\nTiller (the Helm server-side component) has been installed into your Kubernetes Cluster.")
   305  			if !tlsVerify {
   306  				fmt.Fprintln(i.out, "\nPlease note: by default, Tiller is deployed with an insecure 'allow unauthenticated users' policy.\n"+
   307  					"To prevent this, run `helm init` with the --tiller-tls-verify flag.\n"+
   308  					"For more information on securing your installation see: https://docs.helm.sh/using_helm/#securing-your-helm-installation")
   309  			}
   310  		}
   311  		if err := i.ping(i.opts.SelectImage()); err != nil {
   312  			return err
   313  		}
   314  	} else {
   315  		fmt.Fprintln(i.out, "Not installing Tiller due to 'client-only' flag having been set")
   316  	}
   317  
   318  	fmt.Fprintln(i.out, "Happy Helming!")
   319  	return nil
   320  }
   321  
   322  func (i *initCmd) ping(image string) error {
   323  	if i.wait {
   324  		_, kubeClient, err := getKubeClient(settings.KubeContext, settings.KubeConfig)
   325  		if err != nil {
   326  			return err
   327  		}
   328  		if !watchTillerUntilReady(settings.TillerNamespace, kubeClient, settings.TillerConnectionTimeout, image) {
   329  			return fmt.Errorf("tiller was not found. polling deadline exceeded")
   330  		}
   331  
   332  		// establish a connection to Tiller now that we've effectively guaranteed it's available
   333  		if err := setupConnection(); err != nil {
   334  			return err
   335  		}
   336  		i.client = newClient()
   337  		if err := i.client.PingTiller(); err != nil {
   338  			return fmt.Errorf("could not ping Tiller: %s", err)
   339  		}
   340  	}
   341  
   342  	return nil
   343  }
   344  
   345  // ensureDirectories checks to see if $HELM_HOME exists.
   346  //
   347  // If $HELM_HOME does not exist, this function will create it.
   348  func ensureDirectories(home helmpath.Home, out io.Writer) error {
   349  	configDirectories := []string{
   350  		home.String(),
   351  		home.Repository(),
   352  		home.Cache(),
   353  		home.LocalRepository(),
   354  		home.Plugins(),
   355  		home.Starters(),
   356  		home.Archive(),
   357  	}
   358  	for _, p := range configDirectories {
   359  		if fi, err := os.Stat(p); err != nil {
   360  			fmt.Fprintf(out, "Creating %s \n", p)
   361  			if err := os.MkdirAll(p, 0755); err != nil {
   362  				return fmt.Errorf("Could not create %s: %s", p, err)
   363  			}
   364  		} else if !fi.IsDir() {
   365  			return fmt.Errorf("%s must be a directory", p)
   366  		}
   367  	}
   368  
   369  	return nil
   370  }
   371  
   372  func ensureDefaultRepos(home helmpath.Home, out io.Writer, skipRefresh bool) error {
   373  	repoFile := home.RepositoryFile()
   374  	if fi, err := os.Stat(repoFile); err != nil {
   375  		fmt.Fprintf(out, "Creating %s \n", repoFile)
   376  		f := repo.NewRepoFile()
   377  		sr, err := initStableRepo(home.CacheIndex(stableRepository), out, skipRefresh, home)
   378  		if err != nil {
   379  			return err
   380  		}
   381  		lr, err := initLocalRepo(home.LocalRepository(localRepositoryIndexFile), home.CacheIndex("local"), out, home)
   382  		if err != nil {
   383  			return err
   384  		}
   385  		f.Add(sr)
   386  		f.Add(lr)
   387  		if err := f.WriteFile(repoFile, 0644); err != nil {
   388  			return err
   389  		}
   390  	} else if fi.IsDir() {
   391  		return fmt.Errorf("%s must be a file, not a directory", repoFile)
   392  	}
   393  	return nil
   394  }
   395  
   396  func initStableRepo(cacheFile string, out io.Writer, skipRefresh bool, home helmpath.Home) (*repo.Entry, error) {
   397  	fmt.Fprintf(out, "Adding %s repo with URL: %s \n", stableRepository, stableRepositoryURL)
   398  	c := repo.Entry{
   399  		Name:  stableRepository,
   400  		URL:   stableRepositoryURL,
   401  		Cache: cacheFile,
   402  	}
   403  	r, err := repo.NewChartRepository(&c, getter.All(settings))
   404  	if err != nil {
   405  		return nil, err
   406  	}
   407  
   408  	if skipRefresh {
   409  		return &c, nil
   410  	}
   411  
   412  	// In this case, the cacheFile is always absolute. So passing empty string
   413  	// is safe.
   414  	if err := r.DownloadIndexFile(""); err != nil {
   415  		return nil, fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", stableRepositoryURL, err.Error())
   416  	}
   417  
   418  	return &c, nil
   419  }
   420  
   421  func initLocalRepo(indexFile, cacheFile string, out io.Writer, home helmpath.Home) (*repo.Entry, error) {
   422  	if fi, err := os.Stat(indexFile); err != nil {
   423  		fmt.Fprintf(out, "Adding %s repo with URL: %s \n", localRepository, localRepositoryURL)
   424  		i := repo.NewIndexFile()
   425  		if err := i.WriteFile(indexFile, 0644); err != nil {
   426  			return nil, err
   427  		}
   428  
   429  		//TODO: take this out and replace with helm update functionality
   430  		if err := createLink(indexFile, cacheFile, home); err != nil {
   431  			return nil, err
   432  		}
   433  	} else if fi.IsDir() {
   434  		return nil, fmt.Errorf("%s must be a file, not a directory", indexFile)
   435  	}
   436  
   437  	return &repo.Entry{
   438  		Name:  localRepository,
   439  		URL:   localRepositoryURL,
   440  		Cache: cacheFile,
   441  	}, nil
   442  }
   443  
   444  func ensureRepoFileFormat(file string, out io.Writer) error {
   445  	r, err := repo.LoadRepositoriesFile(file)
   446  	if err == repo.ErrRepoOutOfDate {
   447  		fmt.Fprintln(out, "Updating repository file format...")
   448  		if err := r.WriteFile(file, 0644); err != nil {
   449  			return err
   450  		}
   451  	}
   452  
   453  	return nil
   454  }
   455  
   456  // watchTillerUntilReady waits for the tiller pod to become available. This is useful in situations where we
   457  // want to wait before we call New().
   458  //
   459  // Returns true if it exists. If the timeout was reached and it could not find the pod, it returns false.
   460  func watchTillerUntilReady(namespace string, client kubernetes.Interface, timeout int64, newImage string) bool {
   461  	deadlinePollingChan := time.NewTimer(time.Duration(timeout) * time.Second).C
   462  	checkTillerPodTicker := time.NewTicker(500 * time.Millisecond)
   463  	doneChan := make(chan bool)
   464  
   465  	defer checkTillerPodTicker.Stop()
   466  
   467  	go func() {
   468  		for range checkTillerPodTicker.C {
   469  			image, err := portforwarder.GetTillerPodImage(client.CoreV1(), namespace)
   470  			if err == nil && image == newImage {
   471  				doneChan <- true
   472  				break
   473  			}
   474  		}
   475  	}()
   476  
   477  	for {
   478  		select {
   479  		case <-deadlinePollingChan:
   480  			return false
   481  		case <-doneChan:
   482  			return true
   483  		}
   484  	}
   485  }