Commit d41f0933 authored by Taylor Thomas's avatar Taylor Thomas Committed by GitHub

Merge pull request #1850 from frodenas/deinit

feat(helm): Add  command to uninstall Tiller
parents e440a36d 074d746c
......@@ -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))
}
/*
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
}
/*
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)
}
}
/*
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
}
/*
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)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment