mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 23:37:01 +00:00
Merge pull request #105867 from VilledeMontreal/feature/compPlugins
Shell completion for plugins
This commit is contained in:
commit
797536fc76
@ -279,8 +279,15 @@ func NewKubectlCommand(o KubectlOptions) *cobra.Command {
|
||||
Run: runHelp,
|
||||
// Hook before and after Run initialize and write profiles to disk,
|
||||
// respectively.
|
||||
PersistentPreRunE: func(*cobra.Command, []string) error {
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
rest.SetDefaultWarningHandler(warningHandler)
|
||||
|
||||
if cmd.Name() == cobra.ShellCompRequestCmd {
|
||||
// This is the __complete or __completeNoDesc command which
|
||||
// indicates shell completion has been requested.
|
||||
plugin.SetupPluginCompletion(cmd, args)
|
||||
}
|
||||
|
||||
return initProfiling()
|
||||
},
|
||||
PersistentPostRunE: func(*cobra.Command, []string) error {
|
||||
|
@ -115,58 +115,27 @@ func (o *PluginListOptions) Complete(cmd *cobra.Command) error {
|
||||
}
|
||||
|
||||
func (o *PluginListOptions) Run() error {
|
||||
pluginsFound := false
|
||||
isFirstFile := true
|
||||
pluginErrors := []error{}
|
||||
pluginWarnings := 0
|
||||
plugins, pluginErrors := o.ListPlugins()
|
||||
|
||||
for _, dir := range uniquePathsList(o.PluginPaths) {
|
||||
if len(strings.TrimSpace(dir)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
if _, ok := err.(*os.PathError); ok {
|
||||
fmt.Fprintf(o.ErrOut, "Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err)
|
||||
continue
|
||||
}
|
||||
|
||||
pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) {
|
||||
continue
|
||||
}
|
||||
|
||||
if isFirstFile {
|
||||
fmt.Fprintf(o.Out, "The following compatible plugins are available:\n\n")
|
||||
pluginsFound = true
|
||||
isFirstFile = false
|
||||
}
|
||||
|
||||
pluginPath := f.Name()
|
||||
if !o.NameOnly {
|
||||
pluginPath = filepath.Join(dir, pluginPath)
|
||||
}
|
||||
|
||||
fmt.Fprintf(o.Out, "%s\n", pluginPath)
|
||||
if errs := o.Verifier.Verify(filepath.Join(dir, f.Name())); len(errs) != 0 {
|
||||
for _, err := range errs {
|
||||
fmt.Fprintf(o.ErrOut, " - %s\n", err)
|
||||
pluginWarnings++
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(plugins) > 0 {
|
||||
fmt.Fprintf(o.Out, "The following compatible plugins are available:\n\n")
|
||||
} else {
|
||||
pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kubectl plugins in your PATH"))
|
||||
}
|
||||
|
||||
if !pluginsFound {
|
||||
pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kubectl plugins in your PATH"))
|
||||
pluginWarnings := 0
|
||||
for _, pluginPath := range plugins {
|
||||
if o.NameOnly {
|
||||
fmt.Fprintf(o.Out, "%s\n", filepath.Base(pluginPath))
|
||||
} else {
|
||||
fmt.Fprintf(o.Out, "%s\n", pluginPath)
|
||||
}
|
||||
if errs := o.Verifier.Verify(pluginPath); len(errs) != 0 {
|
||||
for _, err := range errs {
|
||||
fmt.Fprintf(o.ErrOut, " - %s\n", err)
|
||||
pluginWarnings++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pluginWarnings > 0 {
|
||||
@ -187,6 +156,42 @@ func (o *PluginListOptions) Run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListPlugins returns list of plugin paths.
|
||||
func (o *PluginListOptions) ListPlugins() ([]string, []error) {
|
||||
plugins := []string{}
|
||||
errors := []error{}
|
||||
|
||||
for _, dir := range uniquePathsList(o.PluginPaths) {
|
||||
if len(strings.TrimSpace(dir)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
if _, ok := err.(*os.PathError); ok {
|
||||
fmt.Fprintf(o.ErrOut, "Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err)
|
||||
continue
|
||||
}
|
||||
|
||||
errors = append(errors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) {
|
||||
continue
|
||||
}
|
||||
|
||||
plugins = append(plugins, filepath.Join(dir, f.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
return plugins, errors
|
||||
}
|
||||
|
||||
// pathVerifier receives a path and determines if it is valid or not
|
||||
type PathVerifier interface {
|
||||
// Verify determines if a given path is valid
|
||||
|
231
staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go
Normal file
231
staging/src/k8s.io/kubectl/pkg/cmd/plugin/plugin_completion.go
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
if len(args) > 0 {
|
||||
if strings.HasPrefix(args[0], "-") {
|
||||
// Plugins are not supported if the first argument is a flag,
|
||||
// so no need to add them in that case.
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) == 1 {
|
||||
// We are completing a subcommand at the first level so
|
||||
// we should include all plugins names.
|
||||
addPluginCommands(cmd)
|
||||
return
|
||||
}
|
||||
|
||||
// We have more than one argument.
|
||||
// Check if we know the first level subcommand.
|
||||
// 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() {
|
||||
if args[0] == subCmd.Name() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// We don't know the subcommand for which completion
|
||||
// is being called: it could be a plugin.
|
||||
//
|
||||
// When using a plugin, the kubectl global flags are not supported.
|
||||
// Therefore, when doing completion, we need to remove these flags
|
||||
// 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()
|
||||
cobra.CompDebugln("Cleared global flags for plugin completion", true)
|
||||
|
||||
addPluginCommands(cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := genericclioptions.IOStreams{
|
||||
In: &bytes.Buffer{},
|
||||
Out: ioutil.Discard,
|
||||
ErrOut: ioutil.Discard,
|
||||
}
|
||||
|
||||
o := &PluginListOptions{IOStreams: streams}
|
||||
o.Complete(kubectl)
|
||||
plugins, _ := o.ListPlugins()
|
||||
|
||||
for _, plugin := range plugins {
|
||||
plugin = filepath.Base(plugin)
|
||||
args := []string{}
|
||||
|
||||
// Plugins are named "kubectl-<name>" or with more - such as
|
||||
// "kubectl-<name>-<subcmd1>..."
|
||||
for _, arg := range strings.Split(plugin, "-")[1:] {
|
||||
// Underscores (_) in plugin's filename are replaced with dashes(-)
|
||||
// e.g. foo_bar -> foo-bar
|
||||
args = append(args, strings.Replace(arg, "_", "-", -1))
|
||||
}
|
||||
|
||||
// In order to avoid that the same plugin command is added more than once,
|
||||
// find the lowest command given args from the root command
|
||||
parentCmd, remainingArgs, _ := kubectl.Find(args)
|
||||
if parentCmd == nil {
|
||||
parentCmd = kubectl
|
||||
}
|
||||
|
||||
for _, remainingArg := range remainingArgs {
|
||||
cmd := &cobra.Command{
|
||||
Use: remainingArg,
|
||||
// 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),
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pluginCompletion deals with shell completion beyond the plugin name, it allows to complete
|
||||
// plugin arguments and flags.
|
||||
// It will look on $PATH for a specific executable file that will provide completions
|
||||
// for the plugin in question.
|
||||
//
|
||||
// When called, this completion executable should print the completion choices to stdout.
|
||||
// The arguments passed to the executable file will be the arguments for the plugin currently
|
||||
// on the command-line. For example, if a user types:
|
||||
//
|
||||
// kubectl myplugin arg1 arg2 a<TAB>
|
||||
//
|
||||
// the completion executable will be called with arguments: "arg1" "arg2" "a".
|
||||
// And if a user types:
|
||||
//
|
||||
// kubectl myplugin arg1 arg2 <TAB>
|
||||
//
|
||||
// the completion executable will be called with arguments: "arg1" "arg2" "". Notice the empty
|
||||
// last argument which indicates that a new word should be completed but that the user has not
|
||||
// typed anything for it yet.
|
||||
//
|
||||
// Kubectl's plugin completion logic supports Cobra's ShellCompDirective system. This means a plugin
|
||||
// can optionally print :<value of a shell completion directive> as its very last line to provide
|
||||
// directives to the shell on how to perform completion. If this directive is not present, the
|
||||
// cobra.ShellCompDirectiveDefault will be used. Please see Cobra's documentation for more details:
|
||||
// https://github.com/spf13/cobra/blob/master/shell_completions.md#dynamic-completion-of-nouns
|
||||
//
|
||||
// The completion executable should be named kubectl_complete-<plugin>. For example, for a plugin
|
||||
// named kubectl-get_all, the completion file should be named kubectl_complete-get_all. The completion
|
||||
// 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)
|
||||
|
||||
path, found := lookupCompletionExec(pluginName)
|
||||
if !found {
|
||||
cobra.CompDebugln(fmt.Sprintf("Plugin %s does not provide a matching completion executable", pluginName), true)
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
|
||||
args = append(args, toComplete)
|
||||
cobra.CompDebugln(fmt.Sprintf("About to call: %s %s", path, strings.Join(args, " ")), true)
|
||||
return getPluginCompletions(path, args, os.Environ())
|
||||
}
|
||||
|
||||
// lookupCompletionExec will look for the existence of an executable
|
||||
// that can provide completion for the given plugin name.
|
||||
// The first filepath to match is returned, or a boolean false if
|
||||
// such an executable is not found.
|
||||
func lookupCompletionExec(pluginName string) (string, bool) {
|
||||
// Convert the plugin name into the plugin completion name by inserting "_complete" before the first -.
|
||||
// For example, convert kubectl-get_all to kubectl_complete-get_all
|
||||
pluginCompExec := strings.Replace(pluginName, "-", "_complete-", 1)
|
||||
cobra.CompDebugln(fmt.Sprintf("About to look for: %s", pluginCompExec), true)
|
||||
path, err := exec.LookPath(pluginCompExec)
|
||||
if err != nil || len(path) == 0 {
|
||||
return "", false
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
|
||||
// getPluginCompletions receives an executable's filepath, a slice
|
||||
// of arguments, and a slice of environment variables
|
||||
// to relay to the executable.
|
||||
// The executable is responsible for printing the completions of the
|
||||
// plugin for the current set of arguments.
|
||||
func getPluginCompletions(executablePath string, cmdArgs, environment []string) ([]string, cobra.ShellCompDirective) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
prog := exec.Command(executablePath, cmdArgs...)
|
||||
prog.Stdin = os.Stdin
|
||||
prog.Stdout = buf
|
||||
prog.Stderr = os.Stderr
|
||||
prog.Env = environment
|
||||
|
||||
var comps []string
|
||||
directive := cobra.ShellCompDirectiveDefault
|
||||
if err := prog.Run(); err == nil {
|
||||
for _, comp := range strings.Split(buf.String(), "\n") {
|
||||
// Remove any empty lines
|
||||
if len(comp) > 0 {
|
||||
comps = append(comps, comp)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the last line of output is of the form :<integer>, which
|
||||
// indicates a Cobra ShellCompDirective. We do this for plugins
|
||||
// that use Cobra or the ones that wish to use this directive to
|
||||
// communicate a special behavior for the shell.
|
||||
if len(comps) > 0 {
|
||||
lastLine := comps[len(comps)-1]
|
||||
if len(lastLine) > 1 && lastLine[0] == ':' {
|
||||
if strInt, err := strconv.Atoi(lastLine[1:]); err == nil {
|
||||
directive = cobra.ShellCompDirective(strInt)
|
||||
comps = comps[:len(comps)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return comps, directive
|
||||
}
|
@ -20,6 +20,8 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -178,6 +180,34 @@ func TestPluginPathsAreValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPlugins(t *testing.T) {
|
||||
pluginPath, _ := filepath.Abs("./testdata")
|
||||
expectPlugins := []string{
|
||||
filepath.Join(pluginPath, "kubectl-foo"),
|
||||
filepath.Join(pluginPath, "kubectl-version"),
|
||||
}
|
||||
|
||||
verifier := newFakePluginPathVerifier()
|
||||
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams()
|
||||
pluginPaths := []string{pluginPath}
|
||||
|
||||
o := &PluginListOptions{
|
||||
Verifier: verifier,
|
||||
IOStreams: ioStreams,
|
||||
|
||||
PluginPaths: pluginPaths,
|
||||
}
|
||||
|
||||
plugins, errs := o.ListPlugins()
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("unexpected errors: %v", errs)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectPlugins, plugins) {
|
||||
t.Fatalf("saw unexpected plugins. Expecting %v, got %v", expectPlugins, plugins)
|
||||
}
|
||||
}
|
||||
|
||||
type duplicatePathError struct {
|
||||
path string
|
||||
}
|
||||
|
@ -58,6 +58,29 @@ that kubectl points to.
|
||||
It can also be used as a means of showcasing usage of the cli-runtime set of utilities to aid in
|
||||
third-party plugin development.
|
||||
|
||||
## Shell completion
|
||||
|
||||
This plugin supports shell completion when used through kubectl. To enable shell completion for the plugin
|
||||
you must copy the file `./kubectl_complete-ns` somewhere on `$PATH` and give it executable permissions.
|
||||
|
||||
The `./kubectl_complete-ns` script shows a hybrid approach to providing completions:
|
||||
1. it uses the builtin `__complete` command provided by [Cobra](https://github.com/spf13/cobra) for flags
|
||||
1. it calls `kubectl` to obtain the list of namespaces to complete arguments (note that a more elegant approach would be to have the `kubectl-ns` program itself provide completion of arguments by implementing Cobra's `ValidArgsFunction` to fetch the list of namespaces, but it would then be a less varied example)
|
||||
|
||||
One can then do things like:
|
||||
```
|
||||
$ kubectl ns <TAB>
|
||||
default kube-node-lease kube-public kube-system
|
||||
|
||||
$ kubectl ns --<TAB>
|
||||
--as -- Username to impersonate for the operation. User could be a regular user or a service account in a namespace.
|
||||
--as-group -- Group to impersonate for the operation, this flag can be repeated to specify multiple groups.
|
||||
--as-uid -- UID to impersonate for the operation.
|
||||
--cache-dir -- Default cache directory
|
||||
[...]
|
||||
```
|
||||
|
||||
Note: kubectl v1.26 or higher is required for shell completion to work for plugins.
|
||||
## Cleanup
|
||||
|
||||
You can "uninstall" this plugin from kubectl by simply removing it from your PATH:
|
||||
|
24
staging/src/k8s.io/sample-cli-plugin/kubectl_complete-ns
Executable file
24
staging/src/k8s.io/sample-cli-plugin/kubectl_complete-ns
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# If we are completing a flag, use Cobra's builtin completion system.
|
||||
# To know if we are completing a flag we need the last argument starts with a `-` and does not contain an `=`
|
||||
args=("$@")
|
||||
lastArg=${args[((${#args[@]}-1))]}
|
||||
if [[ "$lastArg" == -* ]]; then
|
||||
if [[ "$lastArg" != *=* ]]; then
|
||||
kubectl ns __complete "$@"
|
||||
fi
|
||||
else
|
||||
# TODO Make sure we are not completing the value of a flag.
|
||||
# TODO Only complete a single argument.
|
||||
# Both are pretty hard to do in a shell script. The better way to do this would be to let
|
||||
# Cobra do all the completions by using `cobra.ValidArgsFunction` in the program.
|
||||
# But the below, although imperfect, is a nice example for plugins that don't use Cobra.
|
||||
|
||||
# We are probably completing an argument. This plugin only accepts namespaces, let's fetch them.
|
||||
kubectl get namespaces --output go-template='{{ range .items }}{{ .metadata.name }}{{"\n"}}{{ end }}'
|
||||
|
||||
# Turn off file completion. See the ShellCompDirective documentation within
|
||||
# https://github.com/spf13/cobra/blob/main/shell_completions.md#completion-of-nouns
|
||||
echo :4
|
||||
fi
|
Loading…
Reference in New Issue
Block a user