mirror of https://github.com/k3s-io/k3s
Merge pull request #45981 from fabianofranz/kubectl_plugins_v1_part1
Automatic merge from submit-queue (batch tested with PRs 46033, 46122, 46053, 46018, 45981) Command tree and exported env in kubectl plugins This is part of `kubectl` plugins V1: - Adds support to several env vars passing context information to the plugin. Plugins can make use of them to connect to the REST API, access global flags, get the path of the plugin caller (so that `kubectl` can be invoked) and so on. Exported env vars include - `KUBECTL_PLUGINS_DESCRIPTOR_*`: the plugin descriptor fields - `KUBECTL_PLUGINS_GLOBAL_FLAG_*`: one for each global flag, useful to access namespace, context, etc - ~`KUBECTL_PLUGINS_REST_CLIENT_CONFIG_*`: one for most fields in `rest.Config` so that a REST client can be built.~ - `KUBECTL_PLUGINS_CALLER`: path to `kubectl` - `KUBECTL_PLUGINS_CURRENT_NAMESPACE`: namespace in use - Adds support for plugins as child of other plugins so that a tree of commands can be built (e.g. `kubectl myplugin list`, `kubectl myplugin add`, etc) **Release note**: ```release-note Added support to a hierarchy of kubectl plugins (a tree of plugins as children of other plugins). Added exported env vars to kubectl plugins so that plugin developers have access to global flags, namespace, the plugin descriptor and the full path to the caller binary. ``` @kubernetes/sig-cli-pr-reviewspull/6/head
commit
8fe818b2a1
|
@ -3769,6 +3769,30 @@ __EOF__
|
|||
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 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'
|
||||
|
||||
#################
|
||||
# Impersonation #
|
||||
#################
|
||||
|
|
|
@ -19,10 +19,10 @@ package cmd
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/plugins"
|
||||
|
@ -61,7 +61,7 @@ func NewCmdPlugin(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cobra.Co
|
|||
if len(loadedPlugins) > 0 {
|
||||
pluginRunner := f.PluginRunner()
|
||||
for _, p := range loadedPlugins {
|
||||
cmd.AddCommand(NewCmdForPlugin(p, pluginRunner, in, out, err))
|
||||
cmd.AddCommand(NewCmdForPlugin(f, p, pluginRunner, in, out, err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,28 +69,81 @@ func NewCmdPlugin(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cobra.Co
|
|||
}
|
||||
|
||||
// NewCmdForPlugin creates a command capable of running the provided plugin.
|
||||
func NewCmdForPlugin(plugin *plugins.Plugin, runner plugins.PluginRunner, in io.Reader, out, errout io.Writer) *cobra.Command {
|
||||
func NewCmdForPlugin(f cmdutil.Factory, plugin *plugins.Plugin, runner plugins.PluginRunner, in io.Reader, out, errout io.Writer) *cobra.Command {
|
||||
if !plugin.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &cobra.Command{
|
||||
cmd := &cobra.Command{
|
||||
Use: plugin.Name,
|
||||
Short: plugin.ShortDesc,
|
||||
Long: templates.LongDesc(plugin.LongDesc),
|
||||
Example: templates.Examples(plugin.Example),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := plugins.RunningContext{
|
||||
In: in,
|
||||
Out: out,
|
||||
ErrOut: errout,
|
||||
Args: args,
|
||||
Env: os.Environ(),
|
||||
WorkingDir: plugin.Dir,
|
||||
if len(plugin.Command) == 0 {
|
||||
cmdutil.DefaultSubCommandRun(errout)(cmd, args)
|
||||
return
|
||||
}
|
||||
if err := runner.Run(plugin, ctx); err != nil {
|
||||
|
||||
envProvider := &plugins.MultiEnvProvider{
|
||||
&plugins.PluginCallerEnvProvider{},
|
||||
&plugins.OSEnvProvider{},
|
||||
&plugins.PluginDescriptorEnvProvider{
|
||||
Plugin: plugin,
|
||||
},
|
||||
&flagsPluginEnvProvider{
|
||||
cmd: cmd,
|
||||
},
|
||||
&factoryAttrsPluginEnvProvider{
|
||||
factory: f,
|
||||
},
|
||||
}
|
||||
|
||||
runningContext := plugins.RunningContext{
|
||||
In: in,
|
||||
Out: out,
|
||||
ErrOut: errout,
|
||||
Args: args,
|
||||
EnvProvider: envProvider,
|
||||
WorkingDir: plugin.Dir,
|
||||
}
|
||||
|
||||
if err := runner.Run(plugin, runningContext); err != nil {
|
||||
cmdutil.CheckErr(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for _, childPlugin := range plugin.Tree {
|
||||
cmd.AddCommand(NewCmdForPlugin(f, childPlugin, runner, in, out, errout))
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
type flagsPluginEnvProvider struct {
|
||||
cmd *cobra.Command
|
||||
}
|
||||
|
||||
func (p *flagsPluginEnvProvider) Env() (plugins.EnvList, error) {
|
||||
prefix := "KUBECTL_PLUGINS_GLOBAL_FLAG_"
|
||||
env := plugins.EnvList{}
|
||||
p.cmd.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
env = append(env, plugins.FlagToEnv(flag, prefix))
|
||||
})
|
||||
return env, nil
|
||||
}
|
||||
|
||||
type factoryAttrsPluginEnvProvider struct {
|
||||
factory cmdutil.Factory
|
||||
}
|
||||
|
||||
func (p *factoryAttrsPluginEnvProvider) Env() (plugins.EnvList, error) {
|
||||
cmdNamespace, _, err := p.factory.DefaultNamespace()
|
||||
if err != nil {
|
||||
return plugins.EnvList{}, err
|
||||
}
|
||||
return plugins.EnvList{
|
||||
plugins.Env{N: "KUBECTL_PLUGINS_CURRENT_NAMESPACE", V: cmdNamespace},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/plugins"
|
||||
)
|
||||
|
@ -91,7 +92,8 @@ func TestPluginCmd(t *testing.T) {
|
|||
success: test.expectedSuccess,
|
||||
}
|
||||
|
||||
cmd := NewCmdForPlugin(test.plugin, runner, inBuf, outBuf, errBuf)
|
||||
f, _, _, _ := cmdtesting.NewAPIFactory()
|
||||
cmd := NewCmdForPlugin(f, test.plugin, runner, inBuf, outBuf, errBuf)
|
||||
if cmd == nil {
|
||||
if !test.expectedNilCmd {
|
||||
t.Fatalf("%s: command was unexpectedly not registered", test.name)
|
||||
|
|
|
@ -11,6 +11,7 @@ load(
|
|||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"env.go",
|
||||
"loader.go",
|
||||
"plugins.go",
|
||||
"runner.go",
|
||||
|
@ -19,6 +20,7 @@ go_library(
|
|||
deps = [
|
||||
"//vendor/github.com/ghodss/yaml:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/github.com/spf13/pflag:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library",
|
||||
],
|
||||
)
|
||||
|
@ -39,10 +41,12 @@ filegroup(
|
|||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"env_test.go",
|
||||
"loader_test.go",
|
||||
"plugins_test.go",
|
||||
"runner_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = ["//vendor/github.com/spf13/pflag:go_default_library"],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
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
|
||||
}
|
||||
|
||||
func (e Env) String() string {
|
||||
return fmt.Sprintf("%s=%s", e.N, e.V)
|
||||
}
|
||||
|
||||
// EnvList is a list of Env
|
||||
type EnvList []Env
|
||||
|
||||
func (e EnvList) Slice() []string {
|
||||
envs := []string{}
|
||||
for _, env := range e {
|
||||
envs = append(envs, env.String())
|
||||
}
|
||||
return envs
|
||||
}
|
||||
|
||||
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() (EnvList, error)
|
||||
}
|
||||
|
||||
// MultiEnvProvider is an EnvProvider for multiple env providers, returns on first error.
|
||||
type MultiEnvProvider []EnvProvider
|
||||
|
||||
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 provides env with the path to the caller binary (usually full path to 'kubectl').
|
||||
type PluginCallerEnvProvider struct{}
|
||||
|
||||
func (p *PluginCallerEnvProvider) Env() (EnvList, error) {
|
||||
caller, err := os.Executable()
|
||||
if err != nil {
|
||||
return EnvList{}, err
|
||||
}
|
||||
return EnvList{
|
||||
{"KUBECTL_PLUGINS_CALLER", caller},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PluginDescriptorEnvProvider provides env vars with information about the running plugin.
|
||||
type PluginDescriptorEnvProvider struct {
|
||||
Plugin *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 provides current environment from the operating system.
|
||||
type OSEnvProvider struct{}
|
||||
|
||||
func (p *OSEnvProvider) Env() (EnvList, error) {
|
||||
return fromSlice(os.Environ()), nil
|
||||
}
|
||||
|
||||
type EmptyEnvProvider struct{}
|
||||
|
||||
func (p *EmptyEnvProvider) Env() (EnvList, error) {
|
||||
return EnvList{}, nil
|
||||
}
|
||||
|
||||
func FlagToEnvName(flagName, prefix string) string {
|
||||
envName := strings.TrimPrefix(flagName, "--")
|
||||
envName = strings.ToUpper(envName)
|
||||
envName = strings.Replace(envName, "-", "_", -1)
|
||||
envName = prefix + envName
|
||||
return envName
|
||||
}
|
||||
|
||||
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]}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
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 {
|
||||
env Env
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
env: Env{"FOO", "BAR"},
|
||||
expected: "FOO=BAR",
|
||||
},
|
||||
{
|
||||
env: Env{"FOO", "BAR="},
|
||||
expected: "FOO=BAR=",
|
||||
},
|
||||
{
|
||||
env: Env{"FOO", ""},
|
||||
expected: "FOO=",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if s := test.env.String(); s != test.expected {
|
||||
t.Errorf("%v: expected string %q, got %q", test.env, test.expected, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvListToSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
env EnvList
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
env: EnvList{
|
||||
{"FOO", "BAR"},
|
||||
{"ZEE", "YO"},
|
||||
{"ONE", "1"},
|
||||
{"EQUALS", "=="},
|
||||
{"EMPTY", ""},
|
||||
},
|
||||
expected: []string{"FOO=BAR", "ZEE=YO", "ONE=1", "EQUALS===", "EMPTY="},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if s := test.env.Slice(); !reflect.DeepEqual(test.expected, s) {
|
||||
t.Errorf("%v: expected %v, got %v", test.env, test.expected, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddToEnvList(t *testing.T) {
|
||||
tests := []struct {
|
||||
add []string
|
||||
expected EnvList
|
||||
}{
|
||||
{
|
||||
add: []string{"FOO=BAR", "EMPTY=", "EQUALS===", "JUSTNAME"},
|
||||
expected: EnvList{
|
||||
{"FOO", "BAR"},
|
||||
{"EMPTY", ""},
|
||||
{"EQUALS", "=="},
|
||||
{"JUSTNAME", ""},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
env := EnvList{}.Merge(test.add...)
|
||||
if !reflect.DeepEqual(test.expected, env) {
|
||||
t.Errorf("%v: expected %v, got %v", test.add, test.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 {
|
||||
flag *pflag.Flag
|
||||
prefix string
|
||||
expected Env
|
||||
}{
|
||||
{
|
||||
flag: flags.Lookup("test"),
|
||||
expected: Env{"TEST", "ok"},
|
||||
},
|
||||
{
|
||||
flag: flags.Lookup("kube-master"),
|
||||
expected: Env{"KUBE_MASTER", "http://something"},
|
||||
},
|
||||
{
|
||||
prefix: "KUBECTL_",
|
||||
flag: flags.Lookup("from-file"),
|
||||
expected: Env{"KUBECTL_FROM_FILE", "nondefault"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if env := FlagToEnv(test.flag, test.prefix); !reflect.DeepEqual(test.expected, env) {
|
||||
t.Errorf("%v: expected %v, got %v", test.flag.Name, test.expected, env)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginDescriptorEnvProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
plugin *Plugin
|
||||
expected EnvList
|
||||
}{
|
||||
{
|
||||
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 _, test := range tests {
|
||||
provider := &PluginDescriptorEnvProvider{
|
||||
Plugin: test.plugin,
|
||||
}
|
||||
env, _ := provider.Env()
|
||||
if !reflect.DeepEqual(test.expected, env) {
|
||||
t.Errorf("%v: expected %v, got %v", test.plugin.Name, test.expected, env)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -88,8 +88,15 @@ func (l *DirectoryPluginLoader) Load() (Plugins, error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
plugin.Dir = filepath.Dir(path)
|
||||
plugin.DescriptorName = fileInfo.Name()
|
||||
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)
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
)
|
||||
|
||||
func TestSuccessfulDirectoryPluginLoader(t *testing.T) {
|
||||
tmp, err := setupValidPlugins(3)
|
||||
tmp, err := setupValidPlugins(3, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
@ -55,6 +55,9 @@ func TestSuccessfulDirectoryPluginLoader(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,7 +110,7 @@ func TestUnexistentDirectoryPluginLoader(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPluginsEnvVarPluginLoader(t *testing.T) {
|
||||
tmp, err := setupValidPlugins(1)
|
||||
tmp, err := setupValidPlugins(1, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
@ -172,19 +175,78 @@ shortDesc: The incomplete test plugin`
|
|||
}
|
||||
}
|
||||
|
||||
func setupValidPlugins(count int) (string, error) {
|
||||
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 <= count; i++ {
|
||||
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`, 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)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,10 @@ limitations under the License.
|
|||
|
||||
package plugins
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Plugin is the representation of a CLI extension (plugin).
|
||||
type Plugin struct {
|
||||
|
@ -28,11 +31,12 @@ type Plugin struct {
|
|||
// PluginDescription 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"`
|
||||
Name string `json:"name"`
|
||||
ShortDesc string `json:"shortDesc"`
|
||||
LongDesc string `json:"longDesc,omitempty"`
|
||||
Example string `json:"example,omitempty"`
|
||||
Command string `json:"command"`
|
||||
Tree []*Plugin `json:"tree,omitempty"`
|
||||
}
|
||||
|
||||
// PluginSource holds the location of a given plugin in the filesystem.
|
||||
|
@ -41,12 +45,23 @@ type Source struct {
|
|||
DescriptorName string `json:"-"`
|
||||
}
|
||||
|
||||
var IncompleteError = fmt.Errorf("incomplete plugin descriptor: name, shortDesc and command fields are required")
|
||||
var (
|
||||
IncompleteError = fmt.Errorf("incomplete plugin descriptor: name, shortDesc and command fields are required")
|
||||
InvalidNameError = fmt.Errorf("plugin name can't contain spaces")
|
||||
)
|
||||
|
||||
func (p Plugin) Validate() error {
|
||||
if len(p.Name) == 0 || len(p.ShortDesc) == 0 || len(p.Command) == 0 {
|
||||
if len(p.Name) == 0 || len(p.ShortDesc) == 0 || (len(p.Command) == 0 && len(p.Tree) == 0) {
|
||||
return IncompleteError
|
||||
}
|
||||
if strings.Index(p.Name, " ") > -1 {
|
||||
return InvalidNameError
|
||||
}
|
||||
for _, child := range p.Tree {
|
||||
if err := child.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -34,12 +34,12 @@ type PluginRunner interface {
|
|||
// in, out, and err streams, arguments and environment passed to it, and the
|
||||
// working directory.
|
||||
type RunningContext struct {
|
||||
In io.Reader
|
||||
Out io.Writer
|
||||
ErrOut io.Writer
|
||||
Args []string
|
||||
Env []string
|
||||
WorkingDir string
|
||||
In io.Reader
|
||||
Out io.Writer
|
||||
ErrOut io.Writer
|
||||
Args []string
|
||||
EnvProvider EnvProvider
|
||||
WorkingDir string
|
||||
}
|
||||
|
||||
// ExecPluginRunner is a PluginRunner that uses Go's os/exec to run plugins.
|
||||
|
@ -62,7 +62,11 @@ func (r *ExecPluginRunner) Run(plugin *Plugin, ctx RunningContext) error {
|
|||
cmd.Stdout = ctx.Out
|
||||
cmd.Stderr = ctx.ErrOut
|
||||
|
||||
cmd.Env = ctx.Env
|
||||
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)
|
||||
|
|
|
@ -61,8 +61,9 @@ func TestExecRunner(t *testing.T) {
|
|||
}
|
||||
|
||||
ctx := RunningContext{
|
||||
Out: outBuf,
|
||||
WorkingDir: ".",
|
||||
Out: outBuf,
|
||||
WorkingDir: ".",
|
||||
EnvProvider: &EmptyEnvProvider{},
|
||||
}
|
||||
|
||||
runner := &ExecPluginRunner{}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/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
|
|
@ -0,0 +1,3 @@
|
|||
name: env
|
||||
shortDesc: "The plugin envs plugin"
|
||||
command: "./env.sh"
|
|
@ -0,0 +1,13 @@
|
|||
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
|
||||
|
Loading…
Reference in New Issue