From a684edd354781f216e790fa84e3c80f1f9175595 Mon Sep 17 00:00:00 2001 From: zhzhuang-zju Date: Fri, 20 Sep 2024 09:42:40 +0800 Subject: [PATCH] Implementing autocompletion for karmadactl Signed-off-by: zhzhuang-zju --- pkg/karmadactl/annotate/annotate.go | 3 + pkg/karmadactl/apiresources/apiresources.go | 5 + pkg/karmadactl/apiresources/apiversions.go | 6 + pkg/karmadactl/apply/apply.go | 7 + pkg/karmadactl/attach/attach.go | 9 +- pkg/karmadactl/completion/completion.go | 143 ++++++ pkg/karmadactl/cordon/cordon.go | 7 + pkg/karmadactl/create/create.go | 5 + pkg/karmadactl/delete/delete.go | 4 + pkg/karmadactl/describe/describe.go | 6 + pkg/karmadactl/edit/edit.go | 4 + pkg/karmadactl/exec/exec.go | 9 + pkg/karmadactl/explain/explain.go | 6 + pkg/karmadactl/get/get.go | 116 +++-- pkg/karmadactl/interpret/interpret.go | 2 + pkg/karmadactl/join/join.go | 2 + pkg/karmadactl/karmadactl.go | 16 +- pkg/karmadactl/label/label.go | 4 + pkg/karmadactl/logs/logs.go | 5 + pkg/karmadactl/patch/patch.go | 4 + pkg/karmadactl/promote/promote.go | 5 + pkg/karmadactl/taint/taint.go | 3 + pkg/karmadactl/token/token.go | 4 + pkg/karmadactl/top/top_node.go | 9 +- pkg/karmadactl/top/top_pods.go | 11 +- pkg/karmadactl/unjoin/unjoin.go | 2 + pkg/karmadactl/util/completion/completion.go | 505 +++++++++++++++++++ 27 files changed, 839 insertions(+), 63 deletions(-) create mode 100644 pkg/karmadactl/completion/completion.go create mode 100644 pkg/karmadactl/util/completion/completion.go diff --git a/pkg/karmadactl/annotate/annotate.go b/pkg/karmadactl/annotate/annotate.go index 9cbc24421c0a..003da8270d38 100644 --- a/pkg/karmadactl/annotate/annotate.go +++ b/pkg/karmadactl/annotate/annotate.go @@ -23,6 +23,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -57,5 +58,7 @@ func NewCmdAnnotate(f util.Factory, parentCommand string, ioStreams genericioopt } options.AddKubeConfigFlags(cmd.Flags()) options.AddNamespaceFlag(cmd.Flags()) + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) return cmd } diff --git a/pkg/karmadactl/apiresources/apiresources.go b/pkg/karmadactl/apiresources/apiresources.go index 5b51ad836240..6103882629f1 100644 --- a/pkg/karmadactl/apiresources/apiresources.go +++ b/pkg/karmadactl/apiresources/apiresources.go @@ -27,6 +27,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -83,6 +84,10 @@ func NewCmdAPIResources(f util.Factory, parentCommand string, ioStreams generici cmd.Flags().StringVar(&o.SortBy, "sort-by", o.SortBy, "If non-empty, sort list of resources using specified field. The field can be either 'name' or 'kind'.") cmd.Flags().BoolVar(&o.Cached, "cached", o.Cached, "Use the cached list of resources if available.") cmd.Flags().StringSliceVar(&o.Categories, "categories", o.Categories, "Limit to resources that belong to the specified categories.") + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForOperationScopeFlag(cmd, options.KarmadaControlPlane, options.Members) + utilcomp.RegisterCompletionFuncForClusterFlag(cmd) return cmd } diff --git a/pkg/karmadactl/apiresources/apiversions.go b/pkg/karmadactl/apiresources/apiversions.go index fb2ea0b5b60f..61edc3b14af5 100644 --- a/pkg/karmadactl/apiresources/apiversions.go +++ b/pkg/karmadactl/apiresources/apiversions.go @@ -27,6 +27,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -62,6 +63,11 @@ func NewCmdAPIVersions(f util.Factory, parentCommand string, ioStreams genericio options.AddKubeConfigFlags(cmd.Flags()) cmd.Flags().VarP(&o.OperationScope, "operation-scope", "s", "Used to control the operation scope of the command. The optional values are karmada and members. Defaults to karmada.") cmd.Flags().StringVar(&o.Cluster, "cluster", "", "Used to specify a target member cluster and only takes effect when the command's operation scope is members, for example: --operation-scope=members --cluster=member1") + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForOperationScopeFlag(cmd, options.KarmadaControlPlane, options.Members) + utilcomp.RegisterCompletionFuncForClusterFlag(cmd) + return cmd } diff --git a/pkg/karmadactl/apply/apply.go b/pkg/karmadactl/apply/apply.go index de1d552d4921..b990a12aad21 100644 --- a/pkg/karmadactl/apply/apply.go +++ b/pkg/karmadactl/apply/apply.go @@ -35,6 +35,7 @@ import ( karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned" "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" "github.com/karmada-io/karmada/pkg/util/names" ) @@ -93,6 +94,7 @@ func NewCmdApply(f util.Factory, parentCommand string, streams genericiooptions. SilenceUsage: true, DisableFlagsInUseLine: true, Example: fmt.Sprintf(applyExample, parentCommand), + ValidArgsFunction: utilcomp.ResourceTypeAndNameCompletionFunc(f), RunE: func(cmd *cobra.Command, args []string) error { if err := o.Complete(f, cmd, parentCommand, args); err != nil { return err @@ -113,6 +115,11 @@ func NewCmdApply(f util.Factory, parentCommand string, streams genericiooptions. options.AddNamespaceFlag(flags) flags.BoolVarP(&o.AllClusters, "all-clusters", "", o.AllClusters, "If present, propagates a group of resources to all member clusters.") flags.StringSliceVarP(&o.Clusters, "cluster", "C", o.Clusters, "If present, propagates a group of resources to specified clusters.") + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) + utilcomp.RegisterCompletionFuncForClusterFlag(cmd) + return cmd } diff --git a/pkg/karmadactl/attach/attach.go b/pkg/karmadactl/attach/attach.go index 3a865f684672..5896b3a31dd8 100644 --- a/pkg/karmadactl/attach/attach.go +++ b/pkg/karmadactl/attach/attach.go @@ -24,11 +24,11 @@ import ( "k8s.io/cli-runtime/pkg/genericiooptions" kubectlattach "k8s.io/kubectl/pkg/cmd/attach" cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/templates" "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -64,7 +64,7 @@ func NewCmdAttach(f util.Factory, parentCommand string, streams genericiooptions Short: "Attach to a running container", Long: "Attach to a process that is already running inside an existing container.", Example: fmt.Sprintf(attachExample, parentCommand), - ValidArgsFunction: completion.PodResourceNameCompletionFunc(f), + ValidArgsFunction: utilcomp.PodResourceNameCompletionFunc(f), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) @@ -85,6 +85,11 @@ func NewCmdAttach(f util.Factory, parentCommand string, streams genericiooptions cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "Only print output from the remote session") cmd.Flags().VarP(&o.OperationScope, "operation-scope", "s", "Used to control the operation scope of the command. The optional values are karmada and members. Defaults to karmada.") cmd.Flags().StringVar(&o.Cluster, "cluster", "", "Used to specify a target member cluster and only takes effect when the command's operation scope is members, for example: --operation-scope=members --cluster=member1") + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) + utilcomp.RegisterCompletionFuncForOperationScopeFlag(cmd, options.KarmadaControlPlane, options.Members) + utilcomp.RegisterCompletionFuncForClusterFlag(cmd) return cmd } diff --git a/pkg/karmadactl/completion/completion.go b/pkg/karmadactl/completion/completion.go new file mode 100644 index 000000000000..92c4d4171bee --- /dev/null +++ b/pkg/karmadactl/completion/completion.go @@ -0,0 +1,143 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package completion + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +const defaultBoilerPlate = ` +# Copyright 2024 The Karmada Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +` + +var ( + completionLong = templates.LongDesc(i18n.T(` + Output shell completion code for the specified shell (bash, zsh). + The shell code must be evaluated to provide interactive + completion of kubectl commands. This can be done by sourcing it from + the .bash_profile. + + Note for zsh users: zsh completions are only supported in versions of zsh >= 5.2.`)) + + completionExample = templates.Examples(i18n.T(` + # Installing bash completion on Linux + ## If bash-completion is not installed on Linux, install the 'bash-completion' package + 1. apt-get install bash-completion + 2. source /usr/share/bash-completion/bash_completion + ## Load the %[1]s completion code for bash into the current shell + source <(%[1]s completion bash) + ## Or, write bash completion code to a file and source it from .bash_profile + 1. %[1]s completion bash > ~/.kube/completion.bash.inc + 2. echo "source '$HOME/.kube/completion.bash.inc'" >> $HOME/.bash_profile + 3. source $HOME/.bash_profile + + # Load the %[1]s completion code for zsh into the current shell + source <(%[1]s completion zsh) + # Set the %[1]s completion code for zsh to autoload on startup + %[1]s completion zsh > "${fpath[1]}/%[1]s"`)) +) + +var ( + // TODO: support output shell completion code for more specified shell, like `fish` and `powershell`. + completionShells = map[string]func(out io.Writer, boilerPlate string, cmd *cobra.Command) error{ + "bash": runCompletionBash, + "zsh": runCompletionZsh, + } +) + +// NewCmdCompletion creates the `completion` command +func NewCmdCompletion(parentCommand string, out io.Writer, boilerPlate string) *cobra.Command { + var shells []string + for s := range completionShells { + shells = append(shells, s) + } + + cmd := &cobra.Command{ + Use: "completion SHELL", + DisableFlagsInUseLine: true, + Short: "Output shell completion code for the specified shell (bash, zsh)", + Long: completionLong, + Example: fmt.Sprintf(completionExample, parentCommand), + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(RunCompletion(out, boilerPlate, cmd, args)) + }, + ValidArgs: shells, + } + + return cmd +} + +// RunCompletion checks given arguments and executes command +func RunCompletion(out io.Writer, boilerPlate string, cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmdutil.UsageErrorf(cmd, "Shell not specified.") + } + if len(args) > 1 { + return cmdutil.UsageErrorf(cmd, "Too many arguments. Expected only the shell type.") + } + run, found := completionShells[args[0]] + if !found { + return cmdutil.UsageErrorf(cmd, "Unsupported shell type %q.", args[0]) + } + + return run(out, boilerPlate, cmd.Parent()) +} + +func runCompletionBash(out io.Writer, boilerPlate string, cmd *cobra.Command) error { + if len(boilerPlate) == 0 { + boilerPlate = defaultBoilerPlate + } + if _, err := out.Write([]byte(boilerPlate)); err != nil { + return err + } + + return cmd.GenBashCompletionV2(out, true) +} + +func runCompletionZsh(out io.Writer, boilerPlate string, cmd *cobra.Command) error { + zshHead := fmt.Sprintf("#compdef %[1]s\ncompdef _%[1]s %[1]s\n", cmd.Name()) + if _, err := out.Write([]byte(zshHead)); err != nil { + return err + } + + if len(boilerPlate) == 0 { + boilerPlate = defaultBoilerPlate + } + if _, err := out.Write([]byte(boilerPlate)); err != nil { + return err + } + + return cmd.GenZshCompletion(out) +} diff --git a/pkg/karmadactl/cordon/cordon.go b/pkg/karmadactl/cordon/cordon.go index 7bc5cf7a8d28..955b3b04496b 100644 --- a/pkg/karmadactl/cordon/cordon.go +++ b/pkg/karmadactl/cordon/cordon.go @@ -33,6 +33,7 @@ import ( karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned" "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -61,6 +62,7 @@ const ( // NewCmdCordon defines the `cordon` command that mark cluster as unschedulable. func NewCmdCordon(f util.Factory, parentCommand string) *cobra.Command { opts := CommandCordonOption{} + cmd := &cobra.Command{ Use: "cordon CLUSTER", Short: "Mark cluster as unschedulable", @@ -68,6 +70,7 @@ func NewCmdCordon(f util.Factory, parentCommand string) *cobra.Command { Example: fmt.Sprintf(cordonExample, parentCommand), SilenceUsage: true, DisableFlagsInUseLine: true, + ValidArgsFunction: utilcomp.SpecifiedResourceTypeAndNameCompletionFunc(f, []string{"cluster"}), RunE: func(_ *cobra.Command, args []string) error { if err := opts.Complete(args); err != nil { return err @@ -86,12 +89,14 @@ func NewCmdCordon(f util.Factory, parentCommand string) *cobra.Command { options.AddKubeConfigFlags(flags) flags.BoolVar(&opts.DryRun, "dry-run", false, "Run the command in dry-run mode, without making any server requests.") + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) return cmd } // NewCmdUncordon defines the `uncordon` command that mark cluster as schedulable. func NewCmdUncordon(f util.Factory, parentCommand string) *cobra.Command { opts := CommandCordonOption{} + cmd := &cobra.Command{ Use: "uncordon CLUSTER", Short: "Mark cluster as schedulable", @@ -99,6 +104,7 @@ func NewCmdUncordon(f util.Factory, parentCommand string) *cobra.Command { Example: fmt.Sprintf(uncordonExample, parentCommand), SilenceUsage: true, DisableFlagsInUseLine: true, + ValidArgsFunction: utilcomp.SpecifiedResourceTypeAndNameCompletionFunc(f, []string{"cluster"}), RunE: func(_ *cobra.Command, args []string) error { if err := opts.Complete(args); err != nil { return err @@ -117,6 +123,7 @@ func NewCmdUncordon(f util.Factory, parentCommand string) *cobra.Command { options.AddKubeConfigFlags(flags) flags.BoolVar(&opts.DryRun, "dry-run", false, "Run the command in dry-run mode, without making any server requests.") + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) return cmd } diff --git a/pkg/karmadactl/create/create.go b/pkg/karmadactl/create/create.go index 905157087f6f..ed34d3becd99 100644 --- a/pkg/karmadactl/create/create.go +++ b/pkg/karmadactl/create/create.go @@ -26,6 +26,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -55,5 +56,9 @@ func NewCmdCreate(f util.Factory, parentCommand string, ioStreams genericiooptio } options.AddKubeConfigFlags(cmd.PersistentFlags()) options.AddNamespaceFlag(cmd.PersistentFlags()) + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) + return cmd } diff --git a/pkg/karmadactl/delete/delete.go b/pkg/karmadactl/delete/delete.go index 84d1c29a8416..79179c4929e0 100644 --- a/pkg/karmadactl/delete/delete.go +++ b/pkg/karmadactl/delete/delete.go @@ -23,6 +23,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -98,5 +99,8 @@ func NewCmdDelete(f util.Factory, parentCommand string, ioStreams genericiooptio } options.AddKubeConfigFlags(cmd.Flags()) options.AddNamespaceFlag(cmd.Flags()) + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) return cmd } diff --git a/pkg/karmadactl/describe/describe.go b/pkg/karmadactl/describe/describe.go index 64c03a29b9de..b2c2ce3788b3 100644 --- a/pkg/karmadactl/describe/describe.go +++ b/pkg/karmadactl/describe/describe.go @@ -26,6 +26,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -74,6 +75,7 @@ func NewCmdDescribe(f util.Factory, parentCommand string, streams genericiooptio Long: fmt.Sprintf(describeLong, parentCommand), SilenceUsage: true, DisableFlagsInUseLine: true, + ValidArgsFunction: utilcomp.ResourceTypeAndNameCompletionFunc(f), Example: fmt.Sprintf(describeExample, parentCommand), RunE: func(_ *cobra.Command, args []string) error { if err := o.Complete(f, args, kubedescribeFlags, parentCommand); err != nil { @@ -101,6 +103,10 @@ func NewCmdDescribe(f util.Factory, parentCommand string, streams genericiooptio flags.VarP(&o.OperationScope, "operation-scope", "s", "Used to control the operation scope of the command. The optional values are karmada and members. Defaults to karmada.") flags.StringVarP(&o.Cluster, "cluster", "C", "", "Used to specify a target member cluster and only takes effect when the command's operation scope is members, for example: --operation-scope=members --cluster=member1") + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) + utilcomp.RegisterCompletionFuncForOperationScopeFlag(cmd, options.KarmadaControlPlane, options.Members) + utilcomp.RegisterCompletionFuncForClusterFlag(cmd) return cmd } diff --git a/pkg/karmadactl/edit/edit.go b/pkg/karmadactl/edit/edit.go index 1bfe9012f45e..56ec6fd64c1d 100644 --- a/pkg/karmadactl/edit/edit.go +++ b/pkg/karmadactl/edit/edit.go @@ -27,6 +27,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -56,5 +57,8 @@ func NewCmdEdit(f util.Factory, parentCommand string, ioStreams genericiooptions } options.AddKubeConfigFlags(cmd.Flags()) options.AddNamespaceFlag(cmd.Flags()) + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) return cmd } diff --git a/pkg/karmadactl/exec/exec.go b/pkg/karmadactl/exec/exec.go index ff43477ca0b1..3cd9624b89b6 100644 --- a/pkg/karmadactl/exec/exec.go +++ b/pkg/karmadactl/exec/exec.go @@ -28,6 +28,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) const ( @@ -80,6 +81,7 @@ func NewCmdExec(f util.Factory, parentCommand string, streams genericiooptions.I SilenceUsage: true, DisableFlagsInUseLine: true, Example: fmt.Sprintf(execExample, parentCommand), + ValidArgsFunction: utilcomp.PodResourceNameCompletionFunc(f), RunE: func(cmd *cobra.Command, args []string) error { argsLenAtDash := cmd.ArgsLenAtDash() if err := o.Complete(f, cmd, args, argsLenAtDash); err != nil { @@ -111,6 +113,13 @@ func NewCmdExec(f util.Factory, parentCommand string, streams genericiooptions.I flags.BoolVarP(&o.KubectlExecOptions.Quiet, "quiet", "q", o.KubectlExecOptions.Quiet, "Only print output from the remote session") flags.VarP(&o.OperationScope, "operation-scope", "s", "Used to control the operation scope of the command. The optional values are karmada and members. Defaults to karmada.") flags.StringVar(&o.Cluster, "cluster", "", "Used to specify a target member cluster and only takes effect when the command's operation scope is members, for example: --operation-scope=members --cluster=member1") + + cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc("container", utilcomp.ContainerCompletionFunc(f))) + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) + utilcomp.RegisterCompletionFuncForOperationScopeFlag(cmd, options.KarmadaControlPlane, options.Members) + utilcomp.RegisterCompletionFuncForClusterFlag(cmd) + return cmd } diff --git a/pkg/karmadactl/explain/explain.go b/pkg/karmadactl/explain/explain.go index 088450e76a08..3f2848a4fc9e 100644 --- a/pkg/karmadactl/explain/explain.go +++ b/pkg/karmadactl/explain/explain.go @@ -27,6 +27,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -90,6 +91,11 @@ func NewCmdExplain(f util.Factory, parentCommand string, streams genericiooption // Only enable --output as a valid flag if the feature is enabled flags.StringVar(&o.OutputFormat, "output", plaintextTemplateName, "Format in which to render the schema. Valid values are: (plaintext, plaintext-openapiv2).") flags.StringVar(&o.Cluster, "cluster", "", "Used to specify a target member cluster and only takes effect when the command's operation scope is member clusters, for example: --operation-scope=all --cluster=member1") + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) + utilcomp.RegisterCompletionFuncForOperationScopeFlag(cmd, options.KarmadaControlPlane, options.Members) + utilcomp.RegisterCompletionFuncForClusterFlag(cmd) return cmd } diff --git a/pkg/karmadactl/get/get.go b/pkg/karmadactl/get/get.go index 8459819e18d5..5f8156220184 100644 --- a/pkg/karmadactl/get/get.go +++ b/pkg/karmadactl/get/get.go @@ -130,13 +130,13 @@ func NewCmdGet(f util.Factory, parentCommand string, streams genericiooptions.IO DisableFlagsInUseLine: true, Example: fmt.Sprintf(getExample, parentCommand), RunE: func(cmd *cobra.Command, args []string) error { - if err := o.Complete(f); err != nil { + if err := o.Complete(f, cmd); err != nil { return err } if err := o.Validate(cmd); err != nil { return err } - if err := o.Run(f, cmd, args); err != nil { + if err := o.Run(f, args); err != nil { return err } return nil @@ -167,7 +167,7 @@ func NewCmdGet(f util.Factory, parentCommand string, streams genericiooptions.IO type CommandGetOptions struct { Clusters []string OperationScope options.OperationScope - targetMemberClusters []string + TargetMemberClusters []string PrintFlags *get.PrintFlags ToPrinter func(*meta.RESTMapping, *bool, bool, bool) (printers.ResourcePrinterFunc, error) @@ -198,7 +198,7 @@ type CommandGetOptions struct { genericiooptions.IOStreams - karmadaClient karmadaclientset.Interface + KarmadaClient karmadaclientset.Interface } // NewCommandGetOptions returns a CommandGetOptions with default chunk size 500. @@ -212,9 +212,7 @@ func NewCommandGetOptions(streams genericiooptions.IOStreams) *CommandGetOptions } // Complete takes the command arguments and infers any remaining options. -func (g *CommandGetOptions) Complete(f util.Factory) error { - newScheme := gclient.NewSchema() - +func (g *CommandGetOptions) Complete(f util.Factory, cmd *cobra.Command) error { err := g.handleNamespaceScopeFlags(f) if err != nil { return err @@ -225,12 +223,54 @@ func (g *CommandGetOptions) Complete(f util.Factory) error { templateArg = *g.PrintFlags.TemplateFlags.TemplateArgument } + outputOption := cmd.Flags().Lookup("output").Value.String() + if strings.Contains(outputOption, "custom-columns") || outputOption == "yaml" || strings.Contains(outputOption, "json") { + g.ServerPrint = false + } + // human readable printers have special conversion rules, so we determine if we're using one. if (len(*g.PrintFlags.OutputFormat) == 0 && len(templateArg) == 0) || *g.PrintFlags.OutputFormat == "wide" { g.IsHumanReadablePrinter = true } - g.ToPrinter = func(mapping *meta.RESTMapping, outputObjects *bool, withNamespace bool, withKind bool) (printers.ResourcePrinterFunc, error) { + g.ToPrinter = g.getResourcePrinter() + karmadaClient, err := f.KarmadaClientSet() + if err != nil { + return err + } + g.KarmadaClient = karmadaClient + return g.HandleClusterScopeFlags() +} + +// Validate checks the set of flags provided by the user. +func (g *CommandGetOptions) Validate(cmd *cobra.Command) error { + if cmdutil.GetFlagBool(cmd, "show-labels") { + outputOption := cmd.Flags().Lookup("output").Value.String() + if outputOption != "" && outputOption != "wide" { + return fmt.Errorf("--show-labels option cannot be used with %s printer", outputOption) + } + } + if g.OutputWatchEvents && !(g.Watch || g.WatchOnly) { + return fmt.Errorf("--output-watch-events option can only be used with --watch or --watch-only") + } + + if err := options.VerifyOperationScopeFlags(g.OperationScope); err != nil { + return err + } + + if options.ContainMembersScope(g.OperationScope) && len(g.Clusters) > 0 { + clusters, err := g.KarmadaClient.ClusterV1alpha1().Clusters().List(context.TODO(), metav1.ListOptions{}) + if err != nil { + return err + } + return util.VerifyClustersExist(g.Clusters, clusters) + } + return nil +} + +func (g *CommandGetOptions) getResourcePrinter() func(mapping *meta.RESTMapping, outputObjects *bool, withNamespace bool, withKind bool) (printers.ResourcePrinterFunc, error) { + newScheme := gclient.NewSchema() + return func(mapping *meta.RESTMapping, outputObjects *bool, withNamespace bool, withKind bool) (printers.ResourcePrinterFunc, error) { // make a new copy of current flags / opts before mutating printFlags := g.PrintFlags.Copy() @@ -263,51 +303,20 @@ func (g *CommandGetOptions) Complete(f util.Factory) error { return printer.PrintObj, nil } - karmadaClient, err := f.KarmadaClientSet() - if err != nil { - return err - } - g.karmadaClient = karmadaClient - return g.handleClusterScopeFlags() -} - -// Validate checks the set of flags provided by the user. -func (g *CommandGetOptions) Validate(cmd *cobra.Command) error { - if cmdutil.GetFlagBool(cmd, "show-labels") { - outputOption := cmd.Flags().Lookup("output").Value.String() - if outputOption != "" && outputOption != "wide" { - return fmt.Errorf("--show-labels option cannot be used with %s printer", outputOption) - } - } - if g.OutputWatchEvents && !(g.Watch || g.WatchOnly) { - return fmt.Errorf("--output-watch-events option can only be used with --watch or --watch-only") - } - - if err := options.VerifyOperationScopeFlags(g.OperationScope); err != nil { - return err - } - - if options.ContainMembersScope(g.OperationScope) && len(g.Clusters) > 0 { - clusters, err := g.karmadaClient.ClusterV1alpha1().Clusters().List(context.TODO(), metav1.ListOptions{}) - if err != nil { - return err - } - return util.VerifyClustersExist(g.Clusters, clusters) - } - return nil } -func (g *CommandGetOptions) handleClusterScopeFlags() error { +// HandleClusterScopeFlags used to handle flags related to cluster scope. +func (g *CommandGetOptions) HandleClusterScopeFlags() error { var err error switch g.OperationScope { case options.KarmadaControlPlane: - g.targetMemberClusters = []string{} + g.TargetMemberClusters = []string{} case options.Members, options.All: if len(g.Clusters) == 0 { - g.targetMemberClusters, err = LoadRegisteredClusters(g.karmadaClient) + g.TargetMemberClusters, err = LoadRegisteredClusters(g.KarmadaClient) return err } - g.targetMemberClusters = g.Clusters + g.TargetMemberClusters = g.Clusters return nil } return nil @@ -339,7 +348,7 @@ type WatchObj struct { } // Run performs the get operation. -func (g *CommandGetOptions) Run(f util.Factory, cmd *cobra.Command, args []string) error { +func (g *CommandGetOptions) Run(f util.Factory, args []string) error { mux := sync.Mutex{} var wg sync.WaitGroup @@ -347,24 +356,19 @@ func (g *CommandGetOptions) Run(f util.Factory, cmd *cobra.Command, args []strin var watchObjs []WatchObj var allErrs []error - outputOption := cmd.Flags().Lookup("output").Value.String() - if strings.Contains(outputOption, "custom-columns") || outputOption == "yaml" || strings.Contains(outputOption, "json") { - g.ServerPrint = false - } - if options.ContainKarmadaScope(g.OperationScope) { g.getObjInfo(&mux, f, "Karmada", true, &objs, &watchObjs, &allErrs, args) } - if len(g.targetMemberClusters) != 0 { - wg.Add(len(g.targetMemberClusters)) - for idx := range g.targetMemberClusters { - memberFactory, err := f.FactoryForMemberCluster(g.targetMemberClusters[idx]) + if len(g.TargetMemberClusters) != 0 { + wg.Add(len(g.TargetMemberClusters)) + for idx := range g.TargetMemberClusters { + memberFactory, err := f.FactoryForMemberCluster(g.TargetMemberClusters[idx]) if err != nil { return err } go func() { - g.getObjInfo(&mux, memberFactory, g.targetMemberClusters[idx], false, &objs, &watchObjs, &allErrs, args) + g.getObjInfo(&mux, memberFactory, g.TargetMemberClusters[idx], false, &objs, &watchObjs, &allErrs, args) wg.Done() }() } @@ -476,7 +480,7 @@ func (g *CommandGetOptions) printIfNotFindResource(written int, allErrs *[]error if written != 0 || g.IgnoreNotFound || len(*allErrs) != 0 { return } - if !options.ContainKarmadaScope(g.OperationScope) && len(g.targetMemberClusters) == 0 { + if !options.ContainKarmadaScope(g.OperationScope) && len(g.TargetMemberClusters) == 0 { fmt.Fprintln(g.ErrOut, "No member Clusters found in Karmada control plane") return } diff --git a/pkg/karmadactl/interpret/interpret.go b/pkg/karmadactl/interpret/interpret.go index 9992613933ac..6122c24b33aa 100644 --- a/pkg/karmadactl/interpret/interpret.go +++ b/pkg/karmadactl/interpret/interpret.go @@ -34,6 +34,7 @@ import ( workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" "github.com/karmada-io/karmada/pkg/karmadactl/util/genericresource" "github.com/karmada-io/karmada/pkg/util/gclient" "github.com/karmada-io/karmada/pkg/util/helper" @@ -131,6 +132,7 @@ func NewCmdInterpret(f util.Factory, parentCommand string, streams genericioopti cmdutil.AddJsonFilenameFlag(flags, &o.FilenameOptions.Filenames, "Filename, directory, or URL to files containing the customizations") flags.BoolVarP(&o.FilenameOptions.Recursive, "recursive", "R", false, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.") + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) return cmd } diff --git a/pkg/karmadactl/join/join.go b/pkg/karmadactl/join/join.go index fd773025f4f1..d3ce88be906e 100644 --- a/pkg/karmadactl/join/join.go +++ b/pkg/karmadactl/join/join.go @@ -33,6 +33,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" cmdutil "github.com/karmada-io/karmada/pkg/karmadactl/util" "github.com/karmada-io/karmada/pkg/karmadactl/util/apiclient" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" "github.com/karmada-io/karmada/pkg/util" ) @@ -77,6 +78,7 @@ func NewCmdJoin(f cmdutil.Factory, parentCommand string) *cobra.Command { opts.AddFlags(flags) options.AddKubeConfigFlags(flags) + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) return cmd } diff --git a/pkg/karmadactl/karmadactl.go b/pkg/karmadactl/karmadactl.go index 6eaa0ff01e1a..069c7c1ca58f 100644 --- a/pkg/karmadactl/karmadactl.go +++ b/pkg/karmadactl/karmadactl.go @@ -34,6 +34,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/apply" "github.com/karmada-io/karmada/pkg/karmadactl/attach" "github.com/karmada-io/karmada/pkg/karmadactl/cmdinit" + "github.com/karmada-io/karmada/pkg/karmadactl/completion" "github.com/karmada-io/karmada/pkg/karmadactl/cordon" "github.com/karmada-io/karmada/pkg/karmadactl/create" "github.com/karmada-io/karmada/pkg/karmadactl/deinit" @@ -56,6 +57,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/top" "github.com/karmada-io/karmada/pkg/karmadactl/unjoin" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" "github.com/karmada-io/karmada/pkg/version/sharedcommand" ) @@ -91,12 +93,21 @@ func NewKarmadaCtlCommand(cmdUse, parentCommand string) *cobra.Command { _ = flag.CommandLine.Parse(nil) f := util.NewFactory(options.DefaultConfigFlags) ioStreams := genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} + + // Avoid import cycle by setting ValidArgsFunction here instead of in NewCmdGet() + getCmd := get.NewCmdGet(f, parentCommand, ioStreams) + getCmd.ValidArgsFunction = utilcomp.ResourceTypeAndNameCompletionFunc(f) + utilcomp.RegisterCompletionFuncForClustersFlag(getCmd) + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(getCmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(getCmd, f) + utilcomp.RegisterCompletionFuncForOperationScopeFlag(getCmd) + groups := templates.CommandGroups{ { Message: "Basic Commands:", Commands: []*cobra.Command{ explain.NewCmdExplain(f, parentCommand, ioStreams), - get.NewCmdGet(f, parentCommand, ioStreams), + getCmd, create.NewCmdCreate(f, parentCommand, ioStreams), karmadactldelete.NewCmdDelete(f, parentCommand, ioStreams), edit.NewCmdEdit(f, parentCommand, ioStreams), @@ -146,6 +157,7 @@ func NewKarmadaCtlCommand(cmdUse, parentCommand string) *cobra.Command { Commands: []*cobra.Command{ label.NewCmdLabel(f, parentCommand, ioStreams), annotate.NewCmdAnnotate(f, parentCommand, ioStreams), + completion.NewCmdCompletion(parentCommand, ioStreams.Out, ""), }, }, { @@ -165,6 +177,8 @@ func NewKarmadaCtlCommand(cmdUse, parentCommand string) *cobra.Command { templates.ActsAsRootCommand(rootCmd, filters, groups...) + utilcomp.SetFactoryForCompletion(f) + return rootCmd } diff --git a/pkg/karmadactl/label/label.go b/pkg/karmadactl/label/label.go index 7e05d70895e0..2be1cec696eb 100644 --- a/pkg/karmadactl/label/label.go +++ b/pkg/karmadactl/label/label.go @@ -23,6 +23,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -56,5 +57,8 @@ func NewCmdLabel(f util.Factory, parentCommand string, ioStreams genericiooption } options.AddKubeConfigFlags(cmd.Flags()) options.AddNamespaceFlag(cmd.Flags()) + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) return cmd } diff --git a/pkg/karmadactl/logs/logs.go b/pkg/karmadactl/logs/logs.go index 0cea7a7c9518..c6725df29d14 100644 --- a/pkg/karmadactl/logs/logs.go +++ b/pkg/karmadactl/logs/logs.go @@ -27,6 +27,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) const ( @@ -79,6 +80,7 @@ func NewCmdLogs(f util.Factory, parentCommand string, streams genericiooptions.I SilenceUsage: true, DisableFlagsInUseLine: true, Example: fmt.Sprintf(logsExample, parentCommand), + ValidArgsFunction: utilcomp.PodResourceNameAndContainerCompletionFunc(f), RunE: func(cmd *cobra.Command, args []string) error { if err := o.Complete(cmd, args, f); err != nil { return err @@ -102,6 +104,9 @@ func NewCmdLogs(f util.Factory, parentCommand string, streams genericiooptions.I flags.StringVarP(&o.Cluster, "cluster", "C", "", "Specify a member cluster") o.KubectlLogsOptions.AddFlags(cmd) + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) + utilcomp.RegisterCompletionFuncForClusterFlag(cmd) return cmd } diff --git a/pkg/karmadactl/patch/patch.go b/pkg/karmadactl/patch/patch.go index 49b9920c9d55..a38c55a7f5f7 100644 --- a/pkg/karmadactl/patch/patch.go +++ b/pkg/karmadactl/patch/patch.go @@ -23,6 +23,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -52,5 +53,8 @@ func NewCmdPatch(f util.Factory, parentCommand string, ioStreams genericiooption } options.AddKubeConfigFlags(cmd.Flags()) options.AddNamespaceFlag(cmd.Flags()) + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) return cmd } diff --git a/pkg/karmadactl/promote/promote.go b/pkg/karmadactl/promote/promote.go index 80c400712ce4..ded8253f5bfa 100644 --- a/pkg/karmadactl/promote/promote.go +++ b/pkg/karmadactl/promote/promote.go @@ -46,6 +46,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/get" "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" "github.com/karmada-io/karmada/pkg/resourceinterpreter/customized/declarative" "github.com/karmada-io/karmada/pkg/resourceinterpreter/customized/webhook" "github.com/karmada-io/karmada/pkg/resourceinterpreter/customized/webhook/request" @@ -103,6 +104,7 @@ func NewCmdPromote(f util.Factory, parentCommand string) *cobra.Command { Example: fmt.Sprintf(promoteExample, parentCommand), SilenceUsage: true, DisableFlagsInUseLine: true, + ValidArgsFunction: utilcomp.ResourceTypeAndNameCompletionFunc(f), RunE: func(_ *cobra.Command, args []string) error { if err := opts.Complete(f, args); err != nil { return err @@ -125,6 +127,9 @@ func NewCmdPromote(f util.Factory, parentCommand string) *cobra.Command { options.AddKubeConfigFlags(flag) options.AddNamespaceFlag(flag) + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) + utilcomp.RegisterCompletionFuncForClusterFlag(cmd) return cmd } diff --git a/pkg/karmadactl/taint/taint.go b/pkg/karmadactl/taint/taint.go index b050d91d59aa..de150eaa843a 100644 --- a/pkg/karmadactl/taint/taint.go +++ b/pkg/karmadactl/taint/taint.go @@ -37,6 +37,7 @@ import ( "github.com/karmada-io/karmada/pkg/generated/clientset/versioned/scheme" "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" "github.com/karmada-io/karmada/pkg/util/lifted" ) @@ -84,6 +85,7 @@ func NewCmdTaint(f util.Factory, parentCommand string) *cobra.Command { Example: fmt.Sprintf(taintExample, parentCommand), SilenceUsage: true, DisableFlagsInUseLine: true, + ValidArgsFunction: utilcomp.SpecifiedResourceTypeAndNameCompletionFunc(f, []string{"cluster"}), RunE: func(_ *cobra.Command, args []string) error { if err := opts.Complete(f, args); err != nil { return err @@ -107,6 +109,7 @@ func NewCmdTaint(f util.Factory, parentCommand string) *cobra.Command { flags.BoolVar(&opts.overwrite, "overwrite", opts.overwrite, "If true, allow taints to be overwritten, otherwise reject taint updates that overwrite existing taints.") flags.BoolVar(&opts.DryRun, "dry-run", false, "Run the command in dry-run mode, without making any server requests.") + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) return cmd } diff --git a/pkg/karmadactl/token/token.go b/pkg/karmadactl/token/token.go index 69f052eaffe8..138358cb79e2 100644 --- a/pkg/karmadactl/token/token.go +++ b/pkg/karmadactl/token/token.go @@ -39,6 +39,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" tokenutil "github.com/karmada-io/karmada/pkg/karmadactl/util/bootstraptoken" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) var ( @@ -135,6 +136,7 @@ func NewCmdTokenCreate(f util.Factory, out io.Writer, tokenOpts *CommandTokenOpt cmd.Flags().StringSliceVar(&tokenOpts.Groups, "groups", tokenutil.DefaultGroups, fmt.Sprintf("Extra groups that this token will authenticate as when used for authentication. Must match %q", bootstrapapi.BootstrapGroupPattern)) cmd.Flags().StringVar(&tokenOpts.Description, "description", tokenOpts.Description, "A human friendly description of how this token is used.") + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) return cmd } @@ -158,6 +160,7 @@ func NewCmdTokenList(f util.Factory, out io.Writer, errW io.Writer, tokenOpts *C options.AddKubeConfigFlags(cmd.Flags()) + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) return cmd } @@ -190,6 +193,7 @@ func NewCmdTokenDelete(f util.Factory, out io.Writer, tokenOpts *CommandTokenOpt options.AddKubeConfigFlags(cmd.Flags()) + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) return cmd } diff --git a/pkg/karmadactl/top/top_node.go b/pkg/karmadactl/top/top_node.go index be72cc4f1e39..055ff08058d2 100644 --- a/pkg/karmadactl/top/top_node.go +++ b/pkg/karmadactl/top/top_node.go @@ -31,7 +31,6 @@ import ( "k8s.io/cli-runtime/pkg/genericiooptions" "k8s.io/client-go/kubernetes" cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" metricsapi "k8s.io/metrics/pkg/apis/metrics" @@ -42,6 +41,7 @@ import ( karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned" "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) // NodeOptions contains all the options for running the top-node cli command. @@ -104,13 +104,16 @@ func NewCmdTopNode(f util.Factory, parentCommand string, o *NodeOptions, streams Short: i18n.T("Display resource (CPU/memory) usage of nodes"), Long: topNodeLong, Example: fmt.Sprintf(topNodeExample, parentCommand), - ValidArgsFunction: completion.ResourceNameCompletionFunc(f, "node"), + ValidArgsFunction: utilcomp.ResourceNameCompletionFunc(f, "node"), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunTopNode(f)) }, Aliases: []string{"nodes", "no"}, + Annotations: map[string]string{ + "parent": "top", // used for completion code to set default operation scope. + }, } cmdutil.AddLabelSelectorFlagVar(cmd, &o.Selector) options.AddKubeConfigFlags(cmd.Flags()) @@ -120,6 +123,8 @@ func NewCmdTopNode(f util.Factory, parentCommand string, o *NodeOptions, streams cmd.Flags().BoolVar(&o.UseProtocolBuffers, "use-protocol-buffers", o.UseProtocolBuffers, "Enables using protocol-buffers to access Metrics API.") cmd.Flags().BoolVar(&o.ShowCapacity, "show-capacity", o.ShowCapacity, "Print node resources based on Capacity instead of Allocatable(default) of the nodes.") + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForClustersFlag(cmd) return cmd } diff --git a/pkg/karmadactl/top/top_pods.go b/pkg/karmadactl/top/top_pods.go index aeca43711a93..712fbb2a3cbc 100644 --- a/pkg/karmadactl/top/top_pods.go +++ b/pkg/karmadactl/top/top_pods.go @@ -33,7 +33,6 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" metricsapi "k8s.io/metrics/pkg/apis/metrics" @@ -44,6 +43,7 @@ import ( karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned" "github.com/karmada-io/karmada/pkg/karmadactl/options" "github.com/karmada-io/karmada/pkg/karmadactl/util" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" ) // PodOptions contains the options to the top command. @@ -115,13 +115,16 @@ func NewCmdTopPod(f util.Factory, parentCommand string, o *PodOptions, streams g Short: i18n.T("Display resource (CPU/memory) usage of pods of member clusters"), Long: topPodLong, Example: fmt.Sprintf(topPodExample, parentCommand), - ValidArgsFunction: completion.ResourceNameCompletionFunc(f, "pod"), + ValidArgsFunction: utilcomp.ResourceNameCompletionFunc(f, "pod"), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, cmd, args)) cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunTopPod(f)) }, Aliases: []string{"pods", "po"}, + Annotations: map[string]string{ + "parent": "top", // used for completion code to set default operation scope. + }, } cmdutil.AddLabelSelectorFlagVar(cmd, &o.LabelSelector) options.AddKubeConfigFlags(cmd.Flags()) @@ -134,6 +137,10 @@ func NewCmdTopPod(f util.Factory, parentCommand string, o *PodOptions, streams g cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If present, print output without headers.") cmd.Flags().BoolVar(&o.UseProtocolBuffers, "use-protocol-buffers", o.UseProtocolBuffers, "Enables using protocol-buffers to access Metrics API.") cmd.Flags().BoolVar(&o.Sum, "sum", o.Sum, "Print the sum of the resource usage") + + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) + utilcomp.RegisterCompletionFuncForNamespaceFlag(cmd, f) + utilcomp.RegisterCompletionFuncForClustersFlag(cmd) return cmd } diff --git a/pkg/karmadactl/unjoin/unjoin.go b/pkg/karmadactl/unjoin/unjoin.go index 3ca26807b1e4..4e0fa4450b0e 100644 --- a/pkg/karmadactl/unjoin/unjoin.go +++ b/pkg/karmadactl/unjoin/unjoin.go @@ -35,6 +35,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/options" cmdutil "github.com/karmada-io/karmada/pkg/karmadactl/util" "github.com/karmada-io/karmada/pkg/karmadactl/util/apiclient" + utilcomp "github.com/karmada-io/karmada/pkg/karmadactl/util/completion" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/names" ) @@ -86,6 +87,7 @@ func NewCmdUnjoin(f cmdutil.Factory, parentCommand string) *cobra.Command { opts.AddFlags(flags) options.AddKubeConfigFlags(flags) + utilcomp.RegisterCompletionFuncForKarmadaContextFlag(cmd) return cmd } diff --git a/pkg/karmadactl/util/completion/completion.go b/pkg/karmadactl/util/completion/completion.go new file mode 100644 index 000000000000..374fa591288c --- /dev/null +++ b/pkg/karmadactl/util/completion/completion.go @@ -0,0 +1,505 @@ +/* +Copyright 2024 The Karmada Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package completion + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/kubectl/pkg/cmd/apiresources" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/polymorphichelpers" + "k8s.io/kubectl/pkg/scheme" + + "github.com/karmada-io/karmada/pkg/karmadactl/get" + "github.com/karmada-io/karmada/pkg/karmadactl/options" + "github.com/karmada-io/karmada/pkg/karmadactl/util" +) + +var factory util.Factory + +// SetFactoryForCompletion Store the factory which is needed by the completion functions. +// Not all commands have access to the factory, so cannot pass it to the completion functions. +func SetFactoryForCompletion(f util.Factory) { + factory = f +} + +// ResourceTypeAndNameCompletionFunc Returns a completion function that completes resource types +// and resource names that match the toComplete prefix. It supports the / form. +func ResourceTypeAndNameCompletionFunc(f util.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return resourceTypeAndNameCompletionFunc(f, nil, true) +} + +// SpecifiedResourceTypeAndNameCompletionFunc Returns a completion function that completes resource +// types limited to the specified allowedTypes, and resource names that match the toComplete prefix. +// It allows for multiple resources. It supports the / form. +func SpecifiedResourceTypeAndNameCompletionFunc(f util.Factory, allowedTypes []string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return resourceTypeAndNameCompletionFunc(f, allowedTypes, true) +} + +// ResourceNameCompletionFunc Returns a completion function that completes as a first argument +// the resource names specified by the resourceType parameter, and which match the toComplete prefix. +// This function does NOT support the / form: it is meant to be used by commands +// that don't support that form. For commands that apply to pods and that support the / +// form, please use PodResourceNameCompletionFunc() +func ResourceNameCompletionFunc(f util.Factory, resourceType string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + if len(args) == 0 { + comps = CompGetResource(f, cmd, resourceType, toComplete) + } + return comps, cobra.ShellCompDirectiveNoFileComp + } +} + +// PodResourceNameCompletionFunc Returns a completion function that completes: +// 1- pod names that match the toComplete prefix +// 2- resource types containing pods which match the toComplete prefix +func PodResourceNameCompletionFunc(f util.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + directive := cobra.ShellCompDirectiveNoFileComp + if len(args) == 0 { + comps, directive = doPodResourceCompletion(f, cmd, toComplete) + } + return comps, directive + } +} + +// PodResourceNameAndContainerCompletionFunc Returns a completion function that completes, as a first argument: +// 1- pod names that match the toComplete prefix +// 2- resource types containing pods which match the toComplete prefix +// and as a second argument the containers within the specified pod. +func PodResourceNameAndContainerCompletionFunc(f util.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + directive := cobra.ShellCompDirectiveNoFileComp + if len(args) == 0 { + comps, directive = doPodResourceCompletion(f, cmd, toComplete) + } else if len(args) == 1 { + podName := convertResourceNameToPodName(f, args[0]) + comps = CompGetContainers(f, cmd, podName, toComplete) + } + return comps, directive + } +} + +// ContainerCompletionFunc Returns a completion function that completes the containers within the +// pod specified by the first argument. The resource containing the pod can be specified in +// the / form. +func ContainerCompletionFunc(f util.Factory) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + // We need the pod name to be able to complete the container names, it must be in args[0]. + // That first argument can also be of the form / so we need to convert it. + if len(args) > 0 { + podName := convertResourceNameToPodName(f, args[0]) + comps = CompGetContainers(f, cmd, podName, toComplete) + } + return comps, cobra.ShellCompDirectiveNoFileComp + } +} + +// CompGetResource gets the list of the resource specified which begin with `toComplete`. +func CompGetResource(f util.Factory, cmd *cobra.Command, resourceName string, toComplete string) []string { + template := "{{ range .items }}{{ .metadata.name }} {{ end }}" + return CompGetFromTemplate(&template, f, cmd, "", []string{resourceName}, toComplete) +} + +// CompGetContainers gets the list of containers of the specified pod which begin with `toComplete`. +func CompGetContainers(f util.Factory, cmd *cobra.Command, podName string, toComplete string) []string { + template := "{{ range .spec.initContainers }}{{ .name }} {{end}}{{ range .spec.containers }}{{ .name }} {{ end }}" + return CompGetFromTemplate(&template, f, cmd, "", []string{"pod", podName}, toComplete) +} + +// CompGetFromTemplate executes a Get operation using the specified template and args and returns the results +// which begin with `toComplete`. +func CompGetFromTemplate(template *string, f util.Factory, cmd *cobra.Command, namespace string, args []string, toComplete string) []string { + buf := new(bytes.Buffer) + streams := genericiooptions.IOStreams{In: os.Stdin, Out: buf, ErrOut: io.Discard} + o := get.NewCommandGetOptions(streams) + + // Get the list of names of the specified resource + o.PrintFlags.TemplateFlags.GoTemplatePrintFlags.TemplateArgument = template + format := "go-template" + o.PrintFlags.OutputFormat = &format + + // Do the steps Complete() would have done. + // We cannot actually call Complete() or Validate() as these function check for + // the presence of flags, which, in our case won't be there + if namespace != "" { + o.Namespace = namespace + o.ExplicitNamespace = true + } else { + var err error + o.Namespace, o.ExplicitNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return nil + } + } + + o.ToPrinter = func(_ *meta.RESTMapping, _ *bool, _ bool, _ bool) (printers.ResourcePrinterFunc, error) { + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return nil, err + } + return printer.PrintObj, nil + } + + o.OperationScope = options.KarmadaControlPlane + // currently, the operation-scope of command `top`, `logs` and `promote` is members. + if cmd.Annotations["parent"] == "top" || cmd.Name() == "logs" || cmd.Name() == "promote" { + o.OperationScope = options.Members + } + operationScopeFlag := cmd.Flag("operation-scope") + if operationScopeFlag != nil { + o.OperationScope = options.OperationScope(operationScopeFlag.Value.String()) + } + o.Clusters, _ = cmd.Flags().GetStringSlice("clusters") + clusterFlag := cmd.Flag("cluster") + if clusterFlag != nil { + cluster := clusterFlag.Value.String() + if len(cluster) != 0 { + o.Clusters = []string{cluster} + } + } + + o.KarmadaClient, _ = f.KarmadaClientSet() + if err := o.HandleClusterScopeFlags(); err != nil { + return nil + } + + if err := o.Run(f, args); err != nil { + return nil + } + var comps []string + resources := strings.Split(buf.String(), " ") + for _, res := range resources { + if res != "" && strings.HasPrefix(res, toComplete) { + comps = append(comps, res) + } + } + return comps +} + +// ListContextsInConfig returns a list of context names which begin with `toComplete` +func ListContextsInConfig(toComplete string) []string { + config, err := factory.ToRawKubeConfigLoader().RawConfig() + if err != nil { + return nil + } + var ret []string + for name := range config.Contexts { + if strings.HasPrefix(name, toComplete) { + ret = append(ret, name) + } + } + return ret +} + +// ListClustersInConfig returns a list of cluster names which begin with `toComplete` +func ListClustersInConfig(toComplete string) []string { + set, err := factory.KarmadaClientSet() + if err != nil { + return nil + } + + list, err := set.ClusterV1alpha1().Clusters().List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil + } + + var ret []string + for _, cluster := range list.Items { + if strings.HasPrefix(cluster.Name, toComplete) { + ret = append(ret, cluster.Name) + } + } + return ret +} + +// compGetResourceList returns the list of api resources which begin with `toComplete`. +func compGetResourceList(restClientGetter genericclioptions.RESTClientGetter, cmd *cobra.Command, toComplete string) []string { + buf := new(bytes.Buffer) + streams := genericiooptions.IOStreams{In: os.Stdin, Out: buf, ErrOut: io.Discard} + + // TODO: Using karmadactlapiresources.CommandAPIResourcesOptions to adapt to the operation scope. + o := apiresources.NewAPIResourceOptions(streams) + + if err := o.Complete(restClientGetter, cmd, nil); err != nil { + return nil + } + + // Get the list of resources + o.Output = "name" + o.Cached = true + o.Verbs = []string{"get"} + // TODO:Should set --request-timeout=5s + + // Ignore errors as the output may still be valid + if err := o.RunAPIResources(); err != nil { + return nil + } + + // Resources can be a comma-separated list. The last element is then + // the one we should complete. For example if toComplete=="pods,secre" + // we should return "pods,secrets" + prefix := "" + suffix := toComplete + lastIdx := strings.LastIndex(toComplete, ",") + if lastIdx != -1 { + prefix = toComplete[0 : lastIdx+1] + suffix = toComplete[lastIdx+1:] + } + var comps []string + resources := strings.Split(buf.String(), "\n") + for _, res := range resources { + if res != "" && strings.HasPrefix(res, suffix) { + comps = append(comps, fmt.Sprintf("%s%s", prefix, res)) + } + } + return comps +} + +// resourceTypeAndNameCompletionFunc Returns a completion function that completes resource types +// and resource names that match the toComplete prefix. It supports the / form. +// +//nolint:gocyclo +func resourceTypeAndNameCompletionFunc(f util.Factory, allowedTypes []string, allowRepeat bool) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + directive := cobra.ShellCompDirectiveNoFileComp + + if len(args) > 0 && !strings.Contains(args[0], "/") { + // The first argument is of the form (e.g., pods) + // All following arguments should be a resource name. + if allowRepeat || len(args) == 1 { + comps = CompGetResource(f, cmd, args[0], toComplete) + + // Remove choices already on the command-line + if len(args) > 1 { + comps = cmdutil.Difference(comps, args[1:]) + } + } + } else { + slashIdx := strings.Index(toComplete, "/") + if slashIdx == -1 { + if len(args) == 0 { + // We are completing the first argument. We default to the normal + // form (not the form /). + // So we suggest resource types and let the shell add a space after + // the completion. + if len(allowedTypes) == 0 { + comps = compGetResourceList(f, cmd, toComplete) + } else { + for _, c := range allowedTypes { + if strings.HasPrefix(c, toComplete) { + comps = append(comps, c) + } + } + } + } else { + // Here we know the first argument contains a / (/). + // All other arguments must also use that form. + if allowRepeat { + // Since toComplete does not already contain a / we know we are completing a + // resource type. Disable adding a space after the completion, and add the / + directive |= cobra.ShellCompDirectiveNoSpace + + if len(allowedTypes) == 0 { + typeComps := compGetResourceList(f, cmd, toComplete) + for _, c := range typeComps { + comps = append(comps, fmt.Sprintf("%s/", c)) + } + } else { + for _, c := range allowedTypes { + if strings.HasPrefix(c, toComplete) { + comps = append(comps, fmt.Sprintf("%s/", c)) + } + } + } + } + } + } else { + // We are completing an argument of the form / + // and since the / is already present, we are completing the resource name. + if allowRepeat || len(args) == 0 { + resourceType := toComplete[:slashIdx] + toComplete = toComplete[slashIdx+1:] + nameComps := CompGetResource(f, cmd, resourceType, toComplete) + for _, c := range nameComps { + comps = append(comps, fmt.Sprintf("%s/%s", resourceType, c)) + } + + // Remove choices already on the command-line. + if len(args) > 0 { + comps = cmdutil.Difference(comps, args[0:]) + } + } + } + } + return comps, directive + } +} + +// doPodResourceCompletion Returns completions of: +// 1- pod names that match the toComplete prefix +// 2- resource types containing pods which match the toComplete prefix +func doPodResourceCompletion(f util.Factory, cmd *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + directive := cobra.ShellCompDirectiveNoFileComp + slashIdx := strings.Index(toComplete, "/") + if slashIdx == -1 { + // Standard case, complete pod names + comps = CompGetResource(f, cmd, "pod", toComplete) + + // Also include resource choices for the / form, + // but only for resources that contain pods + resourcesWithPods := []string{ + "daemonsets", + "deployments", + "pods", + "jobs", + "replicasets", + "replicationcontrollers", + "services", + "statefulsets"} + + if len(comps) == 0 { + // If there are no pods to complete, we will only be completing + // /. We should disable adding a space after the /. + directive |= cobra.ShellCompDirectiveNoSpace + } + + for _, resource := range resourcesWithPods { + if strings.HasPrefix(resource, toComplete) { + comps = append(comps, fmt.Sprintf("%s/", resource)) + } + } + } else { + // Dealing with the / form, use the specified resource type + resourceType := toComplete[:slashIdx] + toComplete = toComplete[slashIdx+1:] + nameComps := CompGetResource(f, cmd, resourceType, toComplete) + for _, c := range nameComps { + comps = append(comps, fmt.Sprintf("%s/%s", resourceType, c)) + } + } + return comps, directive +} + +// convertResourceNameToPodName Converts a resource name to a pod name. +// If the resource name is of the form /, we use +// polymorphichelpers.AttachablePodForObjectFn(), if not, the resource name +// is already a pod name. +func convertResourceNameToPodName(f cmdutil.Factory, resourceName string) string { + var podName string + if !strings.Contains(resourceName, "/") { + // When we don't have the / form, the resource name is the pod name + podName = resourceName + } else { + // if the resource name is of the form /, we need to convert it to a pod name + ns, _, err := f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return "" + } + + resourceWithPod, err := f.NewBuilder(). + WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). + ContinueOnError(). + NamespaceParam(ns).DefaultNamespace(). + ResourceNames("pods", resourceName). + Do().Object() + if err != nil { + return "" + } + + // For shell completion, use a short timeout + forwardablePod, err := polymorphichelpers.AttachablePodForObjectFn(f, resourceWithPod, 100*time.Millisecond) + if err != nil { + return "" + } + podName = forwardablePod.Name + } + return podName +} + +// RegisterCompletionFuncForNamespaceFlag registers CompletionFunc for flag namespace. +func RegisterCompletionFuncForNamespaceFlag(cmd *cobra.Command, f util.Factory) { + cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( + "namespace", + func(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return CompGetResource(f, cmd, "namespace", toComplete), cobra.ShellCompDirectiveNoFileComp + })) +} + +// RegisterCompletionFuncForClusterFlag registers CompletionFunc for flag cluster. +func RegisterCompletionFuncForClusterFlag(cmd *cobra.Command) { + cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( + "cluster", + func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return ListClustersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp + })) +} + +// RegisterCompletionFuncForClustersFlag registers CompletionFunc for flag clusters. +func RegisterCompletionFuncForClustersFlag(cmd *cobra.Command) { + cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( + "clusters", + func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return ListClustersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp + })) +} + +// RegisterCompletionFuncForKarmadaContextFlag registers CompletionFunc for flag karmada-context. +func RegisterCompletionFuncForKarmadaContextFlag(cmd *cobra.Command) { + cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( + "karmada-context", + func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return ListContextsInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp + })) +} + +// RegisterCompletionFuncForOperationScopeFlag registers CompletionFunc for flag operation-scope. +func RegisterCompletionFuncForOperationScopeFlag(cmd *cobra.Command, supportScope ...options.OperationScope) { + cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc( + "operation-scope", + func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var ret []string + + if len(supportScope) == 0 { + supportScope = []options.OperationScope{options.KarmadaControlPlane, options.Members, options.All} + } + for _, scope := range supportScope { + if strings.HasPrefix(scope.String(), toComplete) { + ret = append(ret, scope.String()) + } + } + return ret, cobra.ShellCompDirectiveNoFileComp + })) +}