From d7b7a85fbc0b3831cbc6a750a47dfdcdf777d519 Mon Sep 17 00:00:00 2001 From: Matthias Riegler Date: Wed, 6 Sep 2023 18:12:52 +0200 Subject: [PATCH] feat: make user-defined plugins discoverable with e.g. kubectl help (#116752) * feat: make user-defined plugins discoverable with e.g. kubectl help Signed-off-by: Matthias Riegler * fix: make help text localizable & rename it Signed-off-by: Matthias Riegler * chore: address CRs, cleanup Signed-off-by: Matthias Riegler * fix: plugin execution Signed-off-by: Matthias Riegler --------- Signed-off-by: Matthias Riegler --- staging/src/k8s.io/kubectl/pkg/cmd/cmd.go | 6 ++ .../pkg/cmd/plugin/plugin_completion.go | 60 +++++++++++++------ 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go b/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go index 1813276ac14..6b3a84d50fb 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/cmd.go @@ -469,6 +469,12 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command { filters = append(filters, alpha.Name()) } + // Add plugin command group to the list of command groups. + // The commands are only injected for the scope of showing help and completion, they are not + // invoked directly. + pluginCommandGroup := plugin.GetPluginCommandGroup(cmds) + groups = append(groups, pluginCommandGroup) + templates.ActsAsRootCommand(cmds, filters, groups...) utilcomp.SetFactoryForCompletion(f) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go index 4bc008bd35d..9635fc0b6a3 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go @@ -27,14 +27,24 @@ import ( "strings" "github.com/spf13/cobra" - - "k8s.io/cli-runtime/pkg/genericiooptions" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" ) +func GetPluginCommandGroup(kubectl *cobra.Command) templates.CommandGroup { + // Find root level + return templates.CommandGroup{ + Message: i18n.T("Subcommands provided by plugins:"), + Commands: registerPluginCommands(kubectl, false), + } +} + // SetupPluginCompletion adds a Cobra command to the command tree for each // plugin. This is only done when performing shell completion that relate // to plugins. func SetupPluginCompletion(cmd *cobra.Command, args []string) { + kubectl := cmd.Root() if len(args) > 0 { if strings.HasPrefix(args[0], "-") { // Plugins are not supported if the first argument is a flag, @@ -45,7 +55,7 @@ func SetupPluginCompletion(cmd *cobra.Command, args []string) { if len(args) == 1 { // We are completing a subcommand at the first level so // we should include all plugins names. - addPluginCommands(cmd) + registerPluginCommands(kubectl, true) return } @@ -54,7 +64,7 @@ func SetupPluginCompletion(cmd *cobra.Command, args []string) { // If we don't it could be a plugin and we'll need to add // the plugin commands for completion to work. found := false - for _, subCmd := range cmd.Root().Commands() { + for _, subCmd := range kubectl.Commands() { if args[0] == subCmd.Name() { found = true break @@ -70,19 +80,20 @@ func SetupPluginCompletion(cmd *cobra.Command, args []string) { // to avoid them being included in the completion choices. // This must be done *before* adding the plugin commands so that // when creating those plugin commands, the flags don't exist. - cmd.Root().ResetFlags() + kubectl.ResetFlags() cobra.CompDebugln("Cleared global flags for plugin completion", true) - addPluginCommands(cmd) + registerPluginCommands(kubectl, true) } } } -// addPluginCommand adds a Cobra command to the command tree -// for each plugin so that the completion logic knows about the plugins -func addPluginCommands(cmd *cobra.Command) { - kubectl := cmd.Root() - streams := genericiooptions.IOStreams{ +// registerPluginCommand allows adding Cobra command to the command tree or extracting them for usage in +// e.g. the help function or for registering the completion function +func registerPluginCommands(kubectl *cobra.Command, list bool) (cmds []*cobra.Command) { + userDefinedCommands := []*cobra.Command{} + + streams := genericclioptions.IOStreams{ In: &bytes.Buffer{}, Out: io.Discard, ErrOut: io.Discard, @@ -98,10 +109,18 @@ func addPluginCommands(cmd *cobra.Command) { // Plugins are named "kubectl-" or with more - such as // "kubectl--..." - for _, arg := range strings.Split(plugin, "-")[1:] { + rawPluginArgs := strings.Split(plugin, "-")[1:] + pluginArgs := rawPluginArgs[:1] + if list { + pluginArgs = rawPluginArgs + } + + // Iterate through all segments, for kubectl-my_plugin-sub_cmd, we will end up with + // two iterations: one for my_plugin and one for sub_cmd. + for _, arg := range pluginArgs { // Underscores (_) in plugin's filename are replaced with dashes(-) // e.g. foo_bar -> foo-bar - args = append(args, strings.Replace(arg, "_", "-", -1)) + args = append(args, strings.ReplaceAll(arg, "_", "-")) } // In order to avoid that the same plugin command is added more than once, @@ -117,17 +136,24 @@ func addPluginCommands(cmd *cobra.Command) { // Add a description that will be shown with completion choices. // Make each one different by including the plugin name to avoid // all plugins being grouped in a single line during completion for zsh. - Short: fmt.Sprintf("The command %s is a plugin installed by the user", remainingArg), + Short: fmt.Sprintf(i18n.T("The command %s is a plugin installed by the user"), remainingArg), DisableFlagParsing: true, // Allow plugins to provide their own completion choices ValidArgsFunction: pluginCompletion, // A Run is required for it to be a valid command Run: func(cmd *cobra.Command, args []string) {}, } - parentCmd.AddCommand(cmd) - parentCmd = cmd + // Add the plugin command to the list of user defined commands + userDefinedCommands = append(userDefinedCommands, cmd) + + if list { + parentCmd.AddCommand(cmd) + parentCmd = cmd + } } } + + return userDefinedCommands } // pluginCompletion deals with shell completion beyond the plugin name, it allows to complete @@ -161,7 +187,7 @@ func addPluginCommands(cmd *cobra.Command) { // executable must have executable permissions set on it and must be on $PATH. func pluginCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { // Recreate the plugin name from the commandPath - pluginName := strings.Replace(strings.Replace(cmd.CommandPath(), "-", "_", -1), " ", "-", -1) + pluginName := strings.ReplaceAll(strings.ReplaceAll(cmd.CommandPath(), "-", "_"), " ", "-") path, found := lookupCompletionExec(pluginName) if !found {