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`
This commit is contained in:
KUOKA Yusuke 2019-04-02 21:17:38 +09:00 committed by GitHub
parent fa95e0dd92
commit 8f1a15c9cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 178 additions and 7 deletions

View File

@ -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.

41
main.go
View File

@ -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)",

View File

@ -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")

View File

@ -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)
}
}