diff --git a/cmd/clusterctl/client/cluster/client.go b/cmd/clusterctl/client/cluster/client.go index 552486c52fbd..28d4fa967a1f 100644 --- a/cmd/clusterctl/client/cluster/client.go +++ b/cmd/clusterctl/client/cluster/client.go @@ -235,6 +235,12 @@ type Proxy interface { // ListResources returns all the Kubernetes objects with the given labels existing the listed namespaces. ListResources(labels map[string]string, namespaces ...string) ([]unstructured.Unstructured, error) + + // GetContexts returns the list of contexts in kubeconfig which begin with prefix. + GetContexts(prefix string) ([]string, error) + + // CompGetResourceNameList returns the list of resource names which begin with prefix. + GetResourceNames(groupVersion, kind string, options []client.ListOption, prefix string) ([]string, error) } // retryWithExponentialBackoff repeats an operation until it passes or the exponential backoff times out. diff --git a/cmd/clusterctl/client/cluster/proxy.go b/cmd/clusterctl/client/cluster/proxy.go index 094f4901cdcb..37339e5fdf92 100644 --- a/cmd/clusterctl/client/cluster/proxy.go +++ b/cmd/clusterctl/client/cluster/proxy.go @@ -204,6 +204,47 @@ func (k *proxy) ListResources(labels map[string]string, namespaces ...string) ([ return ret, nil } +// GetContexts returns the list of contexts in kubeconfig which begin with prefix. +func (k *proxy) GetContexts(prefix string) ([]string, error) { + config, err := k.configLoadingRules.Load() + if err != nil { + return nil, err + } + + var comps []string + for name := range config.Contexts { + if strings.HasPrefix(name, prefix) { + comps = append(comps, name) + } + } + + return comps, nil +} + +// GetResourceNames returns the list of resource names which begin with prefix. +func (k *proxy) GetResourceNames(groupVersion, kind string, options []client.ListOption, prefix string) ([]string, error) { + client, err := k.NewClient() + if err != nil { + return nil, err + } + + objList, err := listObjByGVK(client, groupVersion, kind, options) + if err != nil { + return nil, err + } + + var comps []string + for _, item := range objList.Items { + name := item.GetName() + + if strings.HasPrefix(name, prefix) { + comps = append(comps, name) + } + } + + return comps, nil +} + func listObjByGVK(c client.Client, groupVersion, kind string, options []client.ListOption) (*unstructured.UnstructuredList, error) { objList := new(unstructured.UnstructuredList) objList.SetAPIVersion(groupVersion) diff --git a/cmd/clusterctl/cmd/completion.go b/cmd/clusterctl/cmd/completion.go index 6a76dfb8834b..e1f94ef8a129 100644 --- a/cmd/clusterctl/cmd/completion.go +++ b/cmd/clusterctl/cmd/completion.go @@ -23,6 +23,8 @@ import ( "os" "github.com/spf13/cobra" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + "sigs.k8s.io/cluster-api/cmd/clusterctl/client/config" ) const completionBoilerPlate = `# Copyright 2021 The Kubernetes Authors. @@ -138,3 +140,42 @@ func runCompletionZsh(out io.Writer, cmd *cobra.Command) error { return nil } + +func contextCompletionFunc(kubeconfig *string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + configClient, err := config.New(cfgFile) + if err != nil { + return completionError(err) + } + + client := cluster.New(cluster.Kubeconfig{Path: *kubeconfig}, configClient) + comps, err := client.Proxy().GetContexts(toComplete) + if err != nil { + return completionError(err) + } + + return comps, cobra.ShellCompDirectiveNoFileComp + } +} + +func resourceNameCompletionFunc(kubeconfig *string, groupVersion, kind string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + configClient, err := config.New(cfgFile) + if err != nil { + return completionError(err) + } + + client := cluster.New(cluster.Kubeconfig{Path: *kubeconfig}, configClient) + comps, err := client.Proxy().GetResourceNames(groupVersion, kind, nil, toComplete) + if err != nil { + return completionError(err) + } + + return comps, cobra.ShellCompDirectiveNoFileComp + } +} + +func completionError(err error) ([]string, cobra.ShellCompDirective) { + cobra.CompError(err.Error()) + return nil, cobra.ShellCompDirectiveError +} diff --git a/cmd/clusterctl/cmd/root.go b/cmd/clusterctl/cmd/root.go index fed26478cea2..80cdc240c37e 100644 --- a/cmd/clusterctl/cmd/root.go +++ b/cmd/clusterctl/cmd/root.go @@ -112,7 +112,7 @@ func init() { RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Path to clusterctl configuration (default is `$HOME/.cluster-api/clusterctl.yaml`) or to a remote location (i.e. https://example.com/clusterctl.yaml)") - cobra.OnInitialize(initConfig) + cobra.OnInitialize(initConfig, registerCompletionFuncForCommonFlags) } func initConfig() { @@ -135,6 +135,24 @@ func initConfig() { logf.SetLogger(logf.NewLogger(logf.WithThreshold(verbosity))) } +func registerCompletionFuncForCommonFlags() { + visitCommands(RootCmd, func(cmd *cobra.Command) { + if f := cmd.Flags().Lookup("kubeconfig"); f != nil { + kubeconfig := f.Value.String() + + // context in kubeconfig + for _, flagName := range []string{"kubeconfig-context", "to-kubeconfig-context"} { + _ = cmd.RegisterFlagCompletionFunc(flagName, contextCompletionFunc(&kubeconfig)) + } + + // namespace + for _, flagName := range []string{"namespace", "target-namespace", "from-config-map-namespace"} { + _ = cmd.RegisterFlagCompletionFunc(flagName, resourceNameCompletionFunc(&kubeconfig, "v1", "namespace")) + } + } + }) +} + const indentation = ` ` // LongDesc normalizes a command's long description to follow the conventions. diff --git a/cmd/clusterctl/cmd/util.go b/cmd/clusterctl/cmd/util.go index 005cdf1fd046..7dc6459ad686 100644 --- a/cmd/clusterctl/cmd/util.go +++ b/cmd/clusterctl/cmd/util.go @@ -26,6 +26,7 @@ import ( "text/tabwriter" "github.com/pkg/errors" + "github.com/spf13/cobra" "sigs.k8s.io/cluster-api/cmd/clusterctl/client" ) @@ -166,3 +167,12 @@ func printComponentsAsText(c client.Components) error { return nil } + +// visitCommands visits the commands. +func visitCommands(cmd *cobra.Command, fn func(*cobra.Command)) { + fn(cmd) + + for _, c := range cmd.Commands() { + visitCommands(c, fn) + } +} diff --git a/cmd/clusterctl/internal/test/fake_proxy.go b/cmd/clusterctl/internal/test/fake_proxy.go index 427d43d96b03..47da497e81f6 100644 --- a/cmd/clusterctl/internal/test/fake_proxy.go +++ b/cmd/clusterctl/internal/test/fake_proxy.go @@ -122,6 +122,14 @@ func (f *FakeProxy) ListResources(labels map[string]string, namespaces ...string return ret, nil } +func (f *FakeProxy) GetContexts(prefix string) ([]string, error) { + return nil, nil +} + +func (f *FakeProxy) GetResourceNames(groupVersion, kind string, options []client.ListOption, prefix string) ([]string, error) { + return nil, nil +} + func NewFakeProxy() *FakeProxy { return &FakeProxy{ namespace: "default",