From 4bdc6363806cc695db68d10b7276f786c74fecac Mon Sep 17 00:00:00 2001 From: juanvallejo Date: Wed, 1 Aug 2018 18:31:07 -0400 Subject: [PATCH] add updated plugin mechanism --- docs/.generated_docs | 2 + docs/man/man1/kubectl-plugin-list.1 | 3 + .../user-guide/kubectl/kubectl_plugin_list.md | 3 + pkg/kubectl/BUILD | 1 - pkg/kubectl/cmd/BUILD | 3 - pkg/kubectl/cmd/cmd.go | 102 ++++++- pkg/kubectl/cmd/cmd_test.go | 105 +++++++ pkg/kubectl/cmd/plugin.go | 258 +++++++++-------- pkg/kubectl/cmd/plugin_test.go | 115 -------- pkg/kubectl/cmd/run_test.go | 4 + pkg/kubectl/cmd/testdata/plugin/kubectl-foo | 3 + .../cmd/testdata/plugin/kubectl-version | 4 + pkg/kubectl/plugins/BUILD | 53 ---- pkg/kubectl/plugins/env.go | 161 ----------- pkg/kubectl/plugins/env_test.go | 186 ------------- pkg/kubectl/plugins/examples/aging/aging.rb | 70 ----- .../plugins/examples/aging/plugin.yaml | 5 - .../plugins/examples/hello/plugin.yaml | 3 - pkg/kubectl/plugins/loader.go | 204 -------------- pkg/kubectl/plugins/loader_test.go | 262 ------------------ pkg/kubectl/plugins/plugins.go | 123 -------- pkg/kubectl/plugins/plugins_test.go | 181 ------------ pkg/kubectl/plugins/runner.go | 72 ----- pkg/kubectl/plugins/runner_test.go | 83 ------ test/cmd/plugins.sh | 89 ++---- .../pkg/kubectl/plugins/echo/plugin.yaml | 4 - test/fixtures/pkg/kubectl/plugins/env/env.sh | 17 -- .../pkg/kubectl/plugins/env/plugin.yaml | 12 - .../pkg/kubectl/plugins/error/plugin.yaml | 3 - .../pkg/kubectl/plugins/foo/kubectl-foo | 3 + .../pkg/kubectl/plugins/get/plugin.yaml | 3 - .../kubectl/plugins/incomplete/plugin.yaml | 2 - test/fixtures/pkg/kubectl/plugins/kubectl-foo | 3 + .../pkg/kubectl/plugins/tree/plugin.yaml | 13 - .../kubectl/plugins/version/kubectl-version | 4 + .../pkg/kubectl/plugins2/hello/hello.sh | 19 -- .../pkg/kubectl/plugins2/hello/plugin.yaml | 7 - 37 files changed, 403 insertions(+), 1782 deletions(-) create mode 100644 docs/man/man1/kubectl-plugin-list.1 create mode 100644 docs/user-guide/kubectl/kubectl_plugin_list.md delete mode 100644 pkg/kubectl/cmd/plugin_test.go create mode 100755 pkg/kubectl/cmd/testdata/plugin/kubectl-foo create mode 100755 pkg/kubectl/cmd/testdata/plugin/kubectl-version delete mode 100644 pkg/kubectl/plugins/BUILD delete mode 100644 pkg/kubectl/plugins/env.go delete mode 100644 pkg/kubectl/plugins/env_test.go delete mode 100755 pkg/kubectl/plugins/examples/aging/aging.rb delete mode 100644 pkg/kubectl/plugins/examples/aging/plugin.yaml delete mode 100644 pkg/kubectl/plugins/examples/hello/plugin.yaml delete mode 100644 pkg/kubectl/plugins/loader.go delete mode 100644 pkg/kubectl/plugins/loader_test.go delete mode 100644 pkg/kubectl/plugins/plugins.go delete mode 100644 pkg/kubectl/plugins/plugins_test.go delete mode 100644 pkg/kubectl/plugins/runner.go delete mode 100644 pkg/kubectl/plugins/runner_test.go delete mode 100644 test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml delete mode 100755 test/fixtures/pkg/kubectl/plugins/env/env.sh delete mode 100644 test/fixtures/pkg/kubectl/plugins/env/plugin.yaml delete mode 100644 test/fixtures/pkg/kubectl/plugins/error/plugin.yaml create mode 100755 test/fixtures/pkg/kubectl/plugins/foo/kubectl-foo delete mode 100644 test/fixtures/pkg/kubectl/plugins/get/plugin.yaml delete mode 100644 test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml create mode 100755 test/fixtures/pkg/kubectl/plugins/kubectl-foo delete mode 100644 test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml create mode 100755 test/fixtures/pkg/kubectl/plugins/version/kubectl-version delete mode 100755 test/fixtures/pkg/kubectl/plugins2/hello/hello.sh delete mode 100644 test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml diff --git a/docs/.generated_docs b/docs/.generated_docs index 0fd748bcfc21..66a74b649c5c 100644 --- a/docs/.generated_docs +++ b/docs/.generated_docs @@ -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 diff --git a/docs/man/man1/kubectl-plugin-list.1 b/docs/man/man1/kubectl-plugin-list.1 new file mode 100644 index 000000000000..b6fd7a0f9896 --- /dev/null +++ b/docs/man/man1/kubectl-plugin-list.1 @@ -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. diff --git a/docs/user-guide/kubectl/kubectl_plugin_list.md b/docs/user-guide/kubectl/kubectl_plugin_list.md new file mode 100644 index 000000000000..b6fd7a0f9896 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_plugin_list.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. diff --git a/pkg/kubectl/BUILD b/pkg/kubectl/BUILD index 23b233660347..d378f60ff6f8 100644 --- a/pkg/kubectl/BUILD +++ b/pkg/kubectl/BUILD @@ -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", diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index 47cb4143424e..370b69fbb183 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -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", diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 025ba0459b48..48a9c4abc4b7 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -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. diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index eba20839faf8..16fda5740d1c 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -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 +} diff --git a/pkg/kubectl/cmd/plugin.go b/pkg/kubectl/cmd/plugin.go index feb35253146b..5592f0935b13 100644 --- a/pkg/kubectl/cmd/plugin.go +++ b/pkg/kubectl/cmd/plugin.go @@ -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 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. - 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.`) + 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 + cmdutil.CheckErr(o.Complete(cmd)) + cmdutil.CheckErr(o.Run()) + }, + } + + 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 +} + +func (o *PluginListOptions) Complete(cmd *cobra.Command) error { + o.Verifier = &CommandOverrideVerifier{ + root: cmd.Root(), + seenPlugins: make(map[string]string, 0), + } + return 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 } - envProvider := &plugins.MultiEnvProvider{ - &plugins.PluginCallerEnvProvider{}, - &plugins.OSEnvProvider{}, - &plugins.PluginDescriptorEnvProvider{ - Plugin: plugin, - }, - &flagsPluginEnvProvider{ - cmd: cmd, - }, - &factoryAttrsPluginEnvProvider{ - factory: f, - }, + if isFirstFile { + fmt.Fprintf(o.ErrOut, "The following kubectl-compatible plugins are available:\n\n") + pluginsFound = true + isFirstFile = false } - runningContext := plugins.RunningContext{ - IOStreams: streams, - Args: args, - EnvProvider: envProvider, - WorkingDir: plugin.Dir, + pluginPath := f.Name() + if !o.NameOnly { + pluginPath = filepath.Join(dir, pluginPath) } - 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()) - } + 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++ } - - cmdutil.CheckErr(err) } - }, + } } - for _, flag := range plugin.Flags { - cmd.Flags().StringP(flag.Name, flag.Shorthand, flag.DefValue, flag.Desc) + if !pluginsFound { + return fmt.Errorf("error: unable to find any kubectl plugins in your PATH") } - for _, childPlugin := range plugin.Tree { - cmd.AddCommand(NewCmdForPlugin(f, childPlugin, runner, streams)) + 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 cmd + return nil } -type flagsPluginEnvProvider struct { - cmd *cobra.Command +// 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 *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 +type CommandOverrideVerifier struct { + root *cobra.Command + seenPlugins map[string]string } -type factoryAttrsPluginEnvProvider struct { - factory cmdutil.Factory +// 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 (p *factoryAttrsPluginEnvProvider) Env() (plugins.EnvList, error) { - cmdNamespace, _, err := p.factory.ToRawKubeConfigLoader().Namespace() +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(), + + if m := info.Mode(); !m.IsDir() && m&0111 != 0 { + return true, nil } -} -func pluginRunner() plugins.PluginRunner { - return &plugins.ExecPluginRunner{} + return false, nil } diff --git a/pkg/kubectl/cmd/plugin_test.go b/pkg/kubectl/cmd/plugin_test.go deleted file mode 100644 index c47646dd8b44..000000000000 --- a/pkg/kubectl/cmd/plugin_test.go +++ /dev/null @@ -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()) - } - }) - } -} diff --git a/pkg/kubectl/cmd/run_test.go b/pkg/kubectl/cmd/run_test.go index a0c0f56b2fac..e8ab3661c2c0 100644 --- a/pkg/kubectl/cmd/run_test.go +++ b/pkg/kubectl/cmd/run_test.go @@ -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) diff --git a/pkg/kubectl/cmd/testdata/plugin/kubectl-foo b/pkg/kubectl/cmd/testdata/plugin/kubectl-foo new file mode 100755 index 000000000000..651b7662dda4 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/plugin/kubectl-foo @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "I am plugin foo" diff --git a/pkg/kubectl/cmd/testdata/plugin/kubectl-version b/pkg/kubectl/cmd/testdata/plugin/kubectl-version new file mode 100755 index 000000000000..3718966b6892 --- /dev/null +++ b/pkg/kubectl/cmd/testdata/plugin/kubectl-version @@ -0,0 +1,4 @@ +#!/bin/bash + +# This plugin is a no-op and is used to test plugins +# that overshadow existing kubectl commands diff --git a/pkg/kubectl/plugins/BUILD b/pkg/kubectl/plugins/BUILD deleted file mode 100644 index a1f6329f0bbd..000000000000 --- a/pkg/kubectl/plugins/BUILD +++ /dev/null @@ -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", - ], -) diff --git a/pkg/kubectl/plugins/env.go b/pkg/kubectl/plugins/env.go deleted file mode 100644 index 7845b5fe99b6..000000000000 --- a/pkg/kubectl/plugins/env.go +++ /dev/null @@ -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]} -} diff --git a/pkg/kubectl/plugins/env_test.go b/pkg/kubectl/plugins/env_test.go deleted file mode 100644 index 780d239eea8c..000000000000 --- a/pkg/kubectl/plugins/env_test.go +++ /dev/null @@ -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) - } - }) - } - -} diff --git a/pkg/kubectl/plugins/examples/aging/aging.rb b/pkg/kubectl/plugins/examples/aging/aging.rb deleted file mode 100755 index 3a60e94c431e..000000000000 --- a/pkg/kubectl/plugins/examples/aging/aging.rb +++ /dev/null @@ -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 - diff --git a/pkg/kubectl/plugins/examples/aging/plugin.yaml b/pkg/kubectl/plugins/examples/aging/plugin.yaml deleted file mode 100644 index 87275dc095b2..000000000000 --- a/pkg/kubectl/plugins/examples/aging/plugin.yaml +++ /dev/null @@ -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 diff --git a/pkg/kubectl/plugins/examples/hello/plugin.yaml b/pkg/kubectl/plugins/examples/hello/plugin.yaml deleted file mode 100644 index 6df1241063e7..000000000000 --- a/pkg/kubectl/plugins/examples/hello/plugin.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: "hello" -shortDesc: "I say hello!" -command: "echo Hello plugins!" diff --git a/pkg/kubectl/plugins/loader.go b/pkg/kubectl/plugins/loader.go deleted file mode 100644 index 6cbaffc34de6..000000000000 --- a/pkg/kubectl/plugins/loader.go +++ /dev/null @@ -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 -} diff --git a/pkg/kubectl/plugins/loader_test.go b/pkg/kubectl/plugins/loader_test.go deleted file mode 100644 index 34f6b1c6abf2..000000000000 --- a/pkg/kubectl/plugins/loader_test.go +++ /dev/null @@ -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 -} diff --git a/pkg/kubectl/plugins/plugins.go b/pkg/kubectl/plugins/plugins.go deleted file mode 100644 index 7eca6a2f35f4..000000000000 --- a/pkg/kubectl/plugins/plugins.go +++ /dev/null @@ -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 -} diff --git a/pkg/kubectl/plugins/plugins_test.go b/pkg/kubectl/plugins/plugins_test.go deleted file mode 100644 index 9f707f54b124..000000000000 --- a/pkg/kubectl/plugins/plugins_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/pkg/kubectl/plugins/runner.go b/pkg/kubectl/plugins/runner.go deleted file mode 100644 index eff195564b65..000000000000 --- a/pkg/kubectl/plugins/runner.go +++ /dev/null @@ -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() -} diff --git a/pkg/kubectl/plugins/runner_test.go b/pkg/kubectl/plugins/runner_test.go deleted file mode 100644 index f6d5f32254c0..000000000000 --- a/pkg/kubectl/plugins/runner_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/test/cmd/plugins.sh b/test/cmd/plugins.sh index 523e4fe6a157..a389eaf6b40a 100755 --- a/test/cmd/plugins.sh +++ b/test/cmd/plugins.sh @@ -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"' - # no plugins - output_message=$(! kubectl plugin 2>&1) - kube::test::if_has_string "${output_message}" 'no plugins installed' - - # 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' - - # 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' + # 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' - # 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' + # 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' - # 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' + # no plugins + 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' - # 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' + # 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' - # 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 diff --git a/test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml deleted file mode 100644 index 270f0714e931..000000000000 --- a/test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml +++ /dev/null @@ -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!" diff --git a/test/fixtures/pkg/kubectl/plugins/env/env.sh b/test/fixtures/pkg/kubectl/plugins/env/env.sh deleted file mode 100755 index 44c9a4f33574..000000000000 --- a/test/fixtures/pkg/kubectl/plugins/env/env.sh +++ /dev/null @@ -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 diff --git a/test/fixtures/pkg/kubectl/plugins/env/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/env/plugin.yaml deleted file mode 100644 index b01d44a843cc..000000000000 --- a/test/fixtures/pkg/kubectl/plugins/env/plugin.yaml +++ /dev/null @@ -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" diff --git a/test/fixtures/pkg/kubectl/plugins/error/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/error/plugin.yaml deleted file mode 100644 index c8c08a26946d..000000000000 --- a/test/fixtures/pkg/kubectl/plugins/error/plugin.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: "error" -shortDesc: "The tremendous plugin that always fails!" -command: "false" diff --git a/test/fixtures/pkg/kubectl/plugins/foo/kubectl-foo b/test/fixtures/pkg/kubectl/plugins/foo/kubectl-foo new file mode 100755 index 000000000000..651b7662dda4 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/foo/kubectl-foo @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "I am plugin foo" diff --git a/test/fixtures/pkg/kubectl/plugins/get/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/get/plugin.yaml deleted file mode 100644 index 41b8ea954403..000000000000 --- a/test/fixtures/pkg/kubectl/plugins/get/plugin.yaml +++ /dev/null @@ -1,3 +0,0 @@ -name: "get" -shortDesc: "The wonderful new plugin-based get!" -command: "echo new-get" diff --git a/test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml deleted file mode 100644 index de32d95c723d..000000000000 --- a/test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml +++ /dev/null @@ -1,2 +0,0 @@ -name: "incomplete" -shortDesc: "Incomplete plugin" diff --git a/test/fixtures/pkg/kubectl/plugins/kubectl-foo b/test/fixtures/pkg/kubectl/plugins/kubectl-foo new file mode 100755 index 000000000000..651b7662dda4 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/kubectl-foo @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "I am plugin foo" diff --git a/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml deleted file mode 100644 index 889e7a6a75eb..000000000000 --- a/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml +++ /dev/null @@ -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 - diff --git a/test/fixtures/pkg/kubectl/plugins/version/kubectl-version b/test/fixtures/pkg/kubectl/plugins/version/kubectl-version new file mode 100755 index 000000000000..3f7c3b6c31c8 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/version/kubectl-version @@ -0,0 +1,4 @@ +#!/bin/bash + +# This plugin is a no-op and is used to test +# a plugin that overshadows an existing plugin diff --git a/test/fixtures/pkg/kubectl/plugins2/hello/hello.sh b/test/fixtures/pkg/kubectl/plugins2/hello/hello.sh deleted file mode 100755 index c3d7075b4cbc..000000000000 --- a/test/fixtures/pkg/kubectl/plugins2/hello/hello.sh +++ /dev/null @@ -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 "#######" diff --git a/test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml b/test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml deleted file mode 100644 index 52e80dc59fe9..000000000000 --- a/test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml +++ /dev/null @@ -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