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:
parent
a51ca7c43d
commit
90963a2f06
|
@ -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)
|
||||
|
|
|
@ -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-<name>" or with more - such as
|
||||
// "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(-)
|
||||
// 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 {
|
||||
|
|
Loading…
Reference in New Issue