diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 112d5123f..0ce40732b 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -30,6 +30,7 @@ import ( // Import to initialize client auth plugins. _ "k8s.io/client-go/plugin/pkg/client/auth" + "helm.sh/helm/v3/internal/completion" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/gates" @@ -67,6 +68,16 @@ func main() { actionConfig := new(action.Configuration) cmd := newRootCmd(actionConfig, os.Stdout, os.Args[1:]) + if calledCmd, _, err := cmd.Find(os.Args[1:]); err == nil && calledCmd.Name() == completion.CompRequestCmd { + // If completion is being called, we have to check if the completion is for the "--kube-context" + // value; if it is, we cannot call the action.Init() method with an incomplete kube-context value + // or else it will fail immediately. So, we simply unset the invalid kube-context value. + if args := os.Args[1:]; len(args) > 2 && args[len(args)-2] == "--kube-context" { + // We are completing the kube-context value! Reset it as the current value is not valid. + settings.KubeContext = "" + } + } + if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), debug); err != nil { log.Fatal(err) } diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 6ce1dcbf4..3c6a0d18e 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" "helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/internal/completion" @@ -31,30 +32,6 @@ import ( "helm.sh/helm/v3/pkg/action" ) -const ( - contextCompFunc = ` -__helm_get_contexts() -{ - __helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" - local template out - template="{{ range .contexts }}{{ .name }} {{ end }}" - if out=$(kubectl config -o template --template="${template}" view 2>/dev/null); then - COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) ) - fi -} -` -) - -var ( - // Mapping of global flags that can have dynamic completion and the - // completion function to be used. - bashCompletionFlags = map[string]string{ - // Cannot convert the kube-context flag to Go completion yet because - // an incomplete kube-context will make actionConfig.Init() fail at the very start - "kube-context": "__helm_get_contexts", - } -) - var globalUsage = `The Kubernetes package manager Common actions for Helm: @@ -101,14 +78,14 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string Long: globalUsage, SilenceUsage: true, Args: require.NoArgs, - BashCompletionFunction: fmt.Sprintf("%s%s", contextCompFunc, completion.GetBashCustomFunction()), + BashCompletionFunction: completion.GetBashCustomFunction(), } flags := cmd.PersistentFlags() settings.AddFlags(flags) - flag := flags.Lookup("namespace") // Setup shell completion for the namespace flag + flag := flags.Lookup("namespace") completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) { if client, err := actionConfig.KubernetesClientSet(); err == nil { // Choose a long enough timeout that the user notices somethings is not working @@ -129,6 +106,29 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string return nil, completion.BashCompDirectiveDefault }) + // Setup shell completion for the kube-context flag + flag = flags.Lookup("kube-context") + completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) { + completion.CompDebugln("About to get the different kube-contexts") + + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if len(settings.KubeConfig) > 0 { + loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: settings.KubeConfig} + } + if config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + loadingRules, + &clientcmd.ConfigOverrides{}).RawConfig(); err == nil { + ctxs := []string{} + for name := range config.Contexts { + if strings.HasPrefix(name, toComplete) { + ctxs = append(ctxs, name) + } + } + return ctxs, completion.BashCompDirectiveNoFileComp + } + return nil, completion.BashCompDirectiveNoFileComp + }) + // We can safely ignore any errors that flags.Parse encounters since // those errors will be caught later during the call to cmd.Execution. // This call is required to gather configuration information prior to @@ -173,19 +173,6 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string completion.NewCompleteCmd(settings, out), ) - // Add annotation to flags for which we can generate completion choices - for name, completion := range bashCompletionFlags { - if cmd.Flag(name) != nil { - if cmd.Flag(name).Annotations == nil { - cmd.Flag(name).Annotations = map[string][]string{} - } - cmd.Flag(name).Annotations[cobra.BashCompCustom] = append( - cmd.Flag(name).Annotations[cobra.BashCompCustom], - completion, - ) - } - } - // Add *experimental* subcommands registryClient, err := registry.NewClient( registry.ClientOptDebug(settings.Debug), diff --git a/internal/completion/complete.go b/internal/completion/complete.go index a24390fc0..aa0d134b9 100644 --- a/internal/completion/complete.go +++ b/internal/completion/complete.go @@ -35,9 +35,9 @@ import ( // This should ultimately be pushed down into Cobra. // ================================================================================== -// compRequestCmd Hidden command to request completion results from the program. +// CompRequestCmd Hidden command to request completion results from the program. // Used by the shell completion script. -const compRequestCmd = "__complete" +const CompRequestCmd = "__complete" // Global map allowing to find completion functions for commands or flags. var validArgsFunctions = map[interface{}]func(cmd *cobra.Command, args []string, toComplete string) ([]string, BashCompDirective){} @@ -123,7 +123,7 @@ __helm_custom_func() done < <(compgen -W "${out[*]}" -- "$cur") fi } -`, compRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp) +`, CompRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp) } // RegisterValidArgsFunc should be called to register a function to provide argument completion for a command @@ -177,14 +177,14 @@ func (d BashCompDirective) string() string { func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command { debug = settings.Debug return &cobra.Command{ - Use: fmt.Sprintf("%s [command-line]", compRequestCmd), + Use: fmt.Sprintf("%s [command-line]", CompRequestCmd), DisableFlagsInUseLine: true, Hidden: true, DisableFlagParsing: true, Args: require.MinimumNArgs(1), Short: "Request shell completion choices for the specified command-line", Long: fmt.Sprintf("%s is a special command that is used by the shell completion logic\n%s", - compRequestCmd, "to request completion choices for the specified command-line."), + CompRequestCmd, "to request completion choices for the specified command-line."), Run: func(cmd *cobra.Command, args []string) { CompDebugln(fmt.Sprintf("%s was called with args %v", cmd.Name(), args))