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 <matthias.riegler@ankorstore.com>

* fix: make help text localizable & rename it

Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com>

* chore: address CRs, cleanup

Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com>

* fix: plugin execution

Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com>

---------

Signed-off-by: Matthias Riegler <matthias.riegler@ankorstore.com>

Kubernetes-commit: d7b7a85fbc0b3831cbc6a750a47dfdcdf777d519
This commit is contained in:
Matthias Riegler 2023-09-06 18:12:52 +02:00 committed by Kubernetes Publisher
parent a51ca7c43d
commit 90963a2f06
2 changed files with 49 additions and 17 deletions

View File

@ -469,6 +469,12 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
filters = append(filters, alpha.Name()) 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...) templates.ActsAsRootCommand(cmds, filters, groups...)
utilcomp.SetFactoryForCompletion(f) utilcomp.SetFactoryForCompletion(f)

View File

@ -27,14 +27,24 @@ import (
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions" "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 // SetupPluginCompletion adds a Cobra command to the command tree for each
// plugin. This is only done when performing shell completion that relate // plugin. This is only done when performing shell completion that relate
// to plugins. // to plugins.
func SetupPluginCompletion(cmd *cobra.Command, args []string) { func SetupPluginCompletion(cmd *cobra.Command, args []string) {
kubectl := cmd.Root()
if len(args) > 0 { if len(args) > 0 {
if strings.HasPrefix(args[0], "-") { if strings.HasPrefix(args[0], "-") {
// Plugins are not supported if the first argument is a flag, // 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 { if len(args) == 1 {
// We are completing a subcommand at the first level so // We are completing a subcommand at the first level so
// we should include all plugins names. // we should include all plugins names.
addPluginCommands(cmd) registerPluginCommands(kubectl, true)
return 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 // If we don't it could be a plugin and we'll need to add
// the plugin commands for completion to work. // the plugin commands for completion to work.
found := false found := false
for _, subCmd := range cmd.Root().Commands() { for _, subCmd := range kubectl.Commands() {
if args[0] == subCmd.Name() { if args[0] == subCmd.Name() {
found = true found = true
break break
@ -70,19 +80,20 @@ func SetupPluginCompletion(cmd *cobra.Command, args []string) {
// to avoid them being included in the completion choices. // to avoid them being included in the completion choices.
// This must be done *before* adding the plugin commands so that // This must be done *before* adding the plugin commands so that
// when creating those plugin commands, the flags don't exist. // when creating those plugin commands, the flags don't exist.
cmd.Root().ResetFlags() kubectl.ResetFlags()
cobra.CompDebugln("Cleared global flags for plugin completion", true) cobra.CompDebugln("Cleared global flags for plugin completion", true)
addPluginCommands(cmd) registerPluginCommands(kubectl, true)
} }
} }
} }
// addPluginCommand adds a Cobra command to the command tree // registerPluginCommand allows adding Cobra command to the command tree or extracting them for usage in
// for each plugin so that the completion logic knows about the plugins // e.g. the help function or for registering the completion function
func addPluginCommands(cmd *cobra.Command) { func registerPluginCommands(kubectl *cobra.Command, list bool) (cmds []*cobra.Command) {
kubectl := cmd.Root() userDefinedCommands := []*cobra.Command{}
streams := genericiooptions.IOStreams{
streams := genericclioptions.IOStreams{
In: &bytes.Buffer{}, In: &bytes.Buffer{},
Out: io.Discard, Out: io.Discard,
ErrOut: io.Discard, ErrOut: io.Discard,
@ -98,10 +109,18 @@ func addPluginCommands(cmd *cobra.Command) {
// Plugins are named "kubectl-<name>" or with more - such as // Plugins are named "kubectl-<name>" or with more - such as
// "kubectl-<name>-<subcmd1>..." // "kubectl-<name>-<subcmd1>..."
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(-) // Underscores (_) in plugin's filename are replaced with dashes(-)
// e.g. foo_bar -> foo-bar // 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, // 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. // Add a description that will be shown with completion choices.
// Make each one different by including the plugin name to avoid // Make each one different by including the plugin name to avoid
// all plugins being grouped in a single line during completion for zsh. // 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, DisableFlagParsing: true,
// Allow plugins to provide their own completion choices // Allow plugins to provide their own completion choices
ValidArgsFunction: pluginCompletion, ValidArgsFunction: pluginCompletion,
// A Run is required for it to be a valid command // A Run is required for it to be a valid command
Run: func(cmd *cobra.Command, args []string) {}, Run: func(cmd *cobra.Command, args []string) {},
} }
parentCmd.AddCommand(cmd) // Add the plugin command to the list of user defined commands
parentCmd = cmd 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 // 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. // 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) { func pluginCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Recreate the plugin name from the commandPath // 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) path, found := lookupCompletionExec(pluginName)
if !found { if !found {