From 0f5990f4cdc485a06e704b2a0210e9ce046f206d Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 13 Oct 2016 12:36:52 -0700 Subject: [PATCH] feat(helm): add kubeconfig context switching to init command - decouple tunnel from kube client - add context switching for init cmd - add unit tests for installer and init command - refactor installer and remove unused code --- cmd/helm/helm.go | 22 ++++++++- cmd/helm/init.go | 15 +++++-- cmd/helm/init_test.go | 72 ++++++++++++++++++++++++++++++ cmd/helm/installer/install.go | 47 +++---------------- cmd/helm/installer/install_test.go | 49 ++++++++++++++++++++ cmd/helm/tunnel.go | 18 ++------ glide.lock | 7 ++- glide.yaml | 11 ++--- pkg/kube/config.go | 31 +++++++++++++ pkg/kube/tunnel.go | 68 +++++++++++++++------------- 10 files changed, 238 insertions(+), 102 deletions(-) create mode 100644 cmd/helm/installer/install_test.go create mode 100644 pkg/kube/config.go diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 678314441..07ef7bcc5 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -25,6 +25,10 @@ import ( "github.com/spf13/cobra" "google.golang.org/grpc" + "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/client/unversioned" + + "k8s.io/helm/pkg/kube" ) const ( @@ -145,8 +149,8 @@ func setupConnection(c *cobra.Command, args []string) error { } func teardown() { - if tunnel != nil { - tunnel.Close() + if tillerTunnel != nil { + tillerTunnel.Close() } } @@ -176,3 +180,17 @@ func prettyError(err error) error { func homePath() string { return os.ExpandEnv(helmHome) } + +// getKubeClient is a convenience method for creating kubernetes config and client +// for a given kubeconfig context +func getKubeClient(context string) (*restclient.Config, *unversioned.Client, error) { + config, err := kube.GetConfig(context).ClientConfig() + if err != nil { + return nil, nil, fmt.Errorf("could not get kubernetes config for context '%s': %s", context, err) + } + client, err := unversioned.New(config) + if err != nil { + return nil, nil, fmt.Errorf("could not get kubernetes client: %s", err) + } + return config, client, nil +} diff --git a/cmd/helm/init.go b/cmd/helm/init.go index 8e7d11ee1..3a7354cb8 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -21,9 +21,10 @@ import ( "fmt" "io" "os" - "strings" "github.com/spf13/cobra" + kerrors "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/cmd/helm/installer" @@ -47,6 +48,7 @@ type initCmd struct { clientOnly bool out io.Writer home helmpath.Home + kubeClient unversioned.DeploymentsNamespacer } func newInitCmd(out io.Writer) *cobra.Command { @@ -77,8 +79,15 @@ func (i *initCmd) run() error { } if !i.clientOnly { - if err := installer.Install(tillerNamespace, i.image, flagDebug); err != nil { - if !strings.Contains(err.Error(), `"tiller-deploy" already exists`) { + if i.kubeClient == nil { + _, c, err := getKubeClient(kubeContext) + if err != nil { + return fmt.Errorf("could not get kubernetes client: %s", err) + } + i.kubeClient = c + } + if err := installer.Install(i.kubeClient, tillerNamespace, i.image, flagDebug); err != nil { + if !kerrors.IsAlreadyExists(err) { return fmt.Errorf("error installing: %s", err) } fmt.Fprintln(i.out, "Warning: Tiller is already installed in the cluster. (Use --client-only to suppress this message.)") diff --git a/cmd/helm/init_test.go b/cmd/helm/init_test.go index bbea3b608..5b67ea962 100644 --- a/cmd/helm/init_test.go +++ b/cmd/helm/init_test.go @@ -20,11 +20,83 @@ import ( "bytes" "io/ioutil" "os" + "strings" "testing" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/helm/cmd/helm/helmpath" ) +func TestInitCmd(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + fake := testclient.Fake{} + cmd := &initCmd{out: &buf, home: helmpath.Home(home), kubeClient: fake.Extensions()} + if err := cmd.run(); err != nil { + t.Errorf("expected error: %v", err) + } + actions := fake.Actions() + if action, ok := actions[0].(testclient.CreateAction); !ok || action.GetResource() != "deployments" { + t.Errorf("unexpected action: %v, expected create deployment", actions[0]) + } + expected := "Tiller (the helm server side component) has been installed into your Kubernetes Cluster." + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } +} + +func TestInitCmd_exsits(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + fake := testclient.Fake{} + fake.AddReactor("*", "*", func(action testclient.Action) (bool, runtime.Object, error) { + return true, nil, errors.NewAlreadyExists(api.Resource("deployments"), "1") + }) + cmd := &initCmd{out: &buf, home: helmpath.Home(home), kubeClient: fake.Extensions()} + if err := cmd.run(); err != nil { + t.Errorf("expected error: %v", err) + } + expected := "Warning: Tiller is already installed in the cluster. (Use --client-only to suppress this message.)" + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } +} + +func TestInitCmd_clientOnly(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + fake := testclient.Fake{} + cmd := &initCmd{out: &buf, home: helmpath.Home(home), kubeClient: fake.Extensions(), clientOnly: true} + if err := cmd.run(); err != nil { + t.Errorf("expected error: %v", err) + } + if len(fake.Actions()) != 0 { + t.Error("expected client call") + } + expected := "Not installing tiller due to 'client-only' flag having been set" + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } +} func TestEnsureHome(t *testing.T) { home, err := ioutil.TempDir("", "helm_home") if err != nil { diff --git a/cmd/helm/installer/install.go b/cmd/helm/installer/install.go index 0bfd197e8..2fa600d10 100644 --- a/cmd/helm/installer/install.go +++ b/cmd/helm/installer/install.go @@ -18,14 +18,12 @@ package installer // import "k8s.io/helm/cmd/helm/installer" import ( "fmt" - "strings" "k8s.io/kubernetes/pkg/api" - "k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/util/intstr" - "k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/version" ) @@ -37,38 +35,12 @@ const defaultImage = "gcr.io/kubernetes-helm/tiller" // command failed. // // If verbose is true, this will print the manifest to stdout. -func Install(namespace, image string, verbose bool) error { - kc := kube.New(nil) - - if namespace == "" { - ns, _, err := kc.DefaultNamespace() - if err != nil { - return err - } - namespace = ns - } - - c, err := kc.Client() - if err != nil { - return err - } - - ns := generateNamespace(namespace) - if _, err := c.Namespaces().Create(ns); err != nil { - if !errors.IsAlreadyExists(err) { - return err - } - } - +func Install(client unversioned.DeploymentsNamespacer, namespace, image string, verbose bool) error { if image == "" { - // strip git sha off version - tag := strings.Split(version.Version, "+")[0] - image = fmt.Sprintf("%s:%s", defaultImage, tag) + image = fmt.Sprintf("%s:%s", defaultImage, version.Version) } - - rc := generateDeployment(image) - - _, err = c.Deployments(namespace).Create(rc) + obj := generateDeployment(image) + _, err := client.Deployments(namespace).Create(obj) return err } @@ -125,12 +97,3 @@ func generateDeployment(image string) *extensions.Deployment { } return d } - -func generateNamespace(namespace string) *api.Namespace { - return &api.Namespace{ - ObjectMeta: api.ObjectMeta{ - Name: namespace, - Labels: generateLabels(map[string]string{"name": "helm-namespace"}), - }, - } -} diff --git a/cmd/helm/installer/install_test.go b/cmd/helm/installer/install_test.go new file mode 100644 index 000000000..3bb320806 --- /dev/null +++ b/cmd/helm/installer/install_test.go @@ -0,0 +1,49 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package installer // import "k8s.io/helm/cmd/helm/installer" + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" + "k8s.io/kubernetes/pkg/runtime" +) + +func TestInstall(t *testing.T) { + image := "gcr.io/kubernetes-helm/tiller:v2.0.0" + + fake := testclient.Fake{} + fake.AddReactor("create", "deployments", func(action testclient.Action) (bool, runtime.Object, error) { + obj := action.(testclient.CreateAction).GetObject().(*extensions.Deployment) + l := obj.GetLabels() + if reflect.DeepEqual(l, map[string]string{"app": "helm"}) { + t.Errorf("expected labels = '', got '%s'", l) + } + i := obj.Spec.Template.Spec.Containers[0].Image + if i != image { + t.Errorf("expected image = '%s', got '%s'", image, i) + } + return true, obj, nil + }) + + err := Install(fake.Extensions(), "default", image, false) + if err != nil { + t.Errorf("unexpected error: %#+v", err) + } +} diff --git a/cmd/helm/tunnel.go b/cmd/helm/tunnel.go index b6b7fbebd..6b5142397 100644 --- a/cmd/helm/tunnel.go +++ b/cmd/helm/tunnel.go @@ -21,27 +21,16 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/client/unversioned" - "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" "k8s.io/kubernetes/pkg/labels" "k8s.io/helm/pkg/kube" ) // TODO refactor out this global var -var tunnel *kube.Tunnel - -func getKubeConfig(context string) clientcmd.ClientConfig { - rules := clientcmd.NewDefaultClientConfigLoadingRules() - overrides := &clientcmd.ConfigOverrides{} - if context != "" { - overrides.CurrentContext = context - } - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides) -} +var tillerTunnel *kube.Tunnel func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) { - kc := kube.New(getKubeConfig(context)) - client, err := kc.Client() + config, client, err := getKubeClient(context) if err != nil { return nil, err } @@ -51,7 +40,8 @@ func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) { return nil, err } const tillerPort = 44134 - return kc.ForwardPort(namespace, podName, tillerPort) + t := kube.NewTunnel(client.RESTClient, config, namespace, podName, tillerPort) + return t, t.ForwardPort() } func getTillerPodName(client unversioned.PodsNamespacer, namespace string) (string, error) { diff --git a/glide.lock b/glide.lock index bab587bd7..e5dd9bd7a 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 0b56505a7d2b0bde1a8aba9c4ac52ef18ea1eae6d46157db598e5a1051b64cf5 -updated: 2016-10-11T12:54:05.869559929-06:00 +hash: e04a956fc7be01bd1a2450cdc0000ed25394da383f0c7a7e057a9377bd832c1e +updated: 2016-10-13T12:28:13.380298639-07:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -434,7 +434,7 @@ imports: - 1.4/tools/metrics - 1.4/transport - name: k8s.io/kubernetes - version: ef16c3f8079df0654c8336741134ba142846ec13 + version: fc3dab7de68c15de3421896dd051c2f127fb64ab subpackages: - federation/apis/federation - federation/apis/federation/install @@ -446,7 +446,6 @@ imports: - pkg/api - pkg/api/annotations - pkg/api/endpoints - - pkg/api/error - pkg/api/errors - pkg/api/install - pkg/api/meta diff --git a/glide.yaml b/glide.yaml index e7a599b4d..8d30c72b2 100644 --- a/glide.yaml +++ b/glide.yaml @@ -25,25 +25,26 @@ import: version: ~1.4.1 subpackages: - pkg/api - - pkg/api/meta - - pkg/api/error + - pkg/api/errors - pkg/api/unversioned - pkg/apimachinery/registered + - pkg/apis/batch + - pkg/apis/extensions - pkg/client/restclient - pkg/client/unversioned - - pkg/apis/batch - pkg/client/unversioned/clientcmd - - pkg/client/unversioned/fake - pkg/client/unversioned/portforward - pkg/client/unversioned/remotecommand + - pkg/client/unversioned/testclient - pkg/kubectl - pkg/kubectl/cmd/util - pkg/kubectl/resource - pkg/labels - pkg/runtime - - pkg/watch + - pkg/util/intstr - pkg/util/strategicpatch - pkg/util/yaml + - pkg/watch - package: github.com/gosuri/uitable - package: github.com/asaskevich/govalidator version: ^4.0.0 diff --git a/pkg/kube/config.go b/pkg/kube/config.go new file mode 100644 index 000000000..fcb412d80 --- /dev/null +++ b/pkg/kube/config.go @@ -0,0 +1,31 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube // import "k8s.io/helm/pkg/kube" + +import ( + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" +) + +// GetConfig returns a kubernetes client config for a given context. +func GetConfig(context string) clientcmd.ClientConfig { + rules := clientcmd.NewDefaultClientConfigLoadingRules() + overrides := &clientcmd.ConfigOverrides{} + if context != "" { + overrides.CurrentContext = context + } + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides) +} diff --git a/pkg/kube/tunnel.go b/pkg/kube/tunnel.go index 53e0b1a9a..dc279bc68 100644 --- a/pkg/kube/tunnel.go +++ b/pkg/kube/tunnel.go @@ -17,11 +17,13 @@ limitations under the License. package kube import ( - "bytes" "fmt" + "io" + "io/ioutil" "net" "strconv" + "k8s.io/kubernetes/pkg/client/restclient" "k8s.io/kubernetes/pkg/client/unversioned/portforward" "k8s.io/kubernetes/pkg/client/unversioned/remotecommand" ) @@ -30,8 +32,27 @@ import ( type Tunnel struct { Local int Remote int + Namespace string + PodName string + Out io.Writer stopChan chan struct{} readyChan chan struct{} + config *restclient.Config + client *restclient.RESTClient +} + +// NewTunnel creates a new tunnel +func NewTunnel(client *restclient.RESTClient, config *restclient.Config, namespace, podName string, remote int) *Tunnel { + return &Tunnel{ + config: config, + client: client, + Namespace: namespace, + PodName: podName, + Remote: remote, + stopChan: make(chan struct{}, 1), + readyChan: make(chan struct{}, 1), + Out: ioutil.Discard, + } } // Close disconnects a tunnel connection @@ -41,48 +62,31 @@ func (t *Tunnel) Close() { } // ForwardPort opens a tunnel to a kubernetes pod -func (c *Client) ForwardPort(namespace, podName string, remote int) (*Tunnel, error) { - client, err := c.Client() - if err != nil { - return nil, err - } - - config, err := c.ClientConfig() - if err != nil { - return nil, err - } - - // Build a url to the portforward endpoing +func (t *Tunnel) ForwardPort() error { + // Build a url to the portforward endpoint // example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-deploy-9itlq/portforward - u := client.RESTClient.Post(). + u := t.client.Post(). Resource("pods"). - Namespace(namespace). - Name(podName). + Namespace(t.Namespace). + Name(t.PodName). SubResource("portforward").URL() - dialer, err := remotecommand.NewExecutor(config, "POST", u) + dialer, err := remotecommand.NewExecutor(t.config, "POST", u) if err != nil { - return nil, err + return err } local, err := getAvailablePort() if err != nil { - return nil, err - } - - t := &Tunnel{ - Local: local, - Remote: remote, - stopChan: make(chan struct{}, 1), - readyChan: make(chan struct{}, 1), + return fmt.Errorf("could not find an available port: %s", err) } + t.Local = local - ports := []string{fmt.Sprintf("%d:%d", local, remote)} + ports := []string{fmt.Sprintf("%d:%d", t.Local, t.Remote)} - var b bytes.Buffer - pf, err := portforward.New(dialer, ports, t.stopChan, t.readyChan, &b, &b) + pf, err := portforward.New(dialer, ports, t.stopChan, t.readyChan, t.Out, t.Out) if err != nil { - return nil, err + return err } errChan := make(chan error) @@ -92,9 +96,9 @@ func (c *Client) ForwardPort(namespace, podName string, remote int) (*Tunnel, er select { case err = <-errChan: - return t, fmt.Errorf("Error forwarding ports: %v\n", err) + return fmt.Errorf("Error forwarding ports: %v\n", err) case <-pf.Ready: - return t, nil + return nil } }