From 8f1a15c9cd24ae7c8c87103b214097b6430361b8 Mon Sep 17 00:00:00 2001 From: KUOKA Yusuke Date: Tue, 2 Apr 2019 21:17:38 +0900 Subject: [PATCH] feat: `helmfile destroy` deletes and purges releases (#530) * feat: `helmfile destroy` deletes and purges releases This adds `helmfile destroy` that is basically `helmfile delete --purge`. I've also tweaked the behavior of `delete` and `destroy` for releases with `installed: false`, so that it becomes consistent with other helmfile commands. It now delete releases only when `installed: true` AND the release is already installed. **Why an another command?** Because it's easy to remember, and it also makes it easier to iterate on your helmfile. We've been using `helmfile delete` from the beginning of helmfile, and several months have been passed since we've added `--purge` to it. We noticed that we always prefer to use `--purge` so that we can quickly iterate on helmfile by e.g. `helmfile delete --purge && helmfile sync`. But making `--purge` default makes the `delete` command inconsistent with the helm's `delete`. `destroy`, on the other hand, doesn't have such problem, and is still easy to remember for terraform users. Resolves #511 * Update docs about `helmfile delete` and `helmfile destroy` --- README.md | 20 ++++++-- main.go | 41 ++++++++++++++- state/state.go | 4 ++ state/state_test.go | 120 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 178 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 094ecbb8..c0bac61f 100644 --- a/README.md +++ b/README.md @@ -212,16 +212,20 @@ NAME: USAGE: helmfile [global options] command [command options] [arguments...] +VERSION: + v0.52.0 + COMMANDS: repos sync repositories from state file (helm repo add && helm repo update) - charts sync releases from state file (helm upgrade --install) + charts DEPRECATED: sync releases from state file (helm upgrade --install) diff diff releases from state file against env (helm diff) template template releases from state file against env (helm template) lint lint charts from state file (helm lint) sync sync all resources from state file (repos, releases and chart deps) apply apply all resources from state file only when there are changes status retrieve status of releases in state file - delete delete releases from state file (helm delete) + delete DEPRECATED: delete releases from state file (helm delete) + destroy deletes and then purges releases test test releases from state file (helm test) GLOBAL OPTIONS: @@ -265,7 +269,15 @@ The `helmfile apply` sub-command begins by executing `diff`. If `diff` finds tha An expected use-case of `apply` is to schedule it to run periodically, so that you can auto-fix skews between the desired and the current state of your apps running on Kubernetes clusters. -### delete +### destroy + +The `helmfile destroy` sub-command deletes and purges all the releases defined in the manifests. + +`helmfile --interactive destroy` instructs Helmfile to request your confirmation before actually deleting releases. + +`destroy` basically runs `helm delete --purge` on all the targeted releases. If you don't want purging, use `helmfile delete` instead. + +### delete (DEPRECATED) The `helmfile delete` sub-command deletes all the releases defined in the manifests. @@ -762,7 +774,7 @@ Please see #203 for more context. ## Running helmfile interactively -`helmfile --interactive [apply|delete]` requests confirmation from you before actually modifying your cluster. +`helmfile --interactive [apply|destroy]` requests confirmation from you before actually modifying your cluster. Use it when you're running `helmfile` manually on your local machine or a kind of secure administrative hosts. diff --git a/main.go b/main.go index 69b9b80e..a71640fb 100644 --- a/main.go +++ b/main.go @@ -117,7 +117,7 @@ func main() { }, { Name: "charts", - Usage: "sync releases from state file (helm upgrade --install)", + Usage: "DEPRECATED: sync releases from state file (helm upgrade --install)", Flags: []cli.Flag{ cli.StringFlag{ Name: "args", @@ -435,7 +435,7 @@ Do you really want to apply? }, { Name: "delete", - Usage: "delete releases from state file (helm delete)", + Usage: "DEPRECATED: delete releases from state file (helm delete)", Flags: []cli.Flag{ cli.StringFlag{ Name: "args", @@ -476,6 +476,43 @@ Do you really want to delete? }) }, }, + { + Name: "destroy", + Usage: "deletes and then purges releases", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "args", + Value: "", + Usage: "pass args to helm exec", + }, + }, + Action: func(c *cli.Context) error { + return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error { + args := args.GetArgs(c.String("args"), state) + if len(args) > 0 { + helm.SetExtraArgs(args...) + } + + names := make([]string, len(state.Releases)) + for i, r := range state.Releases { + names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart) + } + + msg := fmt.Sprintf(`Affected releases are: +%s + +Do you really want to delete? + Helmfile will delete all your releases, as shown above. + +`, strings.Join(names, "\n")) + interactive := c.GlobalBool("interactive") + if !interactive || interactive && askForConfirmation(msg) { + return state.DeleteReleases(helm, true) + } + return nil + }) + }, + }, { Name: "test", Usage: "test releases from state file (helm test)", diff --git a/state/state.go b/state/state.go index 884c5e46..9195fd81 100644 --- a/state/state.go +++ b/state/state.go @@ -742,6 +742,10 @@ func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) [ // DeleteReleases wrapper for executing helm delete on the releases func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error { return st.scatterGatherReleases(helm, len(st.Releases), func(release ReleaseSpec) error { + if !release.Desired() { + return nil + } + flags := []string{} if purge { flags = append(flags, "--purge") diff --git a/state/state_test.go b/state/state_test.go index 4f224d31..fa839b1e 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -635,6 +635,8 @@ type mockHelmExec struct { charts []string repo []string releases []mockRelease + deleted []mockRelease + lists map[string]string } type mockRelease struct { @@ -690,10 +692,11 @@ func (helm *mockHelmExec) ReleaseStatus(release string) error { return nil } func (helm *mockHelmExec) DeleteRelease(name string, flags ...string) error { + helm.deleted = append(helm.deleted, mockRelease{name: name, flags: flags}) return nil } func (helm *mockHelmExec) List(filter string) (string, error) { - return "", nil + return helm.lists[filter], nil } func (helm *mockHelmExec) DecryptSecret(name string) (string, error) { return "", nil @@ -1291,3 +1294,118 @@ func TestHelmState_NoReleaseMatched(t *testing.T) { t.Run(tt.name, i) } } + +func TestHelmState_Delete(t *testing.T) { + tests := []struct { + name string + deleted []mockRelease + wantErr bool + desired *bool + installed bool + purge bool + }{ + { + name: "desired and installed (purge=false)", + wantErr: false, + desired: boolValue(true), + installed: true, + purge: false, + deleted: []mockRelease{{"releaseA", []string{}}}, + }, + { + name: "desired(default) and installed (purge=false)", + wantErr: false, + desired: nil, + installed: true, + purge: false, + deleted: []mockRelease{{"releaseA", []string{}}}, + }, + { + name: "desired and installed (purge=true)", + wantErr: false, + desired: boolValue(true), + installed: true, + purge: true, + deleted: []mockRelease{{"releaseA", []string{"--purge"}}}, + }, + { + name: "desired but not installed (purge=false)", + wantErr: false, + desired: boolValue(true), + installed: false, + purge: false, + deleted: []mockRelease{}, + }, + { + name: "desired but not installed (purge=true)", + wantErr: false, + desired: boolValue(true), + installed: false, + purge: true, + deleted: []mockRelease{}, + }, + { + name: "installed but filtered (purge=false)", + wantErr: false, + desired: boolValue(false), + installed: true, + purge: false, + deleted: []mockRelease{}, + }, + { + name: "installed but filtered (purge=true)", + wantErr: false, + desired: boolValue(false), + installed: true, + purge: true, + deleted: []mockRelease{}, + }, + { + name: "not installed, and filtered (purge=false)", + wantErr: false, + desired: boolValue(false), + installed: false, + purge: false, + deleted: []mockRelease{}, + }, + { + name: "not installed, and filtered (purge=true)", + wantErr: false, + desired: boolValue(false), + installed: false, + purge: true, + deleted: []mockRelease{}, + }, + } + for _, tt := range tests { + i := func(t *testing.T) { + release := ReleaseSpec{ + Name: "releaseA", + Installed: tt.desired, + } + releases := []ReleaseSpec{ + release, + } + state := &HelmState{ + Releases: releases, + logger: logger, + } + helm := &mockHelmExec{ + lists: map[string]string{}, + deleted: []mockRelease{}, + } + if tt.installed { + helm.lists["^releaseA$"] = "releaseA" + } + errs := state.DeleteReleases(helm, tt.purge) + if (errs != nil) != tt.wantErr { + t.Errorf("DeleteREleases() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr) + return + } + if !reflect.DeepEqual(tt.deleted, helm.deleted) { + t.Errorf("unexpected deletions happened: expected %v, got %v", tt.deleted, helm.deleted) + } + } + t.Run(tt.name, i) + } +}