From 074d746c956e49b476643be95c18de9e791d2163 Mon Sep 17 00:00:00 2001 From: Ferran Rodenas Date: Sat, 21 Jan 2017 15:34:54 +0100 Subject: [PATCH] Add reset command to uninstall Tiller --- cmd/helm/helm.go | 7 + cmd/helm/installer/uninstall.go | 83 ++++++++++++ cmd/helm/installer/uninstall_test.go | 160 +++++++++++++++++++++++ cmd/helm/reset.go | 131 +++++++++++++++++++ cmd/helm/reset_test.go | 187 +++++++++++++++++++++++++++ 5 files changed, 568 insertions(+) create mode 100644 cmd/helm/installer/uninstall.go create mode 100644 cmd/helm/installer/uninstall_test.go create mode 100644 cmd/helm/reset.go create mode 100644 cmd/helm/reset_test.go diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 352a8340a..e71c0da8f 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -120,6 +120,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newCompletionCmd(out), newHomeCmd(out), newInitCmd(out), + newResetCmd(nil, out), newVersionCmd(nil, out), // Hidden documentation generator command: 'helm docs' @@ -237,3 +238,9 @@ func getKubeClient(context string) (*restclient.Config, *internalclientset.Clien } return config, client, nil } + +// getKubeCmd is a convenience method for creating kubernetes cmd client +// for a given kubeconfig context +func getKubeCmd(context string) *kube.Client { + return kube.New(kube.GetConfig(context)) +} diff --git a/cmd/helm/installer/uninstall.go b/cmd/helm/installer/uninstall.go new file mode 100644 index 000000000..d3e5858fa --- /dev/null +++ b/cmd/helm/installer/uninstall.go @@ -0,0 +1,83 @@ +/* +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 ( + "strings" + + "github.com/ghodss/yaml" + + "k8s.io/kubernetes/pkg/api" + kerrors "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" + + "k8s.io/helm/pkg/kube" +) + +// Uninstall uses kubernetes client to uninstall tiller +func Uninstall(kubeClient internalclientset.Interface, kubeCmd *kube.Client, namespace string, verbose bool) error { + if _, err := kubeClient.Core().Services(namespace).Get("tiller-deploy"); err != nil { + if !kerrors.IsNotFound(err) { + return err + } + } else if err := deleteService(kubeClient.Core(), namespace); err != nil { + return err + } + if obj, err := kubeClient.Extensions().Deployments(namespace).Get("tiller-deploy"); err != nil { + if !kerrors.IsNotFound(err) { + return err + } + } else if err := deleteDeployment(kubeCmd, namespace, obj); err != nil { + return err + } + return nil +} + +// deleteService deletes the Tiller Service resource +func deleteService(client internalversion.ServicesGetter, namespace string) error { + return client.Services(namespace).Delete("tiller-deploy", &api.DeleteOptions{}) +} + +// deleteDeployment deletes the Tiller Deployment resource +// We need to use the kubeCmd reaper instead of the kube API because GC for deployment dependents +// is not yet supported at the k8s server level (<= 1.5) +func deleteDeployment(kubeCmd *kube.Client, namespace string, obj *extensions.Deployment) error { + obj.Kind = "Deployment" + obj.APIVersion = "extensions/v1beta1" + buf, err := yaml.Marshal(obj) + if err != nil { + return err + } + reader := strings.NewReader(string(buf)) + infos, err := kubeCmd.Build(namespace, reader) + if err != nil { + return err + } + for _, info := range infos { + reaper, err := kubeCmd.Reaper(info.Mapping) + if err != nil { + return err + } + err = reaper.Stop(info.Namespace, info.Name, 0, nil) + if err != nil { + return err + } + } + return nil +} diff --git a/cmd/helm/installer/uninstall_test.go b/cmd/helm/installer/uninstall_test.go new file mode 100644 index 000000000..2d9525096 --- /dev/null +++ b/cmd/helm/installer/uninstall_test.go @@ -0,0 +1,160 @@ +/* +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 ( + "testing" + "time" + + "k8s.io/kubernetes/pkg/api" + apierrors "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/meta" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + testcore "k8s.io/kubernetes/pkg/client/testing/core" + "k8s.io/kubernetes/pkg/kubectl" + cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/runtime" + + "k8s.io/helm/pkg/kube" +) + +type fakeReaper struct { + namespace string + name string +} + +func (r *fakeReaper) Stop(namespace, name string, timeout time.Duration, gracePeriod *api.DeleteOptions) error { + r.namespace = namespace + r.name = name + return nil +} + +type fakeReaperFactory struct { + cmdutil.Factory + reaper kubectl.Reaper +} + +func (f *fakeReaperFactory) Reaper(mapping *meta.RESTMapping) (kubectl.Reaper, error) { + return f.reaper, nil +} + +func TestUninstall(t *testing.T) { + existingService := service(api.NamespaceDefault) + existingDeployment := deployment(api.NamespaceDefault, "image", false) + + fc := &fake.Clientset{} + fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingService, nil + }) + fc.AddReactor("delete", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, nil, nil + }) + fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingDeployment, nil + }) + + f, _, _, _ := cmdtesting.NewAPIFactory() + r := &fakeReaper{} + rf := &fakeReaperFactory{Factory: f, reaper: r} + kc := &kube.Client{Factory: rf} + + if err := Uninstall(fc, kc, api.NamespaceDefault, false); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 3 { + t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) + } + + if r.namespace != api.NamespaceDefault { + t.Errorf("unexpected reaper namespace: %s", r.name) + } + + if r.name != "tiller-deploy" { + t.Errorf("unexpected reaper name: %s", r.name) + } +} + +func TestUninstall_serviceNotFound(t *testing.T) { + existingDeployment := deployment(api.NamespaceDefault, "imageToReplace", false) + + fc := &fake.Clientset{} + fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, nil, apierrors.NewNotFound(api.Resource("services"), "1") + }) + fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingDeployment, nil + }) + + f, _, _, _ := cmdtesting.NewAPIFactory() + r := &fakeReaper{} + rf := &fakeReaperFactory{Factory: f, reaper: r} + kc := &kube.Client{Factory: rf} + + if err := Uninstall(fc, kc, api.NamespaceDefault, false); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 2 { + t.Errorf("unexpected actions: %v, expected 2 actions got %d", actions, len(actions)) + } + + if r.namespace != api.NamespaceDefault { + t.Errorf("unexpected reaper namespace: %s", r.name) + } + + if r.name != "tiller-deploy" { + t.Errorf("unexpected reaper name: %s", r.name) + } +} + +func TestUninstall_deploymentNotFound(t *testing.T) { + existingService := service(api.NamespaceDefault) + + fc := &fake.Clientset{} + fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, existingService, nil + }) + fc.AddReactor("delete", "services", func(action testcore.Action) (bool, runtime.Object, error) { + return true, nil, nil + }) + fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { + return true, nil, apierrors.NewNotFound(api.Resource("deployments"), "1") + }) + + f, _, _, _ := cmdtesting.NewAPIFactory() + r := &fakeReaper{} + rf := &fakeReaperFactory{Factory: f, reaper: r} + kc := &kube.Client{Factory: rf} + + if err := Uninstall(fc, kc, api.NamespaceDefault, false); err != nil { + t.Errorf("unexpected error: %#+v", err) + } + + if actions := fc.Actions(); len(actions) != 3 { + t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) + } + + if r.namespace != "" { + t.Errorf("unexpected reaper namespace: %s", r.name) + } + + if r.name != "" { + t.Errorf("unexpected reaper name: %s", r.name) + } +} diff --git a/cmd/helm/reset.go b/cmd/helm/reset.go new file mode 100644 index 000000000..a6c5d42bb --- /dev/null +++ b/cmd/helm/reset.go @@ -0,0 +1,131 @@ +/* +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 main + +import ( + "errors" + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/cmd/helm/installer" + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/proto/hapi/release" +) + +const resetDesc = ` +This command uninstalls Tiller (the helm server side component) from your +Kubernetes Cluster and optionally deletes local configuration in +$HELM_HOME (default ~/.helm/) +` + +type resetCmd struct { + force bool + removeHelmHome bool + namespace string + out io.Writer + home helmpath.Home + client helm.Interface + kubeClient internalclientset.Interface + kubeCmd *kube.Client +} + +func newResetCmd(client helm.Interface, out io.Writer) *cobra.Command { + d := &resetCmd{ + out: out, + client: client, + } + + cmd := &cobra.Command{ + Use: "reset", + Short: "uninstalls Tiller from a cluster", + Long: resetDesc, + PersistentPreRunE: setupConnection, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return errors.New("This command does not accept arguments") + } + + d.namespace = tillerNamespace + d.home = helmpath.Home(homePath()) + d.client = ensureHelmClient(d.client) + + return d.run() + }, + } + + f := cmd.Flags() + f.BoolVarP(&d.force, "force", "f", false, "forces Tiller uninstall even if there are releases installed") + f.BoolVar(&d.removeHelmHome, "remove-helm-home", false, "if set deletes $HELM_HOME") + + return cmd +} + +// runReset uninstalls tiller from Kubernetes Cluster and deletes local config +func (d *resetCmd) run() error { + if d.kubeClient == nil { + _, c, err := getKubeClient(kubeContext) + if err != nil { + return fmt.Errorf("could not get kubernetes client: %s", err) + } + d.kubeClient = c + } + if d.kubeCmd == nil { + d.kubeCmd = getKubeCmd(kubeContext) + } + + res, err := d.client.ListReleases( + helm.ReleaseListStatuses([]release.Status_Code{release.Status_DEPLOYED}), + ) + if err != nil { + return prettyError(err) + } + + if len(res.Releases) > 0 && !d.force { + return fmt.Errorf("There are still %d deployed releases (Tip: use --force).", len(res.Releases)) + } + + if err := installer.Uninstall(d.kubeClient, d.kubeCmd, d.namespace, flagDebug); err != nil { + return fmt.Errorf("error unstalling Tiller: %s", err) + } + + if d.removeHelmHome { + if err := deleteDirectories(d.home, d.out); err != nil { + return err + } + } + + fmt.Fprintln(d.out, "Tiller (the helm server side component) has been uninstalled from your Kubernetes Cluster.") + return nil +} + +// deleteDirectories deletes $HELM_HOME +func deleteDirectories(home helmpath.Home, out io.Writer) error { + if _, err := os.Stat(home.String()); err == nil { + fmt.Fprintf(out, "Deleting %s \n", home.String()) + if err := os.RemoveAll(home.String()); err != nil { + return fmt.Errorf("Could not remove %s: %s", home.String(), err) + } + } + + return nil +} diff --git a/cmd/helm/reset_test.go b/cmd/helm/reset_test.go new file mode 100644 index 000000000..42b7aebce --- /dev/null +++ b/cmd/helm/reset_test.go @@ -0,0 +1,187 @@ +/* +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 main + +import ( + "bytes" + "io/ioutil" + "os" + "strings" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestResetCmd(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + c := &fakeReleaseClient{} + fc := fake.NewSimpleClientset() + cmd := &resetCmd{ + out: &buf, + home: helmpath.Home(home), + client: c, + kubeClient: fc, + namespace: api.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Errorf("unexpected error: %v", err) + } + actions := fc.Actions() + if len(actions) != 2 { + t.Errorf("Expected 2 actions, got %d", len(actions)) + } + if !actions[0].Matches("get", "services") { + t.Errorf("unexpected action: %v, expected get service", actions[1]) + } + if !actions[1].Matches("get", "deployments") { + t.Errorf("unexpected action: %v, expected get deployment", actions[0]) + } + expected := "Tiller (the helm server side component) has been uninstalled from your Kubernetes Cluster." + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } + if _, err := os.Stat(home); err != nil { + t.Errorf("Helm home directory %s does not exists", home) + } +} + +func TestResetCmd_removeHelmHome(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + c := &fakeReleaseClient{} + fc := fake.NewSimpleClientset() + cmd := &resetCmd{ + removeHelmHome: true, + out: &buf, + home: helmpath.Home(home), + client: c, + kubeClient: fc, + namespace: api.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Errorf("unexpected error: %v", err) + } + actions := fc.Actions() + if len(actions) != 2 { + t.Errorf("Expected 2 actions, got %d", len(actions)) + } + if !actions[0].Matches("get", "services") { + t.Errorf("unexpected action: %v, expected get service", actions[1]) + } + if !actions[1].Matches("get", "deployments") { + t.Errorf("unexpected action: %v, expected get deployment", actions[0]) + } + expected := "Tiller (the helm server side component) has been uninstalled from your Kubernetes Cluster." + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } + if _, err := os.Stat(home); err == nil { + t.Errorf("Helm home directory %s already exists", home) + } +} + +func TestReset_deployedReleases(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + resp := []*release.Release{ + releaseMock(&releaseOptions{name: "atlas-guide", statusCode: release.Status_DEPLOYED}), + } + c := &fakeReleaseClient{ + rels: resp, + } + fc := fake.NewSimpleClientset() + cmd := &resetCmd{ + out: &buf, + home: helmpath.Home(home), + client: c, + kubeClient: fc, + namespace: api.NamespaceDefault, + } + err = cmd.run() + expected := "There are still 1 deployed releases (Tip: use --force)" + if !strings.Contains(err.Error(), expected) { + t.Errorf("unexpected error: %v", err) + } + if _, err := os.Stat(home); err != nil { + t.Errorf("Helm home directory %s does not exists", home) + } +} + +func TestReset_forceFlag(t *testing.T) { + home, err := ioutil.TempDir("", "helm_home") + if err != nil { + t.Fatal(err) + } + defer os.Remove(home) + + var buf bytes.Buffer + resp := []*release.Release{ + releaseMock(&releaseOptions{name: "atlas-guide", statusCode: release.Status_DEPLOYED}), + } + c := &fakeReleaseClient{ + rels: resp, + } + fc := fake.NewSimpleClientset() + cmd := &resetCmd{ + force: true, + out: &buf, + home: helmpath.Home(home), + client: c, + kubeClient: fc, + namespace: api.NamespaceDefault, + } + if err := cmd.run(); err != nil { + t.Errorf("unexpected error: %v", err) + } + actions := fc.Actions() + if len(actions) != 2 { + t.Errorf("Expected 2 actions, got %d", len(actions)) + } + if !actions[0].Matches("get", "services") { + t.Errorf("unexpected action: %v, expected get service", actions[1]) + } + if !actions[1].Matches("get", "deployments") { + t.Errorf("unexpected action: %v, expected get deployment", actions[0]) + } + expected := "Tiller (the helm server side component) has been uninstalled from your Kubernetes Cluster." + if !strings.Contains(buf.String(), expected) { + t.Errorf("expected %q, got %q", expected, buf.String()) + } + if _, err := os.Stat(home); err != nil { + t.Errorf("Helm home directory %s does not exists", home) + } +}