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-logs.1
|
||||||
docs/man/man1/kubectl-options.1
|
docs/man/man1/kubectl-options.1
|
||||||
docs/man/man1/kubectl-patch.1
|
docs/man/man1/kubectl-patch.1
|
||||||
|
docs/man/man1/kubectl-plugin-list.1
|
||||||
docs/man/man1/kubectl-plugin.1
|
docs/man/man1/kubectl-plugin.1
|
||||||
docs/man/man1/kubectl-port-forward.1
|
docs/man/man1/kubectl-port-forward.1
|
||||||
docs/man/man1/kubectl-proxy.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_options.md
|
||||||
docs/user-guide/kubectl/kubectl_patch.md
|
docs/user-guide/kubectl/kubectl_patch.md
|
||||||
docs/user-guide/kubectl/kubectl_plugin.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_port-forward.md
|
||||||
docs/user-guide/kubectl/kubectl_proxy.md
|
docs/user-guide/kubectl/kubectl_proxy.md
|
||||||
docs/user-guide/kubectl/kubectl_replace.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/explain:all-srcs",
|
||||||
"//pkg/kubectl/genericclioptions:all-srcs",
|
"//pkg/kubectl/genericclioptions:all-srcs",
|
||||||
"//pkg/kubectl/metricsutil:all-srcs",
|
"//pkg/kubectl/metricsutil:all-srcs",
|
||||||
"//pkg/kubectl/plugins:all-srcs",
|
|
||||||
"//pkg/kubectl/polymorphichelpers:all-srcs",
|
"//pkg/kubectl/polymorphichelpers:all-srcs",
|
||||||
"//pkg/kubectl/proxy:all-srcs",
|
"//pkg/kubectl/proxy:all-srcs",
|
||||||
"//pkg/kubectl/scheme:all-srcs",
|
"//pkg/kubectl/scheme:all-srcs",
|
||||||
|
|
|
@ -78,7 +78,6 @@ go_library(
|
||||||
"//pkg/kubectl/genericclioptions/printers:go_default_library",
|
"//pkg/kubectl/genericclioptions/printers:go_default_library",
|
||||||
"//pkg/kubectl/genericclioptions/resource:go_default_library",
|
"//pkg/kubectl/genericclioptions/resource:go_default_library",
|
||||||
"//pkg/kubectl/metricsutil:go_default_library",
|
"//pkg/kubectl/metricsutil:go_default_library",
|
||||||
"//pkg/kubectl/plugins:go_default_library",
|
|
||||||
"//pkg/kubectl/polymorphichelpers:go_default_library",
|
"//pkg/kubectl/polymorphichelpers:go_default_library",
|
||||||
"//pkg/kubectl/proxy:go_default_library",
|
"//pkg/kubectl/proxy:go_default_library",
|
||||||
"//pkg/kubectl/scheme:go_default_library",
|
"//pkg/kubectl/scheme:go_default_library",
|
||||||
|
@ -171,7 +170,6 @@ go_test(
|
||||||
"label_test.go",
|
"label_test.go",
|
||||||
"logs_test.go",
|
"logs_test.go",
|
||||||
"patch_test.go",
|
"patch_test.go",
|
||||||
"plugin_test.go",
|
|
||||||
"portforward_test.go",
|
"portforward_test.go",
|
||||||
"replace_test.go",
|
"replace_test.go",
|
||||||
"rollingupdate_test.go",
|
"rollingupdate_test.go",
|
||||||
|
@ -202,7 +200,6 @@ go_test(
|
||||||
"//pkg/kubectl/genericclioptions:go_default_library",
|
"//pkg/kubectl/genericclioptions:go_default_library",
|
||||||
"//pkg/kubectl/genericclioptions/printers:go_default_library",
|
"//pkg/kubectl/genericclioptions/printers:go_default_library",
|
||||||
"//pkg/kubectl/genericclioptions/resource:go_default_library",
|
"//pkg/kubectl/genericclioptions/resource:go_default_library",
|
||||||
"//pkg/kubectl/plugins:go_default_library",
|
|
||||||
"//pkg/kubectl/polymorphichelpers:go_default_library",
|
"//pkg/kubectl/polymorphichelpers:go_default_library",
|
||||||
"//pkg/kubectl/scheme:go_default_library",
|
"//pkg/kubectl/scheme:go_default_library",
|
||||||
"//pkg/kubectl/util/i18n:go_default_library",
|
"//pkg/kubectl/util/i18n:go_default_library",
|
||||||
|
|
|
@ -21,6 +21,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
utilflag "k8s.io/apiserver/pkg/util/flag"
|
utilflag "k8s.io/apiserver/pkg/util/flag"
|
||||||
|
@ -36,7 +42,6 @@ import (
|
||||||
"k8s.io/kubernetes/pkg/kubectl/cmd/wait"
|
"k8s.io/kubernetes/pkg/kubectl/cmd/wait"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -257,7 +262,100 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDefaultKubectlCommand() *cobra.Command {
|
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.
|
// NewKubectlCommand creates the `kubectl` command and its nested children.
|
||||||
|
|
|
@ -19,6 +19,7 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -37,6 +38,7 @@ import (
|
||||||
"k8s.io/kubernetes/pkg/api/testapi"
|
"k8s.io/kubernetes/pkg/api/testapi"
|
||||||
apitesting "k8s.io/kubernetes/pkg/api/testing"
|
apitesting "k8s.io/kubernetes/pkg/api/testing"
|
||||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||||
|
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/scheme"
|
"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")
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"path/filepath"
|
||||||
"syscall"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
||||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
"k8s.io/kubernetes/pkg/kubectl/genericclioptions"
|
||||||
"k8s.io/kubernetes/pkg/kubectl/plugins"
|
|
||||||
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
"k8s.io/kubernetes/pkg/kubectl/util/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
plugin_long = templates.LongDesc(`
|
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
|
Plugins provide extended functionality that is not part of the major command-line distribution.
|
||||||
and can even be provided by third-parties. Please refer to the documentation and
|
Please refer to the documentation and examples for more information about how write your own plugins.`)
|
||||||
examples for more information about how to install and 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 {
|
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{
|
cmd := &cobra.Command{
|
||||||
Use: "plugin NAME",
|
Use: "plugin [flags]",
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
Short: i18n.T("Runs a command-line plugin"),
|
Short: i18n.T("Provides utilities for interacting with plugins."),
|
||||||
Long: plugin_long,
|
Long: plugin_long,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if len(loadedPlugins) == 0 {
|
|
||||||
cmdutil.CheckErr(fmt.Errorf("no plugins installed."))
|
|
||||||
}
|
|
||||||
cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args)
|
cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(loadedPlugins) > 0 {
|
cmd.AddCommand(NewCmdPluginList(f, streams))
|
||||||
pluginRunner := pluginRunner()
|
|
||||||
for _, p := range loadedPlugins {
|
|
||||||
cmd.AddCommand(NewCmdForPlugin(f, p, pluginRunner, streams))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCmdForPlugin creates a command capable of running the provided plugin.
|
type PluginListOptions struct {
|
||||||
func NewCmdForPlugin(f cmdutil.Factory, plugin *plugins.Plugin, runner plugins.PluginRunner, streams genericclioptions.IOStreams) *cobra.Command {
|
Verifier PathVerifier
|
||||||
if !plugin.IsValid() {
|
NameOnly bool
|
||||||
return nil
|
|
||||||
|
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{
|
cmd := &cobra.Command{
|
||||||
Use: plugin.Name,
|
Use: "list",
|
||||||
Short: plugin.ShortDesc,
|
Short: "list all visible plugin executables on a user's PATH",
|
||||||
Long: templates.LongDesc(plugin.LongDesc),
|
Long: plugin_list_long,
|
||||||
Example: templates.Examples(plugin.Example),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if len(plugin.Command) == 0 {
|
cmdutil.CheckErr(o.Complete(cmd))
|
||||||
cmdutil.DefaultSubCommandRun(streams.ErrOut)(cmd, args)
|
cmdutil.CheckErr(o.Run())
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
envProvider := &plugins.MultiEnvProvider{
|
|
||||||
&plugins.PluginCallerEnvProvider{},
|
|
||||||
&plugins.OSEnvProvider{},
|
|
||||||
&plugins.PluginDescriptorEnvProvider{
|
|
||||||
Plugin: plugin,
|
|
||||||
},
|
|
||||||
&flagsPluginEnvProvider{
|
|
||||||
cmd: cmd,
|
|
||||||
},
|
|
||||||
&factoryAttrsPluginEnvProvider{
|
|
||||||
factory: f,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
runningContext := plugins.RunningContext{
|
cmd.Flags().BoolVar(&o.NameOnly, "name-only", o.NameOnly, "If true, display only the binary name of each plugin, rather than its full path")
|
||||||
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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type flagsPluginEnvProvider struct {
|
func (o *PluginListOptions) Complete(cmd *cobra.Command) error {
|
||||||
cmd *cobra.Command
|
o.Verifier = &CommandOverrideVerifier{
|
||||||
|
root: cmd.Root(),
|
||||||
|
seenPlugins: make(map[string]string, 0),
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *flagsPluginEnvProvider) Env() (plugins.EnvList, error) {
|
func (o *PluginListOptions) Run() error {
|
||||||
globalPrefix := "KUBECTL_PLUGINS_GLOBAL_FLAG_"
|
path := "PATH"
|
||||||
env := plugins.EnvList{}
|
if runtime.GOOS == "windows" {
|
||||||
p.cmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
|
path = "path"
|
||||||
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 factoryAttrsPluginEnvProvider struct {
|
pluginsFound := false
|
||||||
factory cmdutil.Factory
|
isFirstFile := true
|
||||||
}
|
pluginWarnings := 0
|
||||||
|
for _, dir := range filepath.SplitList(os.Getenv(path)) {
|
||||||
func (p *factoryAttrsPluginEnvProvider) Env() (plugins.EnvList, error) {
|
files, err := ioutil.ReadDir(dir)
|
||||||
cmdNamespace, _, err := p.factory.ToRawKubeConfigLoader().Namespace()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return plugins.EnvList{}, err
|
continue
|
||||||
}
|
}
|
||||||
return plugins.EnvList{
|
|
||||||
plugins.Env{N: "KUBECTL_PLUGINS_CURRENT_NAMESPACE", V: cmdNamespace},
|
for _, f := range files {
|
||||||
}, nil
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// pluginLoader loads plugins from a path set by the KUBECTL_PLUGINS_PATH env var.
|
// pathVerifier receives a path and determines if it is valid or not
|
||||||
// If this env var is not set, it defaults to
|
type PathVerifier interface {
|
||||||
// "~/.kube/plugins", plus
|
// Verify determines if a given path is valid
|
||||||
// "./kubectl/plugins" directory under the "data dir" directory specified by the XDG
|
Verify(path string) []error
|
||||||
// system directory structure spec for the given platform.
|
|
||||||
func pluginLoader() plugins.PluginLoader {
|
|
||||||
if len(os.Getenv("KUBECTL_PLUGINS_PATH")) > 0 {
|
|
||||||
return plugins.KubectlPluginsPathPluginLoader()
|
|
||||||
}
|
|
||||||
return plugins.TolerantMultiPluginLoader{
|
|
||||||
plugins.XDGDataDirsPluginLoader(),
|
|
||||||
plugins.UserDirPluginLoader(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func pluginRunner() plugins.PluginRunner {
|
type CommandOverrideVerifier struct {
|
||||||
return &plugins.ExecPluginRunner{}
|
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 false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
if strings.HasSuffix(info.Name(), ".exe") {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
tf.ClientConfigVal = defaultClientConfig()
|
||||||
|
|
||||||
streams, _, _, bufErr := genericclioptions.NewTestIOStreams()
|
streams, _, _, bufErr := genericclioptions.NewTestIOStreams()
|
||||||
|
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||||
|
bufErr.Write([]byte(str))
|
||||||
|
})
|
||||||
|
|
||||||
cmd := NewCmdRun(tf, streams)
|
cmd := NewCmdRun(tf, streams)
|
||||||
for flagName, flagValue := range test.flags {
|
for flagName, flagValue := range test.flags {
|
||||||
cmd.Flags().Set(flagName, flagValue)
|
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"
|
kube::log::status "Testing kubectl plugins"
|
||||||
|
|
||||||
# top-level plugin command
|
# test plugins that overwrite existing kubectl commands
|
||||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl -h 2>&1)
|
output_message=$(! PATH=${PATH}:"test/fixtures/pkg/kubectl/plugins/version" kubectl plugin list 2>&1)
|
||||||
kube::test::if_has_string "${output_message}" 'plugin\s\+Runs a command-line plugin'
|
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
|
# no plugins
|
||||||
output_message=$(! kubectl plugin 2>&1)
|
output_message=$(! PATH=${PATH}:"test/fixtures/pkg/kubectl/plugins/empty" kubectl plugin list 2>&1)
|
||||||
kube::test::if_has_string "${output_message}" 'no plugins installed'
|
kube::test::if_has_string "${output_message}" 'unable to find any kubectl plugins in your PATH'
|
||||||
|
|
||||||
# single plugins path
|
# attempt to run a plugin in the user's PATH
|
||||||
output_message=$(! KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin 2>&1)
|
output_message=$(PATH=${PATH}:"test/fixtures/pkg/kubectl/plugins" kubectl foo)
|
||||||
kube::test::if_has_string "${output_message}" 'echo\s\+Echoes for test-cmd'
|
kube::test::if_has_string "${output_message}" 'plugin foo'
|
||||||
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
|
# ensure that a kubectl command supersedes a plugin that overshadows it
|
||||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl plugin -h 2>&1)
|
output_message=$(PATH=${PATH}:"test/fixtures/pkg/kubectl/plugins/version" kubectl version)
|
||||||
kube::test::if_has_string "${output_message}" 'echo\s\+Echoes for test-cmd'
|
kube::test::if_has_string "${output_message}" 'Client Version'
|
||||||
kube::test::if_has_string "${output_message}" 'get\s\+The wonderful new plugin-based get!'
|
kube::test::if_has_not_string "${output_message}" 'overshadows an existing plugin'
|
||||||
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'
|
|
||||||
|
|
||||||
set +o nounset
|
set +o nounset
|
||||||
set +o errexit
|
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