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())
|
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)
|
||||||
|
|
|
@ -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) {},
|
||||||
}
|
}
|
||||||
|
// Add the plugin command to the list of user defined commands
|
||||||
|
userDefinedCommands = append(userDefinedCommands, cmd)
|
||||||
|
|
||||||
|
if list {
|
||||||
parentCmd.AddCommand(cmd)
|
parentCmd.AddCommand(cmd)
|
||||||
parentCmd = 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 {
|
||||||
|
|
Loading…
Reference in New Issue