Repository: helm/helm (go)

Issue:
Add shell completion for --version flag
This proposal is to add shell completion for the `--version` flag.  As an example
```
helm show all stable/fluentd --version <TAB>
helm pull stable/fluentd --version <TAB>
helm install myfluentd stable/fluentd --version <TAB>
helm upgrade myfluentd stable/fluentd --version <TAB>
```
would suggest
```
1.0.0   1.10.0  1.2.0   1.4.0   1.5.1   1.7.0   1.9.1   2.0.1   2.0.3   2.2.0   2.3.0   2.3.2   2.4.0
1.1.0   1.10.1  1.3.0   1.5.0   1.6.0   1.9.0   2.0.0   2.0.2   2.1.3   2.2.1   2.3.1   2.3.3   2.4.1
```
This improvement is not as much about saving users some typing as it is about helping users figure out which versions are available.  

Pull request description:
Provide completion for --version flag
**What this PR does / why we need it**:

Fixes #8300 

This commit allows to use shell completion to obtain the list of available versions of a chart referenced in a 'repo/chart' format. It applies to:
```
$ helm install
$ helm template
$ helm upgrade
$ helm show
$ helm pull
```
As an example
```
helm pull stable/fluentd --version <TAB>
```
would suggest
```
1.0.0   1.10.0  1.2.0   1.4.0   1.5.1   1.7.0   1.9.1   2.0.1   2.0.3   2.2.0   2.3.0   2.3.2   2.4.0
1.1.0   1.10.1  1.3.0   1.5.0   1.6.0   1.9.0   2.0.0   2.0.2   2.1.3   2.2.1   2.3.1   2.3.3   2.4.1
```
This improvement is not as much about saving users some typing as it is about helping users figure out which versions are available.  

**Special notes for your reviewer**:
The completion can be slow for the 'stable' repo because its index file takes some time to parse.

**If applicable**:
- [x] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility using the completion tests of the acceptance-testing repo


Patch:
diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go
index d4cd88fef..040773d7b 100644
--- a/cmd/helm/flags.go
+++ b/cmd/helm/flags.go
@@ -19,6 +19,7 @@ package main
 import (
 	"fmt"
 	"log"
+	"path/filepath"
 	"strings"
 
 	"github.com/spf13/cobra"
@@ -27,7 +28,9 @@ import (
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/cli/output"
 	"helm.sh/helm/v3/pkg/cli/values"
+	"helm.sh/helm/v3/pkg/helmpath"
 	"helm.sh/helm/v3/pkg/postrender"
+	"helm.sh/helm/v3/pkg/repo"
 )
 
 const outputFlag = "output"
@@ -127,3 +130,27 @@ func (p postRenderer) Set(s string) error {
 	*p.renderer = pr
 	return nil
 }
+
+func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellCompDirective) {
+	chartInfo := strings.Split(chartRef, "/")
+	if len(chartInfo) != 2 {
+		return nil, cobra.ShellCompDirectiveNoFileComp
+	}
+
+	repoName := chartInfo[0]
+	chartName := chartInfo[1]
+
+	path := filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName))
+
+	var versions []string
+	if indexFile, err := repo.LoadIndexFile(path); err == nil {
+		for _, details := range indexFile.Entries[chartName] {
+			version := details.Metadata.Version
+			if strings.HasPrefix(version, toComplete) {
+				versions = append(versions, version)
+			}
+		}
+	}
+
+	return versions, cobra.ShellCompDirectiveNoFileComp
+}
diff --git a/cmd/helm/install.go b/cmd/helm/install.go
index b489c262d..9024add2f 100644
--- a/cmd/helm/install.go
+++ b/cmd/helm/install.go
@@ -19,6 +19,7 @@ package main
 import (
 	"fmt"
 	"io"
+	"log"
 	"time"
 
 	"github.com/pkg/errors"
@@ -126,14 +127,14 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 		},
 	}
 
-	addInstallFlags(cmd.Flags(), client, valueOpts)
+	addInstallFlags(cmd, cmd.Flags(), client, valueOpts)
 	bindOutputFlag(cmd, &outfmt)
 	bindPostRenderFlag(cmd, &client.PostRenderer)
 
 	return cmd
 }
 
-func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
+func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
 	f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
 	f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install")
 	f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
@@ -151,6 +152,21 @@ func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values
 	f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent")
 	addValueOptionsFlags(f, valueOpts)
 	addChartPathOptionsFlags(f, &client.ChartPathOptions)
+
+	err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		requiredArgs := 2
+		if client.GenerateName {
+			requiredArgs = 1
+		}
+		if len(args) != requiredArgs {
+			return nil, cobra.ShellCompDirectiveNoFileComp
+		}
+		return compVersionFlag(args[requiredArgs-1], toComplete)
+	})
+
+	if err != nil {
+		log.Fatal(err)
+	}
 }
 
 func runInstall(args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) {
diff --git a/cmd/helm/pull.go b/cmd/helm/pull.go
index d10c629db..3f62bf0c7 100644
--- a/cmd/helm/pull.go
+++ b/cmd/helm/pull.go
@@ -19,6 +19,7 @@ package main
 import (
 	"fmt"
 	"io"
+	"log"
 
 	"github.com/spf13/cobra"
 
@@ -82,5 +83,16 @@ func newPullCmd(out io.Writer) *cobra.Command {
 	f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and tardir are specified, tardir is appended to this")
 	addChartPathOptionsFlags(f, &client.ChartPathOptions)
 
+	err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		if len(args) != 1 {
+			return nil, cobra.ShellCompDirectiveNoFileComp
+		}
+		return compVersionFlag(args[0], toComplete)
+	})
+
+	if err != nil {
+		log.Fatal(err)
+	}
+
 	return cmd
 }
diff --git a/cmd/helm/show.go b/cmd/helm/show.go
index c78069a09..c122d8476 100644
--- a/cmd/helm/show.go
+++ b/cmd/helm/show.go
@@ -19,6 +19,7 @@ package main
 import (
 	"fmt"
 	"io"
+	"log"
 
 	"github.com/spf13/cobra"
 
@@ -151,6 +152,17 @@ func addShowFlags(subCmd *cobra.Command, client *action.Show) {
 
 	f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
 	addChartPathOptionsFlags(f, &client.ChartPathOptions)
+
+	err := subCmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		if len(args) != 1 {
+			return nil, cobra.ShellCompDirectiveNoFileComp
+		}
+		return compVersionFlag(args[0], toComplete)
+	})
+
+	if err != nil {
+		log.Fatal(err)
+	}
 }
 
 func runShow(args []string, client *action.Show) (string, error) {
diff --git a/cmd/helm/template.go b/cmd/helm/template.go
index 0fc29e189..83ad61f22 100644
--- a/cmd/helm/template.go
+++ b/cmd/helm/template.go
@@ -139,7 +139,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	}
 
 	f := cmd.Flags()
-	addInstallFlags(f, client, valueOpts)
+	addInstallFlags(cmd, f, client, valueOpts)
 	f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")
 	f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
 	f.BoolVar(&validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install")
diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go
index 1db899eae..dc3bc9a58 100644
--- a/cmd/helm/upgrade.go
+++ b/cmd/helm/upgrade.go
@@ -19,6 +19,7 @@ package main
 import (
 	"fmt"
 	"io"
+	"log"
 	"time"
 
 	"github.com/pkg/errors"
@@ -188,5 +189,16 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	bindOutputFlag(cmd, &outfmt)
 	bindPostRenderFlag(cmd, &client.PostRenderer)
 
+	err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		if len(args) != 2 {
+			return nil, cobra.ShellCompDirectiveNoFileComp
+		}
+		return compVersionFlag(args[1], toComplete)
+	})
+
+	if err != nil {
+		log.Fatal(err)
+	}
+
 	return cmd
 }


Test patch:
diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go
index 7a101940f..36de648f9 100644
--- a/cmd/helm/install_test.go
+++ b/cmd/helm/install_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package main
 
 import (
+	"fmt"
 	"testing"
 )
 
@@ -208,3 +209,33 @@ func TestInstall(t *testing.T) {
 func TestInstallOutputCompletion(t *testing.T) {
 	outputFlagCompletionTest(t, "install")
 }
+
+func TestInstallVersionCompletion(t *testing.T) {
+	repoFile := "testdata/helmhome/helm/repositories.yaml"
+	repoCache := "testdata/helmhome/helm/repository"
+
+	repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache)
+
+	tests := []cmdTestCase{{
+		name:   "completion for install version flag with release name",
+		cmd:    fmt.Sprintf("%s __complete install releasename testing/alpine --version ''", repoSetup),
+		golden: "output/version-comp.txt",
+	}, {
+		name:   "completion for install version flag with generate-name",
+		cmd:    fmt.Sprintf("%s __complete install --generate-name testing/alpine --version ''", repoSetup),
+		golden: "output/version-comp.txt",
+	}, {
+		name:   "completion for install version flag too few args",
+		cmd:    fmt.Sprintf("%s __complete install testing/alpine --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for install version flag too many args",
+		cmd:    fmt.Sprintf("%s __complete install releasename testing/alpine badarg --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for install version flag invalid chart",
+		cmd:    fmt.Sprintf("%s __complete install releasename invalid/invalid --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}}
+	runTestCmd(t, tests)
+}
diff --git a/cmd/helm/pull_test.go b/cmd/helm/pull_test.go
index d4661f928..435df51f1 100644
--- a/cmd/helm/pull_test.go
+++ b/cmd/helm/pull_test.go
@@ -195,3 +195,29 @@ func TestPullCmd(t *testing.T) {
 		})
 	}
 }
+
+func TestPullVersionCompletion(t *testing.T) {
+	repoFile := "testdata/helmhome/helm/repositories.yaml"
+	repoCache := "testdata/helmhome/helm/repository"
+
+	repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache)
+
+	tests := []cmdTestCase{{
+		name:   "completion for pull version flag",
+		cmd:    fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup),
+		golden: "output/version-comp.txt",
+	}, {
+		name:   "completion for pull version flag too few args",
+		cmd:    fmt.Sprintf("%s __complete pull --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for pull version flag too many args",
+		cmd:    fmt.Sprintf("%s __complete pull testing/alpine badarg --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for pull version flag invalid chart",
+		cmd:    fmt.Sprintf("%s __complete pull invalid/invalid --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}}
+	runTestCmd(t, tests)
+}
diff --git a/cmd/helm/show_test.go b/cmd/helm/show_test.go
index 00d7c8145..6c550282f 100644
--- a/cmd/helm/show_test.go
+++ b/cmd/helm/show_test.go
@@ -80,3 +80,41 @@ func TestShowPreReleaseChart(t *testing.T) {
 		})
 	}
 }
+
+func TestShowVersionCompletion(t *testing.T) {
+	repoFile := "testdata/helmhome/helm/repositories.yaml"
+	repoCache := "testdata/helmhome/helm/repository"
+
+	repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache)
+
+	tests := []cmdTestCase{{
+		name:   "completion for show version flag",
+		cmd:    fmt.Sprintf("%s __complete show chart testing/alpine --version ''", repoSetup),
+		golden: "output/version-comp.txt",
+	}, {
+		name:   "completion for show version flag too few args",
+		cmd:    fmt.Sprintf("%s __complete show chart --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for show version flag too many args",
+		cmd:    fmt.Sprintf("%s __complete show chart testing/alpine badarg --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for show version flag invalid chart",
+		cmd:    fmt.Sprintf("%s __complete show chart invalid/invalid --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for show version flag with all",
+		cmd:    fmt.Sprintf("%s __complete show all testing/alpine --version ''", repoSetup),
+		golden: "output/version-comp.txt",
+	}, {
+		name:   "completion for show version flag with readme",
+		cmd:    fmt.Sprintf("%s __complete show readme testing/alpine --version ''", repoSetup),
+		golden: "output/version-comp.txt",
+	}, {
+		name:   "completion for show version flag with values",
+		cmd:    fmt.Sprintf("%s __complete show values testing/alpine --version ''", repoSetup),
+		golden: "output/version-comp.txt",
+	}}
+	runTestCmd(t, tests)
+}
diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go
index 92dd9825e..f8cd31347 100644
--- a/cmd/helm/template_test.go
+++ b/cmd/helm/template_test.go
@@ -124,3 +124,33 @@ func TestTemplateCmd(t *testing.T) {
 	}
 	runTestCmd(t, tests)
 }
+
+func TestTemplateVersionCompletion(t *testing.T) {
+	repoFile := "testdata/helmhome/helm/repositories.yaml"
+	repoCache := "testdata/helmhome/helm/repository"
+
+	repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache)
+
+	tests := []cmdTestCase{{
+		name:   "completion for template version flag with release name",
+		cmd:    fmt.Sprintf("%s __complete template releasename testing/alpine --version ''", repoSetup),
+		golden: "output/version-comp.txt",
+	}, {
+		name:   "completion for template version flag with generate-name",
+		cmd:    fmt.Sprintf("%s __complete template --generate-name testing/alpine --version ''", repoSetup),
+		golden: "output/version-comp.txt",
+	}, {
+		name:   "completion for template version flag too few args",
+		cmd:    fmt.Sprintf("%s __complete template testing/alpine --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for template version flag too many args",
+		cmd:    fmt.Sprintf("%s __complete template releasename testing/alpine badarg --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for template version flag invalid chart",
+		cmd:    fmt.Sprintf("%s __complete template releasename invalid/invalid --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}}
+	runTestCmd(t, tests)
+}
diff --git a/cmd/helm/testdata/output/version-comp.txt b/cmd/helm/testdata/output/version-comp.txt
new file mode 100644
index 000000000..098e2cec2
--- /dev/null
+++ b/cmd/helm/testdata/output/version-comp.txt
@@ -0,0 +1,5 @@
+0.3.0-rc.1
+0.2.0
+0.1.0
+:4
+Completion ended with directive: ShellCompDirectiveNoFileComp
diff --git a/cmd/helm/testdata/output/version-invalid-comp.txt b/cmd/helm/testdata/output/version-invalid-comp.txt
new file mode 100644
index 000000000..8d9fad576
--- /dev/null
+++ b/cmd/helm/testdata/output/version-invalid-comp.txt
@@ -0,0 +1,2 @@
+:4
+Completion ended with directive: ShellCompDirectiveNoFileComp
diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go
index 6f260ae57..7e88bc0df 100644
--- a/cmd/helm/upgrade_test.go
+++ b/cmd/helm/upgrade_test.go
@@ -381,3 +381,29 @@ func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int,
 func TestUpgradeOutputCompletion(t *testing.T) {
 	outputFlagCompletionTest(t, "upgrade")
 }
+
+func TestUpgradeVersionCompletion(t *testing.T) {
+	repoFile := "testdata/helmhome/helm/repositories.yaml"
+	repoCache := "testdata/helmhome/helm/repository"
+
+	repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache)
+
+	tests := []cmdTestCase{{
+		name:   "completion for upgrade version flag",
+		cmd:    fmt.Sprintf("%s __complete upgrade releasename testing/alpine --version ''", repoSetup),
+		golden: "output/version-comp.txt",
+	}, {
+		name:   "completion for upgrade version flag too few args",
+		cmd:    fmt.Sprintf("%s __complete upgrade releasename --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for upgrade version flag too many args",
+		cmd:    fmt.Sprintf("%s __complete upgrade releasename testing/alpine badarg --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}, {
+		name:   "completion for upgrade version flag invalid chart",
+		cmd:    fmt.Sprintf("%s __complete upgrade releasename invalid/invalid --version ''", repoSetup),
+		golden: "output/version-invalid-comp.txt",
+	}}
+	runTestCmd(t, tests)
+}