Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make user-defined plugins discoverable with e.g. kubectl help #116752

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions staging/src/k8s.io/kubectl/pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 43 additions & 17 deletions staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down