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:
		
							parent
							
								
									fa95e0dd92
								
							
						
					
					
						commit
						8f1a15c9cd
					
				
							
								
								
									
										20
									
								
								README.md
								
								
								
								
							
							
						
						
									
										20
									
								
								README.md
								
								
								
								
							|  | @ -212,16 +212,20 @@ NAME: | ||||||
| USAGE: | USAGE: | ||||||
|    helmfile [global options] command [command options] [arguments...] |    helmfile [global options] command [command options] [arguments...] | ||||||
| 
 | 
 | ||||||
|  | VERSION: | ||||||
|  |    v0.52.0 | ||||||
|  | 
 | ||||||
| COMMANDS: | COMMANDS: | ||||||
|      repos     sync repositories from state file (helm repo add && helm repo update) |      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) |      diff      diff releases from state file against env (helm diff) | ||||||
|      template  template releases from state file against env (helm template) |      template  template releases from state file against env (helm template) | ||||||
|      lint      lint charts from state file (helm lint) |      lint      lint charts from state file (helm lint) | ||||||
|      sync      sync all resources from state file (repos, releases and chart deps) |      sync      sync all resources from state file (repos, releases and chart deps) | ||||||
|      apply     apply all resources from state file only when there are changes |      apply     apply all resources from state file only when there are changes | ||||||
|      status    retrieve status of releases in state file |      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) |      test      test releases from state file (helm test) | ||||||
| 
 | 
 | ||||||
| GLOBAL OPTIONS: | 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. | 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. | 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 | ## 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. | Use it when you're running `helmfile` manually on your local machine or a kind of secure administrative hosts. | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										41
									
								
								main.go
								
								
								
								
							
							
						
						
									
										41
									
								
								main.go
								
								
								
								
							|  | @ -117,7 +117,7 @@ func main() { | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "charts", | 			Name:  "charts", | ||||||
| 			Usage: "sync releases from state file (helm upgrade --install)", | 			Usage: "DEPRECATED: sync releases from state file (helm upgrade --install)", | ||||||
| 			Flags: []cli.Flag{ | 			Flags: []cli.Flag{ | ||||||
| 				cli.StringFlag{ | 				cli.StringFlag{ | ||||||
| 					Name:  "args", | 					Name:  "args", | ||||||
|  | @ -435,7 +435,7 @@ Do you really want to apply? | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:  "delete", | 			Name:  "delete", | ||||||
| 			Usage: "delete releases from state file (helm delete)", | 			Usage: "DEPRECATED: delete releases from state file (helm delete)", | ||||||
| 			Flags: []cli.Flag{ | 			Flags: []cli.Flag{ | ||||||
| 				cli.StringFlag{ | 				cli.StringFlag{ | ||||||
| 					Name:  "args", | 					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", | 			Name:  "test", | ||||||
| 			Usage: "test releases from state file (helm test)", | 			Usage: "test releases from state file (helm test)", | ||||||
|  |  | ||||||
|  | @ -742,6 +742,10 @@ func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) [ | ||||||
| // DeleteReleases wrapper for executing helm delete on the releases
 | // DeleteReleases wrapper for executing helm delete on the releases
 | ||||||
| func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error { | func (st *HelmState) DeleteReleases(helm helmexec.Interface, purge bool) []error { | ||||||
| 	return st.scatterGatherReleases(helm, len(st.Releases), func(release ReleaseSpec) error { | 	return st.scatterGatherReleases(helm, len(st.Releases), func(release ReleaseSpec) error { | ||||||
|  | 		if !release.Desired() { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		flags := []string{} | 		flags := []string{} | ||||||
| 		if purge { | 		if purge { | ||||||
| 			flags = append(flags, "--purge") | 			flags = append(flags, "--purge") | ||||||
|  |  | ||||||
|  | @ -635,6 +635,8 @@ type mockHelmExec struct { | ||||||
| 	charts   []string | 	charts   []string | ||||||
| 	repo     []string | 	repo     []string | ||||||
| 	releases []mockRelease | 	releases []mockRelease | ||||||
|  | 	deleted  []mockRelease | ||||||
|  | 	lists    map[string]string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type mockRelease struct { | type mockRelease struct { | ||||||
|  | @ -690,10 +692,11 @@ func (helm *mockHelmExec) ReleaseStatus(release string) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| func (helm *mockHelmExec) DeleteRelease(name string, flags ...string) error { | func (helm *mockHelmExec) DeleteRelease(name string, flags ...string) error { | ||||||
|  | 	helm.deleted = append(helm.deleted, mockRelease{name: name, flags: flags}) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| func (helm *mockHelmExec) List(filter string) (string, error) { | func (helm *mockHelmExec) List(filter string) (string, error) { | ||||||
| 	return "", nil | 	return helm.lists[filter], nil | ||||||
| } | } | ||||||
| func (helm *mockHelmExec) DecryptSecret(name string) (string, error) { | func (helm *mockHelmExec) DecryptSecret(name string) (string, error) { | ||||||
| 	return "", nil | 	return "", nil | ||||||
|  | @ -1291,3 +1294,118 @@ func TestHelmState_NoReleaseMatched(t *testing.T) { | ||||||
| 		t.Run(tt.name, i) | 		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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue