Repository: helm/helm
Pull request #9481 (2021-03-13 22:25:15)

feat(comp): Uninstall accepts multiple releases
**What this PR does / why we need it**:

The `uninstall` command can accept more than one release name as arguments; this commit teaches the completion logic to respect that.

Tests were also added for completion of commands that were using the code path that was changed.

**Special notes for your reviewer**:

The logic to not suggest a plugin that is already on the command-line is the same as the logic for completion of the `helm repo rm` command which also takes multiple arguments. See https://github.com/helm/helm/blob/64c8df187af892c2a9f872f91e892a717df2e7ff/cmd/helm/repo_list.go#L103

**If applicable**:
- [x] this PR contains unit tests



Patch:
diff --git a/cmd/helm/get_all.go b/cmd/helm/get_all.go
index 53f8d5905..bf367da7f 100644
--- a/cmd/helm/get_all.go
+++ b/cmd/helm/get_all.go
@@ -45,7 +45,7 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			if len(args) != 0 {
 				return nil, cobra.ShellCompDirectiveNoFileComp
 			}
-			return compListReleases(toComplete, cfg)
+			return compListReleases(toComplete, args, cfg)
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			res, err := client.Run(args[0])
diff --git a/cmd/helm/get_hooks.go b/cmd/helm/get_hooks.go
index 8b78653b5..913e2c58a 100644
--- a/cmd/helm/get_hooks.go
+++ b/cmd/helm/get_hooks.go
@@ -45,7 +45,7 @@ func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			if len(args) != 0 {
 				return nil, cobra.ShellCompDirectiveNoFileComp
 			}
-			return compListReleases(toComplete, cfg)
+			return compListReleases(toComplete, args, cfg)
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			res, err := client.Run(args[0])
diff --git a/cmd/helm/get_manifest.go b/cmd/helm/get_manifest.go
index 8ffeb3676..baeaf8d72 100644
--- a/cmd/helm/get_manifest.go
+++ b/cmd/helm/get_manifest.go
@@ -47,7 +47,7 @@ func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
 			if len(args) != 0 {
 				return nil, cobra.ShellCompDirectiveNoFileComp
 			}
-			return compListReleases(toComplete, cfg)
+			return compListReleases(toComplete, args, cfg)
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			res, err := client.Run(args[0])
diff --git a/cmd/helm/get_notes.go b/cmd/helm/get_notes.go
index a9d29ce49..b71bcbdf6 100644
--- a/cmd/helm/get_notes.go
+++ b/cmd/helm/get_notes.go
@@ -43,7 +43,7 @@ func newGetNotesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			if len(args) != 0 {
 				return nil, cobra.ShellCompDirectiveNoFileComp
 			}
-			return compListReleases(toComplete, cfg)
+			return compListReleases(toComplete, args, cfg)
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			res, err := client.Run(args[0])
diff --git a/cmd/helm/get_values.go b/cmd/helm/get_values.go
index c8c87c033..6124e1b33 100644
--- a/cmd/helm/get_values.go
+++ b/cmd/helm/get_values.go
@@ -50,7 +50,7 @@ func newGetValuesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			if len(args) != 0 {
 				return nil, cobra.ShellCompDirectiveNoFileComp
 			}
-			return compListReleases(toComplete, cfg)
+			return compListReleases(toComplete, args, cfg)
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			vals, err := client.Run(args[0])
diff --git a/cmd/helm/history.go b/cmd/helm/history.go
index 61f45c946..06ec07d6d 100644
--- a/cmd/helm/history.go
+++ b/cmd/helm/history.go
@@ -65,7 +65,7 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			if len(args) != 0 {
 				return nil, cobra.ShellCompDirectiveNoFileComp
 			}
-			return compListReleases(toComplete, cfg)
+			return compListReleases(toComplete, args, cfg)
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			history, err := getHistory(client, args[0])
diff --git a/cmd/helm/list.go b/cmd/helm/list.go
index 05e56fcab..f8be65b17 100644
--- a/cmd/helm/list.go
+++ b/cmd/helm/list.go
@@ -193,8 +193,32 @@ func (r *releaseListWriter) WriteYAML(out io.Writer) error {
 	return output.EncodeYAML(out, r.releases)
 }
 
+// Returns all releases from 'releases', except those with names matching 'ignoredReleases'
+func filterReleases(releases []*release.Release, ignoredReleaseNames []string) []*release.Release {
+	// if ignoredReleaseNames is nil, just return releases
+	if ignoredReleaseNames == nil {
+		return releases
+	}
+
+	var filteredReleases []*release.Release
+	for _, rel := range releases {
+		found := false
+		for _, ignoredName := range ignoredReleaseNames {
+			if rel.Name == ignoredName {
+				found = true
+				break
+			}
+		}
+		if !found {
+			filteredReleases = append(filteredReleases, rel)
+		}
+	}
+
+	return filteredReleases
+}
+
 // Provide dynamic auto-completion for release names
-func compListReleases(toComplete string, cfg *action.Configuration) ([]string, cobra.ShellCompDirective) {
+func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *action.Configuration) ([]string, cobra.ShellCompDirective) {
 	cobra.CompDebugln(fmt.Sprintf("compListReleases with toComplete %s", toComplete), settings.Debug)
 
 	client := action.NewList(cfg)
@@ -209,7 +233,8 @@ func compListReleases(toComplete string, cfg *action.Configuration) ([]string, c
 	}
 
 	var choices []string
-	for _, rel := range releases {
+	filteredReleases := filterReleases(releases, ignoredReleaseNames)
+	for _, rel := range filteredReleases {
 		choices = append(choices,
 			fmt.Sprintf("%s\t%s-%s -> %s", rel.Name, rel.Chart.Metadata.Name, rel.Chart.Metadata.Version, rel.Info.Status.String()))
 	}
diff --git a/cmd/helm/rollback.go b/cmd/helm/rollback.go
index 9699b9c05..ea4b75cb1 100644
--- a/cmd/helm/rollback.go
+++ b/cmd/helm/rollback.go
@@ -48,7 +48,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 		Args:  require.MinimumNArgs(1),
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			if len(args) == 0 {
-				return compListReleases(toComplete, cfg)
+				return compListReleases(toComplete, args, cfg)
 			}
 
 			if len(args) == 1 {
diff --git a/cmd/helm/status.go b/cmd/helm/status.go
index 7a3204cb9..6085251d5 100644
--- a/cmd/helm/status.go
+++ b/cmd/helm/status.go
@@ -59,7 +59,7 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 			if len(args) != 0 {
 				return nil, cobra.ShellCompDirectiveNoFileComp
 			}
-			return compListReleases(toComplete, cfg)
+			return compListReleases(toComplete, args, cfg)
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			rel, err := client.Run(args[0])
diff --git a/cmd/helm/uninstall.go b/cmd/helm/uninstall.go
index 509918e53..f4f5d87e8 100644
--- a/cmd/helm/uninstall.go
+++ b/cmd/helm/uninstall.go
@@ -48,10 +48,7 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 		Long:       uninstallDesc,
 		Args:       require.MinimumNArgs(1),
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-			if len(args) != 0 {
-				return nil, cobra.ShellCompDirectiveNoFileComp
-			}
-			return compListReleases(toComplete, cfg)
+			return compListReleases(toComplete, args, cfg)
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			for i := 0; i < len(args); i++ {
diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go
index c2e92fb36..1952b8421 100644
--- a/cmd/helm/upgrade.go
+++ b/cmd/helm/upgrade.go
@@ -74,7 +74,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 		Args:  require.ExactArgs(2),
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			if len(args) == 0 {
-				return compListReleases(toComplete, cfg)
+				return compListReleases(toComplete, args, cfg)
 			}
 			if len(args) == 1 {
 				return compListCharts(toComplete, true)


Test patch:
diff --git a/cmd/helm/completion_test.go b/cmd/helm/completion_test.go
index bd94f6b4c..6d53616d0 100644
--- a/cmd/helm/completion_test.go
+++ b/cmd/helm/completion_test.go
@@ -62,3 +62,32 @@ func TestCompletionFileCompletion(t *testing.T) {
 	checkFileCompletion(t, "completion zsh", false)
 	checkFileCompletion(t, "completion fish", false)
 }
+
+func checkReleaseCompletion(t *testing.T, cmdName string, multiReleasesAllowed bool) {
+	multiReleaseTestGolden := "output/empty_nofile_comp.txt"
+	if multiReleasesAllowed {
+		multiReleaseTestGolden = "output/release_list_repeat_comp.txt"
+	}
+	tests := []cmdTestCase{{
+		name:   "completion for uninstall",
+		cmd:    fmt.Sprintf("__complete %s ''", cmdName),
+		golden: "output/release_list_comp.txt",
+		rels: []*release.Release{
+			release.Mock(&release.MockReleaseOptions{Name: "athos"}),
+			release.Mock(&release.MockReleaseOptions{Name: "porthos"}),
+			release.Mock(&release.MockReleaseOptions{Name: "aramis"}),
+		},
+	}, {
+		name:   "completion for uninstall repetition",
+		cmd:    fmt.Sprintf("__complete %s porthos ''", cmdName),
+		golden: multiReleaseTestGolden,
+		rels: []*release.Release{
+			release.Mock(&release.MockReleaseOptions{Name: "athos"}),
+			release.Mock(&release.MockReleaseOptions{Name: "porthos"}),
+			release.Mock(&release.MockReleaseOptions{Name: "aramis"}),
+		},
+	}}
+	for _, test := range tests {
+		runTestCmd(t, []cmdTestCase{test})
+	}
+}
diff --git a/cmd/helm/get_all_test.go b/cmd/helm/get_all_test.go
index 0c140faf8..948f0aa71 100644
--- a/cmd/helm/get_all_test.go
+++ b/cmd/helm/get_all_test.go
@@ -42,6 +42,10 @@ func TestGetCmd(t *testing.T) {
 	runTestCmd(t, tests)
 }
 
+func TestGetAllCompletion(t *testing.T) {
+	checkReleaseCompletion(t, "get all", false)
+}
+
 func TestGetAllRevisionCompletion(t *testing.T) {
 	revisionFlagCompletionTest(t, "get all")
 }
diff --git a/cmd/helm/get_hooks_test.go b/cmd/helm/get_hooks_test.go
index 6c010d1bd..251d5c731 100644
--- a/cmd/helm/get_hooks_test.go
+++ b/cmd/helm/get_hooks_test.go
@@ -37,6 +37,10 @@ func TestGetHooks(t *testing.T) {
 	runTestCmd(t, tests)
 }
 
+func TestGetHooksCompletion(t *testing.T) {
+	checkReleaseCompletion(t, "get hooks", false)
+}
+
 func TestGetHooksRevisionCompletion(t *testing.T) {
 	revisionFlagCompletionTest(t, "get hooks")
 }
diff --git a/cmd/helm/get_manifest_test.go b/cmd/helm/get_manifest_test.go
index f3f572e80..2f27476b6 100644
--- a/cmd/helm/get_manifest_test.go
+++ b/cmd/helm/get_manifest_test.go
@@ -37,6 +37,10 @@ func TestGetManifest(t *testing.T) {
 	runTestCmd(t, tests)
 }
 
+func TestGetManifestCompletion(t *testing.T) {
+	checkReleaseCompletion(t, "get manifest", false)
+}
+
 func TestGetManifestRevisionCompletion(t *testing.T) {
 	revisionFlagCompletionTest(t, "get manifest")
 }
diff --git a/cmd/helm/get_notes_test.go b/cmd/helm/get_notes_test.go
index 7d43c87e7..8be9a3f7c 100644
--- a/cmd/helm/get_notes_test.go
+++ b/cmd/helm/get_notes_test.go
@@ -37,6 +37,10 @@ func TestGetNotesCmd(t *testing.T) {
 	runTestCmd(t, tests)
 }
 
+func TestGetNotesCompletion(t *testing.T) {
+	checkReleaseCompletion(t, "get notes", false)
+}
+
 func TestGetNotesRevisionCompletion(t *testing.T) {
 	revisionFlagCompletionTest(t, "get notes")
 }
diff --git a/cmd/helm/get_values_test.go b/cmd/helm/get_values_test.go
index 2a71b1e4d..423c32859 100644
--- a/cmd/helm/get_values_test.go
+++ b/cmd/helm/get_values_test.go
@@ -53,6 +53,10 @@ func TestGetValuesCmd(t *testing.T) {
 	runTestCmd(t, tests)
 }
 
+func TestGetValuesCompletion(t *testing.T) {
+	checkReleaseCompletion(t, "get values", false)
+}
+
 func TestGetValuesRevisionCompletion(t *testing.T) {
 	revisionFlagCompletionTest(t, "get values")
 }
diff --git a/cmd/helm/history_test.go b/cmd/helm/history_test.go
index fffd983da..2663e9ee9 100644
--- a/cmd/helm/history_test.go
+++ b/cmd/helm/history_test.go
@@ -109,6 +109,10 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) {
 	runTestCmd(t, tests)
 }
 
+func TestHistoryCompletion(t *testing.T) {
+	checkReleaseCompletion(t, "history", false)
+}
+
 func TestHistoryFileCompletion(t *testing.T) {
 	checkFileCompletion(t, "history", false)
 	checkFileCompletion(t, "history myrelease", false)
diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go
index fbf0dd112..2637cbb9f 100644
--- a/cmd/helm/release_testing.go
+++ b/cmd/helm/release_testing.go
@@ -52,7 +52,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
 			if len(args) != 0 {
 				return nil, cobra.ShellCompDirectiveNoFileComp
 			}
-			return compListReleases(toComplete, cfg)
+			return compListReleases(toComplete, args, cfg)
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			client.Namespace = settings.Namespace()
diff --git a/cmd/helm/release_testing_test.go b/cmd/helm/release_testing_test.go
index 257e95721..680a9bd3e 100644
--- a/cmd/helm/release_testing_test.go
+++ b/cmd/helm/release_testing_test.go
@@ -20,6 +20,10 @@ import (
 	"testing"
 )
 
+func TestReleaseTestingCompletion(t *testing.T) {
+	checkReleaseCompletion(t, "test", false)
+}
+
 func TestReleaseTestingFileCompletion(t *testing.T) {
 	checkFileCompletion(t, "test", false)
 	checkFileCompletion(t, "test myrelease", false)
diff --git a/cmd/helm/testdata/output/release_list_comp.txt b/cmd/helm/testdata/output/release_list_comp.txt
new file mode 100644
index 000000000..226c378a9
--- /dev/null
+++ b/cmd/helm/testdata/output/release_list_comp.txt
@@ -0,0 +1,5 @@
+aramis	foo-0.1.0-beta.1 -> deployed
+athos	foo-0.1.0-beta.1 -> deployed
+porthos	foo-0.1.0-beta.1 -> deployed
+:4
+Completion ended with directive: ShellCompDirectiveNoFileComp
diff --git a/cmd/helm/testdata/output/release_list_repeat_comp.txt b/cmd/helm/testdata/output/release_list_repeat_comp.txt
new file mode 100644
index 000000000..aa330f47f
--- /dev/null
+++ b/cmd/helm/testdata/output/release_list_repeat_comp.txt
@@ -0,0 +1,4 @@
+aramis	foo-0.1.0-beta.1 -> deployed
+athos	foo-0.1.0-beta.1 -> deployed
+:4
+Completion ended with directive: ShellCompDirectiveNoFileComp
diff --git a/cmd/helm/uninstall_test.go b/cmd/helm/uninstall_test.go
index ad78361c1..1a33458c4 100644
--- a/cmd/helm/uninstall_test.go
+++ b/cmd/helm/uninstall_test.go
@@ -67,6 +67,10 @@ func TestUninstall(t *testing.T) {
 	runTestCmd(t, tests)
 }
 
+func TestUninstallCompletion(t *testing.T) {
+	checkReleaseCompletion(t, "uninstall", true)
+}
+
 func TestUninstallFileCompletion(t *testing.T) {
 	checkFileCompletion(t, "uninstall", false)
 	checkFileCompletion(t, "uninstall myrelease", false)