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: | ||||
|    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
								
								
								
								
							
							
						
						
									
										41
									
								
								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)", | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue