mirror of https://github.com/k3s-io/k3s
add updated plugin mechanism
parent
d745508cc0
commit
4bdc636380
|
@ -243,6 +243,7 @@ docs/man/man1/kubectl-label.1
|
|||
docs/man/man1/kubectl-logs.1
|
||||
docs/man/man1/kubectl-options.1
|
||||
docs/man/man1/kubectl-patch.1
|
||||
docs/man/man1/kubectl-plugin-list.1
|
||||
docs/man/man1/kubectl-plugin.1
|
||||
docs/man/man1/kubectl-port-forward.1
|
||||
docs/man/man1/kubectl-proxy.1
|
||||
|
@ -346,6 +347,7 @@ docs/user-guide/kubectl/kubectl_logs.md
|
|||
docs/user-guide/kubectl/kubectl_options.md
|
||||
docs/user-guide/kubectl/kubectl_patch.md
|
||||
docs/user-guide/kubectl/kubectl_plugin.md
|
||||
docs/user-guide/kubectl/kubectl_plugin_list.md
|
||||
docs/user-guide/kubectl/kubectl_port-forward.md
|
||||
docs/user-guide/kubectl/kubectl_proxy.md
|
||||
docs/user-guide/kubectl/kubectl_replace.md
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
This file is autogenerated, but we've stopped checking such files into the
|
||||
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
|
||||
populate this file.
|
|
@ -0,0 +1,3 @@
|
|||
This file is autogenerated, but we've stopped checking such files into the
|
||||
repository to reduce the need for rebases. Please run hack/generate-docs.sh to
|
||||
populate this file.
|
|
@ -188,7 +188,6 @@ filegroup(
|
|||
"//pkg/kubectl/explain:all-srcs",
|
||||
"//pkg/kubectl/genericclioptions:all-srcs",
|
||||
"//pkg/kubectl/metricsutil:all-srcs",
|
||||
"//pkg/kubectl/plugins:all-srcs",
|
||||
"//pkg/kubectl/polymorphichelpers:all-srcs",
|
||||
"//pkg/kubectl/proxy:all-srcs",
|
||||
"//pkg/kubectl/scheme:all-srcs",
|
||||
|
|
|
@ -78,7 +78,6 @@ go_library(
|
|||
"//pkg/kubectl/genericclioptions/printers:go_default_library",
|
||||
"//pkg/kubectl/genericclioptions/resource:go_default_library",
|
||||
"//pkg/kubectl/metricsutil:go_default_library",
|
||||
"//pkg/kubectl/plugins:go_default_library",
|
||||
"//pkg/kubectl/polymorphichelpers:go_default_library",
|
||||
"//pkg/kubectl/proxy:go_default_library",
|
||||
"//pkg/kubectl/scheme:go_default_library",
|
||||
|
@ -171,7 +170,6 @@ go_test(
|
|||
"label_test.go",
|
||||
"logs_test.go",
|
||||
"patch_test.go",
|
||||
"plugin_test.go",
|
||||
"portforward_test.go",
|
||||
"replace_test.go",
|
||||
"rollingupdate_test.go",
|
||||
|
@ -202,7 +200,6 @@ go_test(
|
|||
"//pkg/kubectl/genericclioptions:go_default_library",
|
||||
"//pkg/kubectl/genericclioptions/printers:go_default_library",
|
||||
"//pkg/kubectl/genericclioptions/resource:go_default_library",
|
||||
"//pkg/kubectl/plugins:go_default_library",
|
||||
"//pkg/kubectl/polymorphichelpers:go_default_library",
|
||||
"//pkg/kubectl/scheme:go_default_library",
|
||||
"//pkg/kubectl/util/i18n:go_default_library",
|
||||
|
|
|
@ -21,6 +21,12 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
utilflag "k8s.io/apiserver/pkg/util/flag"
|
||||
|
@ -36,7 +42,6 @@ import (
|
|||
"k8s.io/kubernetes/pkg/kubectl/cmd/wait"
|
||||
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
||||
)
|
||||
|
||||
|
@ -257,7 +262,100 @@ var (
|
|||
)
|
||||
|
||||
func NewDefaultKubectlCommand() *cobra.Command {
|
||||
return NewKubectlCommand(os.Stdin, os.Stdout, os.Stderr)
|
||||
return NewDefaultKubectlCommandWithArgs(&defaultPluginHandler{}, os.Args, os.Stdin, os.Stdout, os.Stderr)
|
||||
}
|
||||
|
||||
func NewDefaultKubectlCommandWithArgs(pluginHandler PluginHandler, args []string, in io.Reader, out, errout io.Writer) *cobra.Command {
|
||||
cmd := NewKubectlCommand(in, out, errout)
|
||||
|
||||
if pluginHandler == nil {
|
||||
return cmd
|
||||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
cmdPathPieces := args[1:]
|
||||
|
||||
// only look for suitable extension executables if
|
||||
// the specified command does not already exist
|
||||
if _, _, err := cmd.Find(cmdPathPieces); err != nil {
|
||||
if err := handleEndpointExtensions(pluginHandler, cmdPathPieces); err != nil {
|
||||
fmt.Fprintf(errout, "%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// PluginHandler is capable of parsing command line arguments
|
||||
// and performing executable filename lookups to search
|
||||
// for valid plugin files, and execute found plugins.
|
||||
type PluginHandler interface {
|
||||
// Lookup receives a potential filename and returns
|
||||
// a full or relative path to an executable, if one
|
||||
// exists at the given filename, or an error.
|
||||
Lookup(filename string) (string, error)
|
||||
// Execute receives an executable's filepath, a slice
|
||||
// of arguments, and a slice of environment variables
|
||||
// to relay to the executable.
|
||||
Execute(executablePath string, cmdArgs, environment []string) error
|
||||
}
|
||||
|
||||
type defaultPluginHandler struct{}
|
||||
|
||||
// Lookup implements PluginHandler
|
||||
func (h *defaultPluginHandler) Lookup(filename string) (string, error) {
|
||||
// if on Windows, append the "exe" extension
|
||||
// to the filename that we are looking up.
|
||||
if runtime.GOOS == "windows" {
|
||||
filename = filename + ".exe"
|
||||
}
|
||||
|
||||
return exec.LookPath(filename)
|
||||
}
|
||||
|
||||
// Execute implements PluginHandler
|
||||
func (h *defaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error {
|
||||
return syscall.Exec(executablePath, cmdArgs, environment)
|
||||
}
|
||||
|
||||
func handleEndpointExtensions(pluginHandler PluginHandler, cmdArgs []string) error {
|
||||
remainingArgs := []string{} // all "non-flag" arguments
|
||||
|
||||
for idx := range cmdArgs {
|
||||
if strings.HasPrefix(cmdArgs[idx], "-") {
|
||||
break
|
||||
}
|
||||
remainingArgs = append(remainingArgs, strings.Replace(cmdArgs[idx], "-", "_", -1))
|
||||
}
|
||||
|
||||
foundBinaryPath := ""
|
||||
|
||||
// attempt to find binary, starting at longest possible name with given cmdArgs
|
||||
for len(remainingArgs) > 0 {
|
||||
path, err := pluginHandler.Lookup(fmt.Sprintf("kubectl-%s", strings.Join(remainingArgs, "-")))
|
||||
if err != nil || len(path) == 0 {
|
||||
remainingArgs = remainingArgs[:len(remainingArgs)-1]
|
||||
continue
|
||||
}
|
||||
|
||||
foundBinaryPath = path
|
||||
break
|
||||
}
|
||||
|
||||
if len(foundBinaryPath) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// invoke cmd binary relaying the current environment and args given
|
||||
// remainingArgs will always have at least one element.
|
||||
// execve will make remainingArgs[0] the "binary name".
|
||||
if err := pluginHandler.Execute(foundBinaryPath, append([]string{foundBinaryPath}, cmdArgs[len(remainingArgs):]...), os.Environ()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewKubectlCommand creates the `kubectl` command and its nested children.
|
||||
|
|
|
@ -19,6 +19,7 @@ package cmd
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
@ -37,6 +38,7 @@ import (
|
|||
"k8s.io/kubernetes/pkg/api/testapi"
|
||||
apitesting "k8s.io/kubernetes/pkg/api/testing"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
||||
"k8s.io/kubernetes/pkg/kubectl/scheme"
|
||||
)
|
||||
|
||||
|
@ -213,3 +215,106 @@ func Test_deprecatedAlias(t *testing.T) {
|
|||
t.Errorf("original function doesn't appear to have been called by alias")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubectlCommandHandlesPlugins(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectPlugin string
|
||||
expectPluginArgs []string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "test that normal commands are able to be executed, when no plugin overshadows them",
|
||||
args: []string{"kubectl", "get", "foo"},
|
||||
expectPlugin: "",
|
||||
expectPluginArgs: []string{},
|
||||
},
|
||||
{
|
||||
name: "test that a plugin executable is found based on command args",
|
||||
args: []string{"kubectl", "foo", "--bar"},
|
||||
expectPlugin: "testdata/plugin/kubectl-foo",
|
||||
expectPluginArgs: []string{"foo", "--bar"},
|
||||
},
|
||||
{
|
||||
name: "test that a plugin does not execute over an existing command by the same name",
|
||||
args: []string{"kubectl", "version"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
pluginsHandler := &testPluginHandler{
|
||||
pluginsDirectory: "testdata/plugin",
|
||||
}
|
||||
_, in, out, errOut := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||
errOut.Write([]byte(str))
|
||||
})
|
||||
|
||||
root := NewDefaultKubectlCommandWithArgs(pluginsHandler, test.args, in, out, errOut)
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if pluginsHandler.err != nil && pluginsHandler.err.Error() != test.expectError {
|
||||
t.Fatalf("unexpected error: expected %q to occur, but got %q", test.expectError, pluginsHandler.err)
|
||||
}
|
||||
|
||||
if pluginsHandler.executedPlugin != test.expectPlugin {
|
||||
t.Fatalf("unexpected plugin execution: expedcted %q, got %q", test.expectPlugin, pluginsHandler.executedPlugin)
|
||||
}
|
||||
|
||||
if len(pluginsHandler.withArgs) != len(test.expectPluginArgs) {
|
||||
t.Fatalf("unexpected plugin execution args: expedcted %q, got %q", test.expectPluginArgs, pluginsHandler.withArgs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testPluginHandler struct {
|
||||
pluginsDirectory string
|
||||
|
||||
// execution results
|
||||
executedPlugin string
|
||||
withArgs []string
|
||||
withEnv []string
|
||||
|
||||
err error
|
||||
}
|
||||
|
||||
func (h *testPluginHandler) Lookup(filename string) (string, error) {
|
||||
dir, err := os.Stat(h.pluginsDirectory)
|
||||
if err != nil {
|
||||
h.err = err
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !dir.IsDir() {
|
||||
h.err = fmt.Errorf("expected %q to be a directory", h.pluginsDirectory)
|
||||
return "", h.err
|
||||
}
|
||||
|
||||
plugins, err := ioutil.ReadDir(h.pluginsDirectory)
|
||||
if err != nil {
|
||||
h.err = err
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, p := range plugins {
|
||||
if p.Name() == filename {
|
||||
return fmt.Sprintf("%s/%s", h.pluginsDirectory, p.Name()), nil
|
||||
}
|
||||
}
|
||||
|
||||
h.err = fmt.Errorf("unable to find a plugin executable %q", filename)
|
||||
return "", h.err
|
||||
}
|
||||
|
||||
func (h *testPluginHandler) Execute(executablePath string, cmdArgs, env []string) error {
|
||||
h.executedPlugin = executablePath
|
||||
h.withArgs = cmdArgs
|
||||
h.withEnv = env
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -18,170 +18,212 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
||||
"k8s.io/kubernetes/pkg/kubectl/plugins"
|
||||
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
plugin_long = templates.LongDesc(`
|
||||
Runs a command-line plugin.
|
||||
Provides utilities for interacting with plugins.
|
||||
|
||||
Plugins are subcommands that are not part of the major command-line distribution
|
||||
and can even be provided by third-parties. Please refer to the documentation and
|
||||
examples for more information about how to install and write your own plugins.`)
|
||||
Plugins provide extended functionality that is not part of the major command-line distribution.
|
||||
Please refer to the documentation and examples for more information about how write your own plugins.`)
|
||||
|
||||
plugin_list_long = templates.LongDesc(`
|
||||
List all available plugin files on a user's PATH.
|
||||
|
||||
Available plugin files are those that are:
|
||||
- executable
|
||||
- anywhere on the user's PATH
|
||||
- begin with "kubectl-"
|
||||
`)
|
||||
)
|
||||
|
||||
// NewCmdPlugin creates the command that is the top-level for plugin commands.
|
||||
func NewCmdPlugin(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
// Loads plugins and create commands for each plugin identified
|
||||
loadedPlugins, loadErr := pluginLoader().Load()
|
||||
if loadErr != nil {
|
||||
glog.V(1).Infof("Unable to load plugins: %v", loadErr)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "plugin NAME",
|
||||
Use: "plugin [flags]",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Runs a command-line plugin"),
|
||||
Short: i18n.T("Provides utilities for interacting with plugins."),
|
||||
Long: plugin_long,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(loadedPlugins) == 0 {
|
||||
cmdutil.CheckErr(fmt.Errorf("no plugins installed."))
|
||||
}
|
||||
cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args)
|
||||
},
|
||||
}
|
||||
|
||||
if len(loadedPlugins) > 0 {
|
||||
pluginRunner := pluginRunner()
|
||||
for _, p := range loadedPlugins {
|
||||
cmd.AddCommand(NewCmdForPlugin(f, p, pluginRunner, streams))
|
||||
}
|
||||
}
|
||||
|
||||
cmd.AddCommand(NewCmdPluginList(f, streams))
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewCmdForPlugin creates a command capable of running the provided plugin.
|
||||
func NewCmdForPlugin(f cmdutil.Factory, plugin *plugins.Plugin, runner plugins.PluginRunner, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
if !plugin.IsValid() {
|
||||
return nil
|
||||
type PluginListOptions struct {
|
||||
Verifier PathVerifier
|
||||
NameOnly bool
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewCmdPluginList provides a way to list all plugin executables visible to kubectl
|
||||
func NewCmdPluginList(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
o := &PluginListOptions{
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: plugin.Name,
|
||||
Short: plugin.ShortDesc,
|
||||
Long: templates.LongDesc(plugin.LongDesc),
|
||||
Example: templates.Examples(plugin.Example),
|
||||
Use: "list",
|
||||
Short: "list all visible plugin executables on a user's PATH",
|
||||
Long: plugin_list_long,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(plugin.Command) == 0 {
|
||||
cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args)
|
||||
return
|
||||
}
|
||||
|
||||
envProvider := &plugins.MultiEnvProvider{
|
||||
&plugins.PluginCallerEnvProvider{},
|
||||
&plugins.OSEnvProvider{},
|
||||
&plugins.PluginDescriptorEnvProvider{
|
||||
Plugin: plugin,
|
||||
},
|
||||
&flagsPluginEnvProvider{
|
||||
cmd: cmd,
|
||||
},
|
||||
&factoryAttrsPluginEnvProvider{
|
||||
factory: f,
|
||||
},
|
||||
}
|
||||
|
||||
runningContext := plugins.RunningContext{
|
||||
IOStreams: streams,
|
||||
Args: args,
|
||||
EnvProvider: envProvider,
|
||||
WorkingDir: plugin.Dir,
|
||||
}
|
||||
|
||||
if err := runner.Run(plugin, runningContext); err != nil {
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
// check for (and exit with) the correct exit code
|
||||
// from a failed plugin command execution
|
||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||
fmt.Fprintf(streams.ErrOut, "error: %v\n", err)
|
||||
os.Exit(status.ExitStatus())
|
||||
}
|
||||
}
|
||||
|
||||
cmdutil.CheckErr(err)
|
||||
}
|
||||
cmdutil.CheckErr(o.Complete(cmd))
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
for _, flag := range plugin.Flags {
|
||||
cmd.Flags().StringP(flag.Name, flag.Shorthand, flag.DefValue, flag.Desc)
|
||||
}
|
||||
|
||||
for _, childPlugin := range plugin.Tree {
|
||||
cmd.AddCommand(NewCmdForPlugin(f, childPlugin, runner, streams))
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&o.NameOnly, "name-only", o.NameOnly, "If true, display only the binary name of each plugin, rather than its full path")
|
||||
return cmd
|
||||
}
|
||||
|
||||
type flagsPluginEnvProvider struct {
|
||||
cmd *cobra.Command
|
||||
func (o *PluginListOptions) Complete(cmd *cobra.Command) error {
|
||||
o.Verifier = &CommandOverrideVerifier{
|
||||
root: cmd.Root(),
|
||||
seenPlugins: make(map[string]string, 0),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *flagsPluginEnvProvider) Env() (plugins.EnvList, error) {
|
||||
globalPrefix := "KUBECTL_PLUGINS_GLOBAL_FLAG_"
|
||||
env := plugins.EnvList{}
|
||||
p.cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
|
||||
env = append(env, plugins.FlagToEnv(flag, globalPrefix))
|
||||
})
|
||||
localPrefix := "KUBECTL_PLUGINS_LOCAL_FLAG_"
|
||||
p.cmd.LocalFlags().VisitAll(func(flag *pflag.Flag) {
|
||||
env = append(env, plugins.FlagToEnv(flag, localPrefix))
|
||||
})
|
||||
return env, nil
|
||||
func (o *PluginListOptions) Run() error {
|
||||
path := "PATH"
|
||||
if runtime.GOOS == "windows" {
|
||||
path = "path"
|
||||
}
|
||||
|
||||
pluginsFound := false
|
||||
isFirstFile := true
|
||||
pluginWarnings := 0
|
||||
for _, dir := range filepath.SplitList(os.Getenv(path)) {
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(f.Name(), "kubectl-") {
|
||||
continue
|
||||
}
|
||||
|
||||
if isFirstFile {
|
||||
fmt.Fprintf(o.ErrOut, "The following kubectl-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 !pluginsFound {
|
||||
return fmt.Errorf("error: unable to find any kubectl plugins in your PATH")
|
||||
}
|
||||
|
||||
if pluginWarnings > 0 {
|
||||
fmt.Fprintln(o.ErrOut)
|
||||
if pluginWarnings == 1 {
|
||||
return fmt.Errorf("one plugin warning was found")
|
||||
}
|
||||
return fmt.Errorf("%v plugin warnings were found", pluginWarnings)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type factoryAttrsPluginEnvProvider struct {
|
||||
factory cmdutil.Factory
|
||||
// pathVerifier receives a path and determines if it is valid or not
|
||||
type PathVerifier interface {
|
||||
// Verify determines if a given path is valid
|
||||
Verify(path string) []error
|
||||
}
|
||||
|
||||
func (p *factoryAttrsPluginEnvProvider) Env() (plugins.EnvList, error) {
|
||||
cmdNamespace, _, err := p.factory.ToRawKubeConfigLoader().Namespace()
|
||||
type CommandOverrideVerifier struct {
|
||||
root *cobra.Command
|
||||
seenPlugins map[string]string
|
||||
}
|
||||
|
||||
// Verify implements PathVerifier and determines if a given path
|
||||
// is valid depending on whether or not it overwrites an existing
|
||||
// kubectl command path, or a previously seen plugin.
|
||||
func (v *CommandOverrideVerifier) Verify(path string) []error {
|
||||
if v.root == nil {
|
||||
return []error{fmt.Errorf("unable to verify path with nil root")}
|
||||
}
|
||||
|
||||
// extract the plugin binary name
|
||||
segs := strings.Split(path, "/")
|
||||
binName := segs[len(segs)-1]
|
||||
|
||||
cmdPath := strings.Split(binName, "-")
|
||||
if len(cmdPath) > 1 {
|
||||
// the first argument is always "kubectl" for a plugin binary
|
||||
cmdPath = cmdPath[1:]
|
||||
}
|
||||
|
||||
errors := []error{}
|
||||
|
||||
if isExec, err := isExecutable(path); err == nil && !isExec {
|
||||
errors = append(errors, fmt.Errorf("warning: %s identified as a kubectl plugin, but it is not executable", path))
|
||||
} else if err != nil {
|
||||
errors = append(errors, fmt.Errorf("error: unable to identify %s as an executable file: %v", path, err))
|
||||
}
|
||||
|
||||
if existingPath, ok := v.seenPlugins[binName]; ok {
|
||||
errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named plugin: %s", path, existingPath))
|
||||
} else {
|
||||
v.seenPlugins[binName] = path
|
||||
}
|
||||
|
||||
if cmd, _, err := v.root.Find(cmdPath); err == nil {
|
||||
errors = append(errors, fmt.Errorf("warning: %s overwrites existing command: %q", binName, cmd.CommandPath()))
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func isExecutable(fullPath string) (bool, error) {
|
||||
info, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
return plugins.EnvList{}, err
|
||||
return false, err
|
||||
}
|
||||
return plugins.EnvList{
|
||||
plugins.Env{N: "KUBECTL_PLUGINS_CURRENT_NAMESPACE", V: cmdNamespace},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// pluginLoader loads plugins from a path set by the KUBECTL_PLUGINS_PATH env var.
|
||||
// If this env var is not set, it defaults to
|
||||
// "~/.kube/plugins", plus
|
||||
// "./kubectl/plugins" directory under the "data dir" directory specified by the XDG
|
||||
// system directory structure spec for the given platform.
|
||||
func pluginLoader() plugins.PluginLoader {
|
||||
if len(os.Getenv("KUBECTL_PLUGINS_PATH")) > 0 {
|
||||
return plugins.KubectlPluginsPathPluginLoader()
|
||||
if runtime.GOOS == "windows" {
|
||||
if strings.HasSuffix(info.Name(), ".exe") {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return plugins.TolerantMultiPluginLoader{
|
||||
plugins.XDGDataDirsPluginLoader(),
|
||||
plugins.UserDirPluginLoader(),
|
||||
}
|
||||
}
|
||||
|
||||
func pluginRunner() plugins.PluginRunner {
|
||||
return &plugins.ExecPluginRunner{}
|
||||
if m := info.Mode(); !m.IsDir() && m&0111 != 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
||||
"k8s.io/kubernetes/pkg/kubectl/plugins"
|
||||
)
|
||||
|
||||
type mockPluginRunner struct {
|
||||
success bool
|
||||
}
|
||||
|
||||
func (r *mockPluginRunner) Run(p *plugins.Plugin, ctx plugins.RunningContext) error {
|
||||
if !r.success {
|
||||
return fmt.Errorf("oops %s", p.Name)
|
||||
}
|
||||
ctx.Out.Write([]byte(fmt.Sprintf("ok: %s", p.Name)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPluginCmd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
plugin *plugins.Plugin
|
||||
expectedSuccess bool
|
||||
expectedNilCmd bool
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
plugin: &plugins.Plugin{
|
||||
Description: plugins.Description{
|
||||
Name: "success",
|
||||
ShortDesc: "The Test Plugin",
|
||||
Command: "echo ok",
|
||||
},
|
||||
},
|
||||
expectedSuccess: true,
|
||||
},
|
||||
{
|
||||
name: "incomplete",
|
||||
plugin: &plugins.Plugin{
|
||||
Description: plugins.Description{
|
||||
Name: "incomplete",
|
||||
ShortDesc: "The Incomplete Plugin",
|
||||
},
|
||||
},
|
||||
expectedNilCmd: true,
|
||||
},
|
||||
{
|
||||
name: "failure",
|
||||
plugin: &plugins.Plugin{
|
||||
Description: plugins.Description{
|
||||
Name: "failure",
|
||||
ShortDesc: "The Failing Plugin",
|
||||
Command: "false",
|
||||
},
|
||||
},
|
||||
expectedSuccess: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
streams, _, outBuf, errBuf := genericclioptions.NewTestIOStreams()
|
||||
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||
errBuf.Write([]byte(str))
|
||||
})
|
||||
|
||||
runner := &mockPluginRunner{
|
||||
success: test.expectedSuccess,
|
||||
}
|
||||
|
||||
f := cmdtesting.NewTestFactory()
|
||||
defer f.Cleanup()
|
||||
|
||||
cmd := NewCmdForPlugin(f, test.plugin, runner, streams)
|
||||
if cmd == nil {
|
||||
if !test.expectedNilCmd {
|
||||
t.Fatalf("%s: command was unexpectedly not registered", test.name)
|
||||
}
|
||||
return
|
||||
}
|
||||
cmd.Run(cmd, []string{})
|
||||
|
||||
if test.expectedSuccess && outBuf.String() != fmt.Sprintf("ok: %s", test.plugin.Name) {
|
||||
t.Errorf("%s: unexpected output: %q", test.name, outBuf.String())
|
||||
}
|
||||
|
||||
if !test.expectedSuccess && errBuf.String() != fmt.Sprintf("error: oops %s", test.plugin.Name) {
|
||||
t.Errorf("%s: unexpected err output: %q", test.name, errBuf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -510,6 +510,10 @@ func TestRunValidations(t *testing.T) {
|
|||
tf.ClientConfigVal = defaultClientConfig()
|
||||
|
||||
streams, _, _, bufErr := genericclioptions.NewTestIOStreams()
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||
bufErr.Write([]byte(str))
|
||||
})
|
||||
|
||||
cmd := NewCmdRun(tf, streams)
|
||||
for flagName, flagValue := range test.flags {
|
||||
cmd.Flags().Set(flagName, flagValue)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "I am plugin foo"
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This plugin is a no-op and is used to test plugins
|
||||
# that overshadow existing kubectl commands
|
|
@ -1,53 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"env.go",
|
||||
"loader.go",
|
||||
"plugins.go",
|
||||
"runner.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/kubectl/plugins",
|
||||
deps = [
|
||||
"//pkg/kubectl/genericclioptions:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library",
|
||||
"//vendor/github.com/ghodss/yaml:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/github.com/spf13/pflag:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"env_test.go",
|
||||
"loader_test.go",
|
||||
"plugins_test.go",
|
||||
"runner_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/kubectl/genericclioptions:go_default_library",
|
||||
"//vendor/github.com/spf13/pflag:go_default_library",
|
||||
],
|
||||
)
|
|
@ -1,161 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 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 plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Env represents an environment variable with its name and value.
|
||||
type Env struct {
|
||||
N string
|
||||
V string
|
||||
}
|
||||
|
||||
// String returns "name=value" string.
|
||||
func (e Env) String() string {
|
||||
return fmt.Sprintf("%s=%s", e.N, e.V)
|
||||
}
|
||||
|
||||
// EnvList is a list of Env.
|
||||
type EnvList []Env
|
||||
|
||||
// Slice returns a slice of "name=value" strings.
|
||||
func (e EnvList) Slice() []string {
|
||||
envs := []string{}
|
||||
for _, env := range e {
|
||||
envs = append(envs, env.String())
|
||||
}
|
||||
return envs
|
||||
}
|
||||
|
||||
// Merge converts "name=value" strings into Env values and merges them into e.
|
||||
func (e EnvList) Merge(s ...string) EnvList {
|
||||
newList := e
|
||||
newList = append(newList, fromSlice(s)...)
|
||||
return newList
|
||||
}
|
||||
|
||||
// EnvProvider provides the environment in which the plugin will run.
|
||||
type EnvProvider interface {
|
||||
// Env returns the env list.
|
||||
Env() (EnvList, error)
|
||||
}
|
||||
|
||||
// MultiEnvProvider satisfies the EnvProvider interface for multiple env providers.
|
||||
type MultiEnvProvider []EnvProvider
|
||||
|
||||
// Env returns the combined env list of multiple env providers, returns on first error.
|
||||
func (p MultiEnvProvider) Env() (EnvList, error) {
|
||||
env := EnvList{}
|
||||
for _, provider := range p {
|
||||
pEnv, err := provider.Env()
|
||||
if err != nil {
|
||||
return EnvList{}, err
|
||||
}
|
||||
env = append(env, pEnv...)
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// PluginCallerEnvProvider satisfies the EnvProvider interface.
|
||||
type PluginCallerEnvProvider struct{}
|
||||
|
||||
// Env returns env with the path to the caller binary (usually full path to 'kubectl').
|
||||
func (p *PluginCallerEnvProvider) Env() (EnvList, error) {
|
||||
caller, err := os.Executable()
|
||||
if err != nil {
|
||||
return EnvList{}, err
|
||||
}
|
||||
return EnvList{
|
||||
{"KUBECTL_PLUGINS_CALLER", caller},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PluginDescriptorEnvProvider satisfies the EnvProvider interface.
|
||||
type PluginDescriptorEnvProvider struct {
|
||||
Plugin *Plugin
|
||||
}
|
||||
|
||||
// Env returns env with information about the running plugin.
|
||||
func (p *PluginDescriptorEnvProvider) Env() (EnvList, error) {
|
||||
if p.Plugin == nil {
|
||||
return []Env{}, fmt.Errorf("plugin not present to extract env")
|
||||
}
|
||||
prefix := "KUBECTL_PLUGINS_DESCRIPTOR_"
|
||||
env := EnvList{
|
||||
{prefix + "NAME", p.Plugin.Name},
|
||||
{prefix + "SHORT_DESC", p.Plugin.ShortDesc},
|
||||
{prefix + "LONG_DESC", p.Plugin.LongDesc},
|
||||
{prefix + "EXAMPLE", p.Plugin.Example},
|
||||
{prefix + "COMMAND", p.Plugin.Command},
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// OSEnvProvider satisfies the EnvProvider interface.
|
||||
type OSEnvProvider struct{}
|
||||
|
||||
// Env returns the current environment from the operating system.
|
||||
func (p *OSEnvProvider) Env() (EnvList, error) {
|
||||
return fromSlice(os.Environ()), nil
|
||||
}
|
||||
|
||||
// EmptyEnvProvider satisfies the EnvProvider interface.
|
||||
type EmptyEnvProvider struct{}
|
||||
|
||||
// Env returns an empty environment.
|
||||
func (p *EmptyEnvProvider) Env() (EnvList, error) {
|
||||
return EnvList{}, nil
|
||||
}
|
||||
|
||||
// FlagToEnvName converts a flag string into a UNIX like environment variable name.
|
||||
// e.g --some-flag => "PREFIX_SOME_FLAG"
|
||||
func FlagToEnvName(flagName, prefix string) string {
|
||||
envName := strings.TrimPrefix(flagName, "--")
|
||||
envName = strings.ToUpper(envName)
|
||||
envName = strings.Replace(envName, "-", "_", -1)
|
||||
envName = prefix + envName
|
||||
return envName
|
||||
}
|
||||
|
||||
// FlagToEnv converts a flag and its value into an Env.
|
||||
// e.g --some-flag some-value => Env{N: "PREFIX_SOME_FLAG", V="SOME_VALUE"}
|
||||
func FlagToEnv(flag *pflag.Flag, prefix string) Env {
|
||||
envName := FlagToEnvName(flag.Name, prefix)
|
||||
return Env{envName, flag.Value.String()}
|
||||
}
|
||||
|
||||
func fromSlice(envs []string) EnvList {
|
||||
list := EnvList{}
|
||||
for _, env := range envs {
|
||||
list = append(list, parseEnv(env))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func parseEnv(env string) Env {
|
||||
if !strings.Contains(env, "=") {
|
||||
env = env + "="
|
||||
}
|
||||
parsed := strings.SplitN(env, "=", 2)
|
||||
return Env{parsed[0], parsed[1]}
|
||||
}
|
|
@ -1,186 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 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 plugins
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env Env
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
env: Env{"FOO", "BAR"},
|
||||
expected: "FOO=BAR",
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
env: Env{"FOO", "BAR="},
|
||||
expected: "FOO=BAR=",
|
||||
},
|
||||
{
|
||||
name: "test3",
|
||||
env: Env{"FOO", ""},
|
||||
expected: "FOO=",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if s := tt.env.String(); s != tt.expected {
|
||||
t.Errorf("%v: expected string %q, got %q", tt.env, tt.expected, s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvListToSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env EnvList
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
env: EnvList{
|
||||
{"FOO", "BAR"},
|
||||
{"ZEE", "YO"},
|
||||
{"ONE", "1"},
|
||||
{"EQUALS", "=="},
|
||||
{"EMPTY", ""},
|
||||
},
|
||||
expected: []string{"FOO=BAR", "ZEE=YO", "ONE=1", "EQUALS===", "EMPTY="},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if s := tt.env.Slice(); !reflect.DeepEqual(tt.expected, s) {
|
||||
t.Errorf("%v: expected %v, got %v", tt.env, tt.expected, s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddToEnvList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
add []string
|
||||
expected EnvList
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
add: []string{"FOO=BAR", "EMPTY=", "EQUALS===", "JUSTNAME"},
|
||||
expected: EnvList{
|
||||
{"FOO", "BAR"},
|
||||
{"EMPTY", ""},
|
||||
{"EQUALS", "=="},
|
||||
{"JUSTNAME", ""},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
env := EnvList{}.Merge(tt.add...)
|
||||
if !reflect.DeepEqual(tt.expected, env) {
|
||||
t.Errorf("%v: expected %v, got %v", tt.add, tt.expected, env)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagToEnv(t *testing.T) {
|
||||
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
|
||||
flags.String("test", "ok", "")
|
||||
flags.String("kube-master", "http://something", "")
|
||||
flags.String("from-file", "default", "")
|
||||
flags.Parse([]string{"--from-file=nondefault"})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
flag *pflag.Flag
|
||||
prefix string
|
||||
expected Env
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
flag: flags.Lookup("test"),
|
||||
expected: Env{"TEST", "ok"},
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
flag: flags.Lookup("kube-master"),
|
||||
expected: Env{"KUBE_MASTER", "http://something"},
|
||||
},
|
||||
{
|
||||
name: "test3",
|
||||
prefix: "KUBECTL_",
|
||||
flag: flags.Lookup("from-file"),
|
||||
expected: Env{"KUBECTL_FROM_FILE", "nondefault"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if env := FlagToEnv(tt.flag, tt.prefix); !reflect.DeepEqual(tt.expected, env) {
|
||||
t.Errorf("%v: expected %v, got %v", tt.flag.Name, tt.expected, env)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginDescriptorEnvProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
plugin *Plugin
|
||||
expected EnvList
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
plugin: &Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "Short Description",
|
||||
Command: "foo --bar",
|
||||
},
|
||||
},
|
||||
expected: EnvList{
|
||||
{"KUBECTL_PLUGINS_DESCRIPTOR_NAME", "test"},
|
||||
{"KUBECTL_PLUGINS_DESCRIPTOR_SHORT_DESC", "Short Description"},
|
||||
{"KUBECTL_PLUGINS_DESCRIPTOR_LONG_DESC", ""},
|
||||
{"KUBECTL_PLUGINS_DESCRIPTOR_EXAMPLE", ""},
|
||||
{"KUBECTL_PLUGINS_DESCRIPTOR_COMMAND", "foo --bar"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider := &PluginDescriptorEnvProvider{
|
||||
Plugin: tt.plugin,
|
||||
}
|
||||
env, _ := provider.Env()
|
||||
if !reflect.DeepEqual(tt.expected, env) {
|
||||
t.Errorf("%v: expected %v, got %v", tt.plugin.Name, tt.expected, env)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require 'json'
|
||||
require 'date'
|
||||
|
||||
class Numeric
|
||||
def duration
|
||||
secs = self.to_int
|
||||
mins = secs / 60
|
||||
hours = mins / 60
|
||||
days = hours / 24
|
||||
|
||||
if days > 0
|
||||
"#{days} days and #{hours % 24} hours"
|
||||
elsif hours > 0
|
||||
"#{hours} hours and #{mins % 60} minutes"
|
||||
elsif mins > 0
|
||||
"#{mins} minutes and #{secs % 60} seconds"
|
||||
elsif secs >= 0
|
||||
"#{secs} seconds"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace = ENV['KUBECTL_PLUGINS_CURRENT_NAMESPACE'] || 'default'
|
||||
pods_json = `kubectl --namespace #{namespace} get pods -o json`
|
||||
pods_parsed = JSON.parse(pods_json)
|
||||
|
||||
puts "The Magnificent Aging Plugin."
|
||||
|
||||
data = Hash.new
|
||||
max_name_length = 0
|
||||
max_age = 0
|
||||
min_age = 0
|
||||
|
||||
pods_parsed['items'].each { |pod|
|
||||
name = pod['metadata']['name']
|
||||
creation = pod['metadata']['creationTimestamp']
|
||||
|
||||
age = Time.now - DateTime.parse(creation).to_time
|
||||
data[name] = age
|
||||
|
||||
if name.length > max_name_length
|
||||
max_name_length = name.length
|
||||
end
|
||||
if age > max_age
|
||||
max_age = age
|
||||
end
|
||||
if age < min_age
|
||||
min_age = age
|
||||
end
|
||||
}
|
||||
|
||||
data = data.sort_by{ |name, age| age }
|
||||
|
||||
if data.length > 0
|
||||
puts ""
|
||||
data.each { |name, age|
|
||||
output = ""
|
||||
output += name.rjust(max_name_length, ' ') + ": "
|
||||
bar_size = (age*80/max_age).ceil
|
||||
bar_size.times{ output += "▒" }
|
||||
output += " " + age.duration
|
||||
puts output
|
||||
puts ""
|
||||
}
|
||||
else
|
||||
puts "No pods"
|
||||
end
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
name: "aging"
|
||||
shortDesc: "Aging shows pods by age"
|
||||
longDesc: >
|
||||
Aging shows pods from the current namespace by age.
|
||||
command: ./aging.rb
|
|
@ -1,3 +0,0 @@
|
|||
name: "hello"
|
||||
shortDesc: "I say hello!"
|
||||
command: "echo Hello plugins!"
|
|
@ -1,204 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 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 plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// PluginDescriptorFilename is the default file name for plugin descriptions.
|
||||
const PluginDescriptorFilename = "plugin.yaml"
|
||||
|
||||
// PluginLoader is capable of loading a list of plugin descriptions.
|
||||
type PluginLoader interface {
|
||||
// Load loads the plugin descriptions.
|
||||
Load() (Plugins, error)
|
||||
}
|
||||
|
||||
// DirectoryPluginLoader is a PluginLoader that loads plugin descriptions
|
||||
// from a given directory in the filesystem. Plugins are located in subdirs
|
||||
// under the loader "root", where each subdir must contain, at least, a plugin
|
||||
// descriptor file called "plugin.yaml" that translates into a PluginDescription.
|
||||
type DirectoryPluginLoader struct {
|
||||
Directory string
|
||||
}
|
||||
|
||||
// Load reads the directory the loader holds and loads plugin descriptions.
|
||||
func (l *DirectoryPluginLoader) Load() (Plugins, error) {
|
||||
if len(l.Directory) == 0 {
|
||||
return nil, fmt.Errorf("directory not specified")
|
||||
}
|
||||
|
||||
list := Plugins{}
|
||||
|
||||
stat, err := os.Stat(l.Directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !stat.IsDir() {
|
||||
return nil, fmt.Errorf("not a directory: %s", l.Directory)
|
||||
}
|
||||
|
||||
base, err := filepath.Abs(l.Directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read the base directory tree searching for plugin descriptors
|
||||
// fails silently (descriptors unable to be read or unmarshalled are logged but skipped)
|
||||
err = filepath.Walk(base, func(path string, fileInfo os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil || fileInfo.IsDir() || fileInfo.Name() != PluginDescriptorFilename {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
glog.V(1).Infof("Unable to read plugin descriptor %s: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
plugin := &Plugin{}
|
||||
if err := yaml.Unmarshal(file, plugin); err != nil {
|
||||
glog.V(1).Infof("Unable to unmarshal plugin descriptor %s: %v", path, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := plugin.Validate(); err != nil {
|
||||
glog.V(1).Infof("%v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var setSource func(path string, fileInfo os.FileInfo, p *Plugin)
|
||||
setSource = func(path string, fileInfo os.FileInfo, p *Plugin) {
|
||||
p.Dir = filepath.Dir(path)
|
||||
p.DescriptorName = fileInfo.Name()
|
||||
for _, child := range p.Tree {
|
||||
setSource(path, fileInfo, child)
|
||||
}
|
||||
}
|
||||
setSource(path, fileInfo, plugin)
|
||||
|
||||
glog.V(6).Infof("Plugin loaded: %s", plugin.Name)
|
||||
list = append(list, plugin)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// UserDirPluginLoader returns a PluginLoader that loads plugins from the
|
||||
// "plugins" directory under the user's kubeconfig dir (usually "~/.kube/plugins/").
|
||||
func UserDirPluginLoader() PluginLoader {
|
||||
dir := filepath.Join(clientcmd.RecommendedConfigDir, "plugins")
|
||||
return &DirectoryPluginLoader{
|
||||
Directory: dir,
|
||||
}
|
||||
}
|
||||
|
||||
// PathFromEnvVarPluginLoader returns a PluginLoader that loads plugins from one or more
|
||||
// directories specified by the provided env var name. In case the env var is not
|
||||
// set, the PluginLoader just loads nothing. A list of subdirectories can be provided,
|
||||
// which will be appended to each path specified by the env var.
|
||||
func PathFromEnvVarPluginLoader(envVarName string, subdirs ...string) PluginLoader {
|
||||
env := os.Getenv(envVarName)
|
||||
if len(env) == 0 {
|
||||
return &DummyPluginLoader{}
|
||||
}
|
||||
loader := MultiPluginLoader{}
|
||||
for _, path := range filepath.SplitList(env) {
|
||||
dir := append([]string{path}, subdirs...)
|
||||
loader = append(loader, &DirectoryPluginLoader{
|
||||
Directory: filepath.Join(dir...),
|
||||
})
|
||||
}
|
||||
return loader
|
||||
}
|
||||
|
||||
// KubectlPluginsPathPluginLoader returns a PluginLoader that loads plugins from one or more
|
||||
// directories specified by the KUBECTL_PLUGINS_PATH env var.
|
||||
func KubectlPluginsPathPluginLoader() PluginLoader {
|
||||
return PathFromEnvVarPluginLoader("KUBECTL_PLUGINS_PATH")
|
||||
}
|
||||
|
||||
// XDGDataDirsPluginLoader returns a PluginLoader that loads plugins from one or more
|
||||
// directories specified by the XDG system directory structure spec in the
|
||||
// XDG_DATA_DIRS env var, plus the "kubectl/plugins/" suffix. According to the
|
||||
// spec, if XDG_DATA_DIRS is not set it defaults to "/usr/local/share:/usr/share".
|
||||
func XDGDataDirsPluginLoader() PluginLoader {
|
||||
envVarName := "XDG_DATA_DIRS"
|
||||
if len(os.Getenv(envVarName)) > 0 {
|
||||
return PathFromEnvVarPluginLoader(envVarName, "kubectl", "plugins")
|
||||
}
|
||||
return TolerantMultiPluginLoader{
|
||||
&DirectoryPluginLoader{
|
||||
Directory: "/usr/local/share/kubectl/plugins",
|
||||
},
|
||||
&DirectoryPluginLoader{
|
||||
Directory: "/usr/share/kubectl/plugins",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MultiPluginLoader is a PluginLoader that can encapsulate multiple plugin loaders,
|
||||
// a successful loading means every encapsulated loader was able to load without errors.
|
||||
type MultiPluginLoader []PluginLoader
|
||||
|
||||
// Load calls Load() for each of the encapsulated Loaders.
|
||||
func (l MultiPluginLoader) Load() (Plugins, error) {
|
||||
plugins := Plugins{}
|
||||
for _, loader := range l {
|
||||
loaded, err := loader.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plugins = append(plugins, loaded...)
|
||||
}
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// TolerantMultiPluginLoader is a PluginLoader than encapsulates multiple plugins loaders,
|
||||
// but is tolerant to errors while loading from them.
|
||||
type TolerantMultiPluginLoader []PluginLoader
|
||||
|
||||
// Load calls Load() for each of the encapsulated Loaders.
|
||||
func (l TolerantMultiPluginLoader) Load() (Plugins, error) {
|
||||
plugins := Plugins{}
|
||||
for _, loader := range l {
|
||||
loaded, _ := loader.Load()
|
||||
if loaded != nil {
|
||||
plugins = append(plugins, loaded...)
|
||||
}
|
||||
}
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// DummyPluginLoader is a noop PluginLoader.
|
||||
type DummyPluginLoader struct{}
|
||||
|
||||
// Load loads nothing.
|
||||
func (l *DummyPluginLoader) Load() (Plugins, error) {
|
||||
return Plugins{}, nil
|
||||
}
|
|
@ -1,262 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 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 plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSuccessfulDirectoryPluginLoader(t *testing.T) {
|
||||
tmp, err := setupValidPlugins(3, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
loader := &DirectoryPluginLoader{
|
||||
Directory: tmp,
|
||||
}
|
||||
plugins, err := loader.Load()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error loading plugins: %v", err)
|
||||
}
|
||||
|
||||
if count := len(plugins); count != 3 {
|
||||
t.Errorf("Unexpected number of loaded plugins, wanted 3, got %d", count)
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if m, _ := regexp.MatchString("^plugin[123]$", plugin.Name); !m {
|
||||
t.Errorf("Unexpected plugin name %s", plugin.Name)
|
||||
}
|
||||
if m, _ := regexp.MatchString("^The plugin[123] test plugin$", plugin.ShortDesc); !m {
|
||||
t.Errorf("Unexpected plugin short desc %s", plugin.ShortDesc)
|
||||
}
|
||||
if m, _ := regexp.MatchString("^echo plugin[123]$", plugin.Command); !m {
|
||||
t.Errorf("Unexpected plugin command %s", plugin.Command)
|
||||
}
|
||||
if count := len(plugin.Tree); count != 0 {
|
||||
t.Errorf("Unexpected number of loaded child plugins, wanted 0, got %d", count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyDirectoryPluginLoader(t *testing.T) {
|
||||
loader := &DirectoryPluginLoader{}
|
||||
_, err := loader.Load()
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got none")
|
||||
}
|
||||
if m, _ := regexp.MatchString("^directory not specified$", err.Error()); !m {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotDirectoryPluginLoader(t *testing.T) {
|
||||
tmp, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected ioutil.TempDir error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
file := filepath.Join(tmp, "test.tmp")
|
||||
if err := ioutil.WriteFile(file, []byte("test"), 644); err != nil {
|
||||
t.Fatalf("unexpected ioutil.WriteFile error: %v", err)
|
||||
}
|
||||
|
||||
loader := &DirectoryPluginLoader{
|
||||
Directory: file,
|
||||
}
|
||||
_, err = loader.Load()
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got none")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not a directory") {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnexistentDirectoryPluginLoader(t *testing.T) {
|
||||
loader := &DirectoryPluginLoader{
|
||||
Directory: "/hopefully-does-not-exist",
|
||||
}
|
||||
_, err := loader.Load()
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got none")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "no such file or directory") {
|
||||
t.Errorf("Unexpected error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubectlPluginsPathPluginLoader(t *testing.T) {
|
||||
tmp, err := setupValidPlugins(1, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
env := "KUBECTL_PLUGINS_PATH"
|
||||
os.Setenv(env, tmp)
|
||||
defer os.Unsetenv(env)
|
||||
|
||||
loader := KubectlPluginsPathPluginLoader()
|
||||
|
||||
plugins, err := loader.Load()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error loading plugins: %v", err)
|
||||
}
|
||||
|
||||
if count := len(plugins); count != 1 {
|
||||
t.Errorf("Unexpected number of loaded plugins, wanted 1, got %d", count)
|
||||
}
|
||||
|
||||
plugin := plugins[0]
|
||||
if "plugin1" != plugin.Name {
|
||||
t.Errorf("Unexpected plugin name %s", plugin.Name)
|
||||
}
|
||||
if "The plugin1 test plugin" != plugin.ShortDesc {
|
||||
t.Errorf("Unexpected plugin short desc %s", plugin.ShortDesc)
|
||||
}
|
||||
if "echo plugin1" != plugin.Command {
|
||||
t.Errorf("Unexpected plugin command %s", plugin.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncompletePluginDescriptor(t *testing.T) {
|
||||
tmp, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected ioutil.TempDir error: %v", err)
|
||||
}
|
||||
|
||||
descriptor := `
|
||||
name: incomplete
|
||||
shortDesc: The incomplete test plugin`
|
||||
|
||||
if err := os.Mkdir(filepath.Join(tmp, "incomplete"), 0755); err != nil {
|
||||
t.Fatalf("unexpected os.Mkdir error: %v", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(filepath.Join(tmp, "incomplete", "plugin.yaml"), []byte(descriptor), 0644); err != nil {
|
||||
t.Fatalf("unexpected ioutil.WriteFile error: %v", err)
|
||||
}
|
||||
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
loader := &DirectoryPluginLoader{
|
||||
Directory: tmp,
|
||||
}
|
||||
plugins, err := loader.Load()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if count := len(plugins); count != 0 {
|
||||
t.Errorf("Unexpected number of loaded plugins, wanted 0, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectoryTreePluginLoader(t *testing.T) {
|
||||
tmp, err := setupValidPlugins(1, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
loader := &DirectoryPluginLoader{
|
||||
Directory: tmp,
|
||||
}
|
||||
plugins, err := loader.Load()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error loading plugins: %v", err)
|
||||
}
|
||||
|
||||
if count := len(plugins); count != 1 {
|
||||
t.Errorf("Unexpected number of loaded plugins, wanted 1, got %d", count)
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if m, _ := regexp.MatchString("^plugin1$", plugin.Name); !m {
|
||||
t.Errorf("Unexpected plugin name %s", plugin.Name)
|
||||
}
|
||||
if m, _ := regexp.MatchString("^The plugin1 test plugin$", plugin.ShortDesc); !m {
|
||||
t.Errorf("Unexpected plugin short desc %s", plugin.ShortDesc)
|
||||
}
|
||||
if m, _ := regexp.MatchString("^echo plugin1$", plugin.Command); !m {
|
||||
t.Errorf("Unexpected plugin command %s", plugin.Command)
|
||||
}
|
||||
if count := len(plugin.Tree); count != 2 {
|
||||
t.Errorf("Unexpected number of loaded child plugins, wanted 2, got %d", count)
|
||||
}
|
||||
for _, child := range plugin.Tree {
|
||||
if m, _ := regexp.MatchString("^child[12]$", child.Name); !m {
|
||||
t.Errorf("Unexpected plugin child name %s", child.Name)
|
||||
}
|
||||
if m, _ := regexp.MatchString("^The child[12] test plugin child of plugin1 of House Targaryen$", child.ShortDesc); !m {
|
||||
t.Errorf("Unexpected plugin child short desc %s", child.ShortDesc)
|
||||
}
|
||||
if m, _ := regexp.MatchString("^echo child[12]$", child.Command); !m {
|
||||
t.Errorf("Unexpected plugin child command %s", child.Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupValidPlugins(nPlugins, nChildren int) (string, error) {
|
||||
tmp, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unexpected ioutil.TempDir error: %v", err)
|
||||
}
|
||||
|
||||
for i := 1; i <= nPlugins; i++ {
|
||||
name := fmt.Sprintf("plugin%d", i)
|
||||
descriptor := fmt.Sprintf(`
|
||||
name: %[1]s
|
||||
shortDesc: The %[1]s test plugin
|
||||
command: echo %[1]s
|
||||
flags:
|
||||
- name: %[1]s-flag
|
||||
desc: A flag for %[1]s`, name)
|
||||
|
||||
if nChildren > 0 {
|
||||
descriptor += `
|
||||
tree:`
|
||||
}
|
||||
|
||||
for j := 1; j <= nChildren; j++ {
|
||||
child := fmt.Sprintf("child%d", i)
|
||||
descriptor += fmt.Sprintf(`
|
||||
- name: %[1]s
|
||||
shortDesc: The %[1]s test plugin child of %[2]s of House Targaryen
|
||||
command: echo %[1]s`, child, name)
|
||||
}
|
||||
|
||||
if err := os.Mkdir(filepath.Join(tmp, name), 0755); err != nil {
|
||||
return "", fmt.Errorf("unexpected os.Mkdir error: %v", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(filepath.Join(tmp, name, "plugin.yaml"), []byte(descriptor), 0644); err != nil {
|
||||
return "", fmt.Errorf("unexpected ioutil.WriteFile error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return tmp, nil
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 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 plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrIncompletePlugin indicates plugin is incomplete.
|
||||
ErrIncompletePlugin = fmt.Errorf("incomplete plugin descriptor: name, shortDesc and command fields are required")
|
||||
// ErrInvalidPluginName indicates plugin name is invalid.
|
||||
ErrInvalidPluginName = fmt.Errorf("plugin name can't contain spaces")
|
||||
// ErrIncompleteFlag indicates flag is incomplete.
|
||||
ErrIncompleteFlag = fmt.Errorf("incomplete flag descriptor: name and desc fields are required")
|
||||
// ErrInvalidFlagName indicates flag name is invalid.
|
||||
ErrInvalidFlagName = fmt.Errorf("flag name can't contain spaces")
|
||||
// ErrInvalidFlagShorthand indicates flag shorthand is invalid.
|
||||
ErrInvalidFlagShorthand = fmt.Errorf("flag shorthand must be only one letter")
|
||||
)
|
||||
|
||||
// Plugin is the representation of a CLI extension (plugin).
|
||||
type Plugin struct {
|
||||
Description
|
||||
Source
|
||||
Context RunningContext `json:"-"`
|
||||
}
|
||||
|
||||
// Description holds everything needed to register a
|
||||
// plugin as a command. Usually comes from a descriptor file.
|
||||
type Description struct {
|
||||
Name string `json:"name"`
|
||||
ShortDesc string `json:"shortDesc"`
|
||||
LongDesc string `json:"longDesc,omitempty"`
|
||||
Example string `json:"example,omitempty"`
|
||||
Command string `json:"command"`
|
||||
Flags []Flag `json:"flags,omitempty"`
|
||||
Tree Plugins `json:"tree,omitempty"`
|
||||
}
|
||||
|
||||
// Source holds the location of a given plugin in the filesystem.
|
||||
type Source struct {
|
||||
Dir string `json:"-"`
|
||||
DescriptorName string `json:"-"`
|
||||
}
|
||||
|
||||
// Validate validates plugin data.
|
||||
func (p Plugin) Validate() error {
|
||||
if len(p.Name) == 0 || len(p.ShortDesc) == 0 || (len(p.Command) == 0 && len(p.Tree) == 0) {
|
||||
return ErrIncompletePlugin
|
||||
}
|
||||
if strings.Contains(p.Name, " ") {
|
||||
return ErrInvalidPluginName
|
||||
}
|
||||
for _, flag := range p.Flags {
|
||||
if err := flag.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, child := range p.Tree {
|
||||
if err := child.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValid returns true if plugin data is valid.
|
||||
func (p Plugin) IsValid() bool {
|
||||
return p.Validate() == nil
|
||||
}
|
||||
|
||||
// Plugins is a list of plugins.
|
||||
type Plugins []*Plugin
|
||||
|
||||
// Flag describes a single flag supported by a given plugin.
|
||||
type Flag struct {
|
||||
Name string `json:"name"`
|
||||
Shorthand string `json:"shorthand,omitempty"`
|
||||
Desc string `json:"desc"`
|
||||
DefValue string `json:"defValue,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates flag data.
|
||||
func (f Flag) Validate() error {
|
||||
if len(f.Name) == 0 || len(f.Desc) == 0 {
|
||||
return ErrIncompleteFlag
|
||||
}
|
||||
if strings.Contains(f.Name, " ") {
|
||||
return ErrInvalidFlagName
|
||||
}
|
||||
return f.ValidateShorthand()
|
||||
}
|
||||
|
||||
// ValidateShorthand validates flag shorthand data.
|
||||
func (f Flag) ValidateShorthand() error {
|
||||
length := len(f.Shorthand)
|
||||
if length == 0 || (length == 1 && unicode.IsLetter(rune(f.Shorthand[0]))) {
|
||||
return nil
|
||||
}
|
||||
return ErrInvalidFlagShorthand
|
||||
}
|
||||
|
||||
// Shorthanded returns true if flag shorthand data is valid.
|
||||
func (f Flag) Shorthanded() bool {
|
||||
return f.ValidateShorthand() == nil
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 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 plugins
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
plugin *Plugin
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
plugin: &Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
Command: "echo 1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
plugin: &Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
},
|
||||
},
|
||||
expectedErr: ErrIncompletePlugin,
|
||||
},
|
||||
{
|
||||
name: "test3",
|
||||
plugin: &Plugin{},
|
||||
expectedErr: ErrIncompletePlugin,
|
||||
},
|
||||
{
|
||||
name: "test4",
|
||||
plugin: &Plugin{
|
||||
Description: Description{
|
||||
Name: "test spaces",
|
||||
ShortDesc: "The test",
|
||||
Command: "echo 1",
|
||||
},
|
||||
},
|
||||
expectedErr: ErrInvalidPluginName,
|
||||
},
|
||||
{
|
||||
name: "test5",
|
||||
plugin: &Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
Command: "echo 1",
|
||||
Flags: []Flag{
|
||||
{
|
||||
Name: "aflag",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: ErrIncompleteFlag,
|
||||
},
|
||||
{
|
||||
name: "test6",
|
||||
plugin: &Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
Command: "echo 1",
|
||||
Flags: []Flag{
|
||||
{
|
||||
Name: "a flag",
|
||||
Desc: "Invalid flag",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: ErrInvalidFlagName,
|
||||
},
|
||||
{
|
||||
name: "test7",
|
||||
plugin: &Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
Command: "echo 1",
|
||||
Flags: []Flag{
|
||||
{
|
||||
Name: "aflag",
|
||||
Desc: "Invalid shorthand",
|
||||
Shorthand: "aa",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: ErrInvalidFlagShorthand,
|
||||
},
|
||||
{
|
||||
name: "test8",
|
||||
plugin: &Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
Command: "echo 1",
|
||||
Flags: []Flag{
|
||||
{
|
||||
Name: "aflag",
|
||||
Desc: "Invalid shorthand",
|
||||
Shorthand: "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: ErrInvalidFlagShorthand,
|
||||
},
|
||||
{
|
||||
name: "test9",
|
||||
plugin: &Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
Command: "echo 1",
|
||||
Flags: []Flag{
|
||||
{
|
||||
Name: "aflag",
|
||||
Desc: "A flag",
|
||||
Shorthand: "a",
|
||||
},
|
||||
},
|
||||
Tree: Plugins{
|
||||
&Plugin{
|
||||
Description: Description{
|
||||
Name: "child",
|
||||
ShortDesc: "The child",
|
||||
LongDesc: "The child long desc",
|
||||
Example: "You can use it like this but you're not supposed to",
|
||||
Command: "echo 1",
|
||||
Flags: []Flag{
|
||||
{
|
||||
Name: "childflag",
|
||||
Desc: "A child flag",
|
||||
},
|
||||
{
|
||||
Name: "childshorthand",
|
||||
Desc: "A child shorthand flag",
|
||||
Shorthand: "s",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.plugin.Validate()
|
||||
if err != tt.expectedErr {
|
||||
t.Errorf("%s: expected error %v, got %v", tt.plugin.Name, tt.expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 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 plugins
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
||||
)
|
||||
|
||||
// PluginRunner is capable of running a plugin in a given running context.
|
||||
type PluginRunner interface {
|
||||
Run(plugin *Plugin, ctx RunningContext) error
|
||||
}
|
||||
|
||||
// RunningContext holds the context in which a given plugin is running - the
|
||||
// in, out, and err streams, arguments and environment passed to it, and the
|
||||
// working directory.
|
||||
type RunningContext struct {
|
||||
genericclioptions.IOStreams
|
||||
Args []string
|
||||
EnvProvider EnvProvider
|
||||
WorkingDir string
|
||||
}
|
||||
|
||||
// ExecPluginRunner is a PluginRunner that uses Go's os/exec to run plugins.
|
||||
type ExecPluginRunner struct{}
|
||||
|
||||
// Run takes a given plugin and runs it in a given context using os/exec, returning
|
||||
// any error found while running.
|
||||
func (r *ExecPluginRunner) Run(plugin *Plugin, ctx RunningContext) error {
|
||||
command := strings.Split(os.ExpandEnv(plugin.Command), " ")
|
||||
base := command[0]
|
||||
args := []string{}
|
||||
if len(command) > 1 {
|
||||
args = command[1:]
|
||||
}
|
||||
args = append(args, ctx.Args...)
|
||||
|
||||
cmd := exec.Command(base, args...)
|
||||
|
||||
cmd.Stdin = ctx.In
|
||||
cmd.Stdout = ctx.Out
|
||||
cmd.Stderr = ctx.ErrOut
|
||||
|
||||
env, err := ctx.EnvProvider.Env()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Env = env.Slice()
|
||||
cmd.Dir = ctx.WorkingDir
|
||||
|
||||
glog.V(9).Infof("Running plugin %q as base command %q with args %v", plugin.Name, base, args)
|
||||
return cmd.Run()
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 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 plugins
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
||||
)
|
||||
|
||||
func TestExecRunner(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
command string
|
||||
expectedMsg string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
command: "echo test ok",
|
||||
expectedMsg: "test ok\n",
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
command: "false",
|
||||
expectedErr: "exit status 1",
|
||||
},
|
||||
{
|
||||
name: "env",
|
||||
command: "echo $KUBECTL_PLUGINS_TEST",
|
||||
expectedMsg: "ok\n",
|
||||
},
|
||||
}
|
||||
|
||||
os.Setenv("KUBECTL_PLUGINS_TEST", "ok")
|
||||
defer os.Unsetenv("KUBECTL_PLUGINS_TEST")
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
streams, _, outBuf, _ := genericclioptions.NewTestIOStreams()
|
||||
|
||||
plugin := &Plugin{
|
||||
Description: Description{
|
||||
Name: tt.name,
|
||||
ShortDesc: "Test Runner Plugin",
|
||||
Command: tt.command,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := RunningContext{
|
||||
IOStreams: streams,
|
||||
WorkingDir: ".",
|
||||
EnvProvider: &EmptyEnvProvider{},
|
||||
}
|
||||
|
||||
runner := &ExecPluginRunner{}
|
||||
err := runner.Run(plugin, ctx)
|
||||
|
||||
if outBuf.String() != tt.expectedMsg {
|
||||
t.Errorf("%s: unexpected output: %q", tt.name, outBuf.String())
|
||||
}
|
||||
|
||||
if err != nil && err.Error() != tt.expectedErr {
|
||||
t.Errorf("%s: unexpected err output: %v", tt.name, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -24,81 +24,30 @@ run_plugins_tests() {
|
|||
|
||||
kube::log::status "Testing kubectl plugins"
|
||||
|
||||
# top-level plugin command
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl -h 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'plugin\s\+Runs a command-line plugin'
|
||||
# test plugins that overwrite existing kubectl commands
|
||||
output_message=$(! PATH=${PATH}:"test/fixtures/pkg/kubectl/plugins/version" kubectl plugin list 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'kubectl-version overwrites existing command: "kubectl version"'
|
||||
|
||||
# test plugins that overwrite similarly-named plugins
|
||||
output_message=$(! PATH=${PATH}:"test/fixtures/pkg/kubectl/plugins:test/fixtures/pkg/kubectl/plugins/foo" kubectl plugin list 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'test/fixtures/pkg/kubectl/plugins/foo/kubectl-foo is overshadowed by a similarly named plugin'
|
||||
|
||||
# test plugins with no warnings
|
||||
output_message=$(PATH=${PATH}:"test/fixtures/pkg/kubectl/plugins" kubectl plugin list 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'plugins are available'
|
||||
|
||||
# no plugins
|
||||
output_message=$(! kubectl plugin 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'no plugins installed'
|
||||
output_message=$(! PATH=${PATH}:"test/fixtures/pkg/kubectl/plugins/empty" kubectl plugin list 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'unable to find any kubectl plugins in your PATH'
|
||||
|
||||
# single plugins path
|
||||
output_message=$(! KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'echo\s\+Echoes for test-cmd'
|
||||
kube::test::if_has_string "${output_message}" 'get\s\+The wonderful new plugin-based get!'
|
||||
kube::test::if_has_string "${output_message}" 'error\s\+The tremendous plugin that always fails!'
|
||||
kube::test::if_has_not_string "${output_message}" 'The hello plugin'
|
||||
kube::test::if_has_not_string "${output_message}" 'Incomplete plugin'
|
||||
kube::test::if_has_not_string "${output_message}" 'no plugins installed'
|
||||
# attempt to run a plugin in the user's PATH
|
||||
output_message=$(PATH=${PATH}:"test/fixtures/pkg/kubectl/plugins" kubectl foo)
|
||||
kube::test::if_has_string "${output_message}" 'plugin foo'
|
||||
|
||||
# multiple plugins path
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl plugin -h 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'echo\s\+Echoes for test-cmd'
|
||||
kube::test::if_has_string "${output_message}" 'get\s\+The wonderful new plugin-based get!'
|
||||
kube::test::if_has_string "${output_message}" 'error\s\+The tremendous plugin that always fails!'
|
||||
kube::test::if_has_string "${output_message}" 'hello\s\+The hello plugin'
|
||||
kube::test::if_has_not_string "${output_message}" 'Incomplete plugin'
|
||||
|
||||
# don't override existing commands
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl get -h 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'Display one or many resources'
|
||||
kube::test::if_has_not_string "$output_message{output_message}" 'The wonderful new plugin-based get'
|
||||
|
||||
# plugin help
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl plugin hello -h 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'The hello plugin is a new plugin used by test-cmd to test multiple plugin locations.'
|
||||
kube::test::if_has_string "${output_message}" 'Usage:'
|
||||
|
||||
# run plugin
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl plugin hello 2>&1)
|
||||
kube::test::if_has_string "${output_message}" '#hello#'
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl plugin echo 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'This plugin works!'
|
||||
output_message=$(! KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/ kubectl plugin hello 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'unknown command'
|
||||
output_message=$(! KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/ kubectl plugin error 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'error: exit status 1'
|
||||
|
||||
# plugin tree
|
||||
output_message=$(! KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin tree 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'Plugin with a tree of commands'
|
||||
kube::test::if_has_string "${output_message}" 'child1\s\+The first child of a tree'
|
||||
kube::test::if_has_string "${output_message}" 'child2\s\+The second child of a tree'
|
||||
kube::test::if_has_string "${output_message}" 'child3\s\+The third child of a tree'
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin tree child1 --help 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'The first child of a tree'
|
||||
kube::test::if_has_not_string "${output_message}" 'The second child'
|
||||
kube::test::if_has_not_string "${output_message}" 'child2'
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin tree child1 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'child one'
|
||||
kube::test::if_has_not_string "${output_message}" 'child1'
|
||||
kube::test::if_has_not_string "${output_message}" 'The first child'
|
||||
|
||||
# plugin env
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin env -h 2>&1)
|
||||
kube::test::if_has_string "${output_message}" "This is a flag 1"
|
||||
kube::test::if_has_string "${output_message}" "This is a flag 2"
|
||||
kube::test::if_has_string "${output_message}" "This is a flag 3"
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin env --test1=value1 -t value2 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'KUBECTL_PLUGINS_CURRENT_NAMESPACE'
|
||||
kube::test::if_has_string "${output_message}" 'KUBECTL_PLUGINS_CALLER'
|
||||
kube::test::if_has_string "${output_message}" 'KUBECTL_PLUGINS_DESCRIPTOR_COMMAND=./env.sh'
|
||||
kube::test::if_has_string "${output_message}" 'KUBECTL_PLUGINS_DESCRIPTOR_SHORT_DESC=The plugin envs plugin'
|
||||
kube::test::if_has_string "${output_message}" 'KUBECTL_PLUGINS_GLOBAL_FLAG_KUBECONFIG'
|
||||
kube::test::if_has_string "${output_message}" 'KUBECTL_PLUGINS_GLOBAL_FLAG_REQUEST_TIMEOUT=0'
|
||||
kube::test::if_has_string "${output_message}" 'KUBECTL_PLUGINS_LOCAL_FLAG_TEST1=value1'
|
||||
kube::test::if_has_string "${output_message}" 'KUBECTL_PLUGINS_LOCAL_FLAG_TEST2=value2'
|
||||
kube::test::if_has_string "${output_message}" 'KUBECTL_PLUGINS_LOCAL_FLAG_TEST3=default'
|
||||
# ensure that a kubectl command supersedes a plugin that overshadows it
|
||||
output_message=$(PATH=${PATH}:"test/fixtures/pkg/kubectl/plugins/version" kubectl version)
|
||||
kube::test::if_has_string "${output_message}" 'Client Version'
|
||||
kube::test::if_has_not_string "${output_message}" 'overshadows an existing plugin'
|
||||
|
||||
set +o nounset
|
||||
set +o errexit
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
name: "echo"
|
||||
shortDesc: "Echoes for test-cmd"
|
||||
longDesc: "Long description for the test-cmd echo plugin"
|
||||
command: "echo This plugin works!"
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2017 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.
|
||||
|
||||
env | grep 'KUBECTL_PLUGINS' | sort
|
|
@ -1,12 +0,0 @@
|
|||
name: env
|
||||
shortDesc: "The plugin envs plugin"
|
||||
command: "./env.sh"
|
||||
flags:
|
||||
- name: "test1"
|
||||
desc: "This is a flag 1"
|
||||
- name: "test2"
|
||||
desc: "This is a flag 2"
|
||||
shorthand: "t"
|
||||
- name: "test3"
|
||||
desc: "This is a flag 3"
|
||||
defValue: "default"
|
|
@ -1,3 +0,0 @@
|
|||
name: "error"
|
||||
shortDesc: "The tremendous plugin that always fails!"
|
||||
command: "false"
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "I am plugin foo"
|
|
@ -1,3 +0,0 @@
|
|||
name: "get"
|
||||
shortDesc: "The wonderful new plugin-based get!"
|
||||
command: "echo new-get"
|
|
@ -1,2 +0,0 @@
|
|||
name: "incomplete"
|
||||
shortDesc: "Incomplete plugin"
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "I am plugin foo"
|
|
@ -1,13 +0,0 @@
|
|||
name: "tree"
|
||||
shortDesc: "Plugin with a tree of commands"
|
||||
tree:
|
||||
- name: "child1"
|
||||
shortDesc: "The first child of a tree"
|
||||
command: echo child one
|
||||
- name: "child2"
|
||||
shortDesc: "The second child of a tree"
|
||||
command: echo child two
|
||||
- name: "child3"
|
||||
shortDesc: "The third child of a tree"
|
||||
command: echo child three
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This plugin is a no-op and is used to test
|
||||
# a plugin that overshadows an existing plugin
|
|
@ -1,19 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2017 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.
|
||||
|
||||
echo "#######"
|
||||
echo "#hello#"
|
||||
echo "#######"
|
|
@ -1,7 +0,0 @@
|
|||
name: hello
|
||||
shortDesc: "The hello plugin"
|
||||
longDesc: >
|
||||
The hello plugin is a new
|
||||
plugin used by test-cmd
|
||||
to test multiple plugin locations.
|
||||
command: ./hello.sh
|
Loading…
Reference in New Issue