diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 64200adaf..9ade3e344 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -45,6 +45,14 @@ const ( tillerNamespaceEnvVar = "TILLER_NAMESPACE" ) +var ( + tlsCaCertFile string // path to TLS CA certificate file + tlsCertFile string // path to TLS certificate file + tlsKeyFile string // path to TLS key file + tlsVerify bool // enable TLS and verify remote certificates + tlsEnable bool // enable TLS +) + var ( helmHome string tillerHost string diff --git a/cmd/helm/init.go b/cmd/helm/init.go index f855abe9a..bf7d38f16 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -70,6 +70,7 @@ type initCmd struct { dryRun bool out io.Writer home helmpath.Home + opts installer.Options kubeClient internalclientset.Interface } @@ -99,25 +100,73 @@ func newInitCmd(out io.Writer) *cobra.Command { f.BoolVarP(&i.clientOnly, "client-only", "c", false, "if set does not install tiller") f.BoolVar(&i.dryRun, "dry-run", false, "do not install local or remote") + // f.BoolVar(&tlsEnable, "tiller-tls", false, "install tiller with TLS enabled") + // f.BoolVar(&tlsVerify, "tiller-tls-verify", false, "install tiller with TLS enabled and to verify remote certificates") + // f.StringVar(&tlsKeyFile, "tiller-tls-key", "", "path to TLS key file to install with tiller") + // f.StringVar(&tlsCertFile, "tiller-tls-cert", "", "path to TLS certificate file to install with tiller") + // f.StringVar(&tlsCaCertFile, "tls-ca-cert", "", "path to CA root certificate") + return cmd } +// tlsOptions sanitizes the tls flags as well as checks for the existence of required +// tls files indicated by those flags, if any. +func (i *initCmd) tlsOptions() error { + i.opts.EnableTLS = tlsEnable || tlsVerify + i.opts.VerifyTLS = tlsVerify + + if i.opts.EnableTLS { + missing := func(file string) bool { + _, err := os.Stat(file) + return os.IsNotExist(err) + } + if i.opts.TLSKeyFile = tlsKeyFile; i.opts.TLSKeyFile == "" || missing(i.opts.TLSKeyFile) { + return errors.New("missing required TLS key file") + } + if i.opts.TLSCertFile = tlsCertFile; i.opts.TLSCertFile == "" || missing(i.opts.TLSCertFile) { + return errors.New("missing required TLS certificate file") + } + if i.opts.VerifyTLS { + if i.opts.TLSCaCertFile = tlsCaCertFile; i.opts.TLSCaCertFile == "" || missing(i.opts.TLSCaCertFile) { + return errors.New("missing required TLS CA file") + } + } + } + return nil +} + // runInit initializes local config and installs tiller to Kubernetes Cluster func (i *initCmd) run() error { + if err := i.tlsOptions(); err != nil { + return err + } + i.opts.Namespace = i.namespace + i.opts.UseCanary = i.canary + i.opts.ImageSpec = i.image + if flagDebug { - dm, err := installer.DeploymentManifest(i.namespace, i.image, i.canary) - if err != nil { + var mfs string + var err error + + // write deployment manifest + if mfs, err = installer.DeploymentManifest(&i.opts); err != nil { return err } - fm := fmt.Sprintf("apiVersion: extensions/v1beta1\nkind: Deployment\n%s", dm) - fmt.Fprintln(i.out, fm) + fmt.Fprintln(i.out, fmt.Sprintf("apiVersion: extensions/v1beta1\nkind: Deployment\n%s", mfs)) - sm, err := installer.ServiceManifest(i.namespace) - if err != nil { + // write service manifest + if mfs, err = installer.ServiceManifest(i.namespace); err != nil { return err } - fm = fmt.Sprintf("apiVersion: v1\nkind: Service\n%s", sm) - fmt.Fprintln(i.out, fm) + fmt.Fprintln(i.out, fmt.Sprintf("apiVersion: v1\nkind: Service\n%s", mfs)) + + // write secret manifest + if i.opts.EnableTLS { + if mfs, err = installer.SecretManifest(&i.opts); err != nil { + return err + } + fmt.Fprintln(i.out, fmt.Sprintf("apiVersion: v1\nkind: Secret\n%s", mfs)) + } } if i.dryRun { @@ -143,13 +192,12 @@ func (i *initCmd) run() error { } i.kubeClient = c } - opts := &installer.Options{Namespace: i.namespace, ImageSpec: i.image, UseCanary: i.canary} - if err := installer.Install(i.kubeClient, opts); err != nil { + if err := installer.Install(i.kubeClient, &i.opts); err != nil { if !kerrors.IsAlreadyExists(err) { return fmt.Errorf("error installing: %s", err) } if i.upgrade { - if err := installer.Upgrade(i.kubeClient, opts); err != nil { + if err := installer.Upgrade(i.kubeClient, &i.opts); err != nil { return fmt.Errorf("error when upgrading: %s", err) } fmt.Fprintln(i.out, "\nTiller (the helm server side component) has been upgraded to the current version.") diff --git a/cmd/helm/installer/install.go b/cmd/helm/installer/install.go index 2ca06a9c1..338e31eca 100644 --- a/cmd/helm/installer/install.go +++ b/cmd/helm/installer/install.go @@ -17,7 +17,7 @@ limitations under the License. package installer // import "k8s.io/helm/cmd/helm/installer" import ( - "fmt" + "io/ioutil" "github.com/ghodss/yaml" @@ -28,22 +28,23 @@ import ( "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" extensionsclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/extensions/internalversion" "k8s.io/kubernetes/pkg/util/intstr" - - "k8s.io/helm/pkg/version" ) -const defaultImage = "gcr.io/kubernetes-helm/tiller" - // Install uses kubernetes client to install tiller. // // Returns an error if the command failed. func Install(client internalclientset.Interface, opts *Options) error { - if err := createDeployment(client.Extensions(), opts.Namespace, opts.ImageSpec, opts.UseCanary); err != nil { + if err := createDeployment(client.Extensions(), opts); err != nil { return err } if err := createService(client.Core(), opts.Namespace); err != nil { return err } + if opts.tls() { + if err := createSecret(client.Core(), opts); err != nil { + return err + } + } return nil } @@ -55,7 +56,7 @@ func Upgrade(client internalclientset.Interface, opts *Options) error { if err != nil { return err } - obj.Spec.Template.Spec.Containers[0].Image = selectImage(opts.ImageSpec, opts.UseCanary) + obj.Spec.Template.Spec.Containers[0].Image = opts.selectImage() if _, err := client.Extensions().Deployments(opts.Namespace).Update(obj); err != nil { return err } @@ -73,15 +74,15 @@ func Upgrade(client internalclientset.Interface, opts *Options) error { } // createDeployment creates the Tiller deployment reource -func createDeployment(client extensionsclient.DeploymentsGetter, namespace, image string, canary bool) error { - obj := deployment(namespace, image, canary) +func createDeployment(client extensionsclient.DeploymentsGetter, opts *Options) error { + obj := deployment(opts) _, err := client.Deployments(obj.Namespace).Create(obj) return err } // deployment gets the deployment object that installs Tiller. -func deployment(namespace, image string, canary bool) *extensions.Deployment { - return generateDeployment(namespace, selectImage(image, canary)) +func deployment(opts *Options) *extensions.Deployment { + return generateDeployment(opts) } // createService creates the Tiller service resource @@ -96,21 +97,10 @@ func service(namespace string) *api.Service { return generateService(namespace) } -func selectImage(image string, canary bool) string { - switch { - case canary: - image = defaultImage + ":canary" - case image == "": - image = fmt.Sprintf("%s:%s", defaultImage, version.Version) - } - return image -} - // DeploymentManifest gets the manifest (as a string) that describes the Tiller Deployment // resource. -func DeploymentManifest(namespace, image string, canary bool) (string, error) { - obj := deployment(namespace, image, canary) - +func DeploymentManifest(opts *Options) (string, error) { + obj := deployment(opts) buf, err := yaml.Marshal(obj) return string(buf), err } @@ -129,11 +119,11 @@ func generateLabels(labels map[string]string) map[string]string { return labels } -func generateDeployment(namespace, image string) *extensions.Deployment { +func generateDeployment(opts *Options) *extensions.Deployment { labels := generateLabels(map[string]string{"name": "tiller"}) d := &extensions.Deployment{ ObjectMeta: api.ObjectMeta{ - Namespace: namespace, + Namespace: opts.Namespace, Name: "tiller-deploy", Labels: labels, }, @@ -147,13 +137,13 @@ func generateDeployment(namespace, image string) *extensions.Deployment { Containers: []api.Container{ { Name: "tiller", - Image: image, + Image: opts.selectImage(), ImagePullPolicy: "IfNotPresent", Ports: []api.ContainerPort{ {ContainerPort: 44134, Name: "tiller"}, }, Env: []api.EnvVar{ - {Name: "TILLER_NAMESPACE", Value: namespace}, + {Name: "TILLER_NAMESPACE", Value: opts.Namespace}, }, LivenessProbe: &api.Probe{ Handler: api.Handler{ @@ -181,6 +171,37 @@ func generateDeployment(namespace, image string) *extensions.Deployment { }, }, } + + if opts.tls() { + const certsDir = "/etc/certs" + + var tlsVerify, tlsEnable = "", "1" + if opts.VerifyTLS { + tlsVerify = "1" + } + + // Mount secret to "/etc/certs" + d.Spec.Template.Spec.Containers[0].VolumeMounts = append(d.Spec.Template.Spec.Containers[0].VolumeMounts, api.VolumeMount{ + Name: "tiller-certs", + ReadOnly: true, + MountPath: certsDir, + }) + // Add environment variable required for enabling TLS + d.Spec.Template.Spec.Containers[0].Env = append(d.Spec.Template.Spec.Containers[0].Env, []api.EnvVar{ + {Name: "TILLER_TLS_VERIFY", Value: tlsVerify}, + {Name: "TILLER_TLS_ENABLE", Value: tlsEnable}, + {Name: "TILLER_TLS_CERTS", Value: certsDir}, + }...) + // Add secret volume to deployment + d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, api.Volume{ + Name: "tiller-certs", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "tiller-secret", + }, + }, + }) + } return d } @@ -206,3 +227,54 @@ func generateService(namespace string) *api.Service { } return s } + +// SecretManifest gets the manifest (as a string) that describes the Tiller Secret resource. +func SecretManifest(opts *Options) (string, error) { + o, err := generateSecret(opts) + if err != nil { + return "", err + } + buf, err := yaml.Marshal(o) + return string(buf), err +} + +// createSecret creates the Tiller secret resource. +func createSecret(client internalversion.SecretsGetter, opts *Options) error { + o, err := generateSecret(opts) + if err != nil { + return err + } + _, err = client.Secrets(o.Namespace).Create(o) + return err +} + +// generateSecret builds the secret object that hold Tiller secrets. +func generateSecret(opts *Options) (*api.Secret, error) { + const secretName = "tiller-secret" + + labels := generateLabels(map[string]string{"name": "tiller"}) + secret := &api.Secret{ + Type: api.SecretTypeOpaque, + Data: make(map[string][]byte), + ObjectMeta: api.ObjectMeta{ + Name: secretName, + Labels: labels, + Namespace: opts.Namespace, + }, + } + var err error + if secret.Data["tls.key"], err = read(opts.TLSKeyFile); err != nil { + return nil, err + } + if secret.Data["tls.crt"], err = read(opts.TLSCertFile); err != nil { + return nil, err + } + if opts.VerifyTLS { + if secret.Data["ca.crt"], err = read(opts.TLSCaCertFile); err != nil { + return nil, err + } + } + return secret, nil +} + +func read(path string) (b []byte, err error) { return ioutil.ReadFile(path) } diff --git a/cmd/helm/installer/install_test.go b/cmd/helm/installer/install_test.go index daa09b0d8..4d9a2cbca 100644 --- a/cmd/helm/installer/install_test.go +++ b/cmd/helm/installer/install_test.go @@ -45,7 +45,7 @@ func TestDeploymentManifest(t *testing.T) { } for _, tt := range tests { - o, err := DeploymentManifest(api.NamespaceDefault, tt.image, tt.canary) + o, err := DeploymentManifest(&Options{Namespace: api.NamespaceDefault, ImageSpec: tt.image, UseCanary: tt.canary}) if err != nil { t.Fatalf("%s: error %q", tt.name, err) } @@ -146,7 +146,11 @@ func TestInstall_canary(t *testing.T) { func TestUpgrade(t *testing.T) { image := "gcr.io/kubernetes-helm/tiller:v2.0.0" - existingDeployment := deployment(api.NamespaceDefault, "imageToReplace", false) + existingDeployment := deployment(&Options{ + Namespace: api.NamespaceDefault, + ImageSpec: "imageToReplace", + UseCanary: false, + }) existingService := service(api.NamespaceDefault) fc := &fake.Clientset{} @@ -178,7 +182,11 @@ func TestUpgrade(t *testing.T) { func TestUpgrade_serviceNotFound(t *testing.T) { image := "gcr.io/kubernetes-helm/tiller:v2.0.0" - existingDeployment := deployment(api.NamespaceDefault, "imageToReplace", false) + existingDeployment := deployment(&Options{ + Namespace: api.NamespaceDefault, + ImageSpec: "imageToReplace", + UseCanary: false, + }) fc := &fake.Clientset{} fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { diff --git a/cmd/helm/installer/options.go b/cmd/helm/installer/options.go index 909b7e9e4..41f8aaf2a 100644 --- a/cmd/helm/installer/options.go +++ b/cmd/helm/installer/options.go @@ -16,6 +16,13 @@ limitations under the License. package installer // import "k8s.io/helm/cmd/helm/installer" +import ( + "fmt" + "k8s.io/helm/pkg/version" +) + +const defaultImage = "gcr.io/kubernetes-helm/tiller" + // Options control how to install tiller into a cluster, upgrade, and uninstall tiller from a cluster. type Options struct { // EnableTLS instructs tiller to serve with TLS enabled. @@ -43,7 +50,7 @@ type Options struct { // key tiller should use. // // Required and valid if and only if EnableTLS or VerifyTLS is set. - TLSKey string + TLSKeyFile string // TLSCertFile identifies the file containing the pem encoded TLS // certificate tiller should use. @@ -57,3 +64,16 @@ type Options struct { // Required and valid if and only if VerifyTLS is set. TLSCaCertFile string } + +func (opts *Options) selectImage() string { + switch { + case opts.UseCanary: + return defaultImage + ":canary" + case opts.ImageSpec == "": + return fmt.Sprintf("%s:%s", defaultImage, version.Version) + default: + return opts.ImageSpec + } +} + +func (opts *Options) tls() bool { return opts.EnableTLS || opts.VerifyTLS } diff --git a/cmd/helm/installer/uninstall_test.go b/cmd/helm/installer/uninstall_test.go index e0db07fe6..bda91ca1c 100644 --- a/cmd/helm/installer/uninstall_test.go +++ b/cmd/helm/installer/uninstall_test.go @@ -55,7 +55,11 @@ func (f *fakeReaperFactory) Reaper(mapping *meta.RESTMapping) (kubectl.Reaper, e func TestUninstall(t *testing.T) { existingService := service(api.NamespaceDefault) - existingDeployment := deployment(api.NamespaceDefault, "image", false) + existingDeployment := deployment(&Options{ + Namespace: api.NamespaceDefault, + ImageSpec: "image", + UseCanary: false, + }) fc := &fake.Clientset{} fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { @@ -92,7 +96,7 @@ func TestUninstall(t *testing.T) { } func TestUninstall_serviceNotFound(t *testing.T) { - existingDeployment := deployment(api.NamespaceDefault, "imageToReplace", false) + existingDeployment := deployment(&Options{Namespace: api.NamespaceDefault, ImageSpec: "imageToReplace", UseCanary: false}) fc := &fake.Clientset{} fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) {