diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 234d9aa02fa..86a181fc501 100644 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -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 # ################# diff --git a/pkg/kubectl/cmd/plugin.go b/pkg/kubectl/cmd/plugin.go index 808eb5508f1..0ea15522871 100644 --- a/pkg/kubectl/cmd/plugin.go +++ b/pkg/kubectl/cmd/plugin.go @@ -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 } diff --git a/pkg/kubectl/cmd/plugin_test.go b/pkg/kubectl/cmd/plugin_test.go index bada4900c82..439ebea56e9 100644 --- a/pkg/kubectl/cmd/plugin_test.go +++ b/pkg/kubectl/cmd/plugin_test.go @@ -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) diff --git a/pkg/kubectl/plugins/BUILD b/pkg/kubectl/plugins/BUILD index 233e90fc02b..96ac7e138af 100644 --- a/pkg/kubectl/plugins/BUILD +++ b/pkg/kubectl/plugins/BUILD @@ -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"], ) diff --git a/pkg/kubectl/plugins/env.go b/pkg/kubectl/plugins/env.go new file mode 100644 index 00000000000..f7becb52b19 --- /dev/null +++ b/pkg/kubectl/plugins/env.go @@ -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]} +} diff --git a/pkg/kubectl/plugins/env_test.go b/pkg/kubectl/plugins/env_test.go new file mode 100644 index 00000000000..73a1440daf6 --- /dev/null +++ b/pkg/kubectl/plugins/env_test.go @@ -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) + } + } + +} diff --git a/pkg/kubectl/plugins/loader.go b/pkg/kubectl/plugins/loader.go index 9c1f54f3a99..74899e1d5ec 100644 --- a/pkg/kubectl/plugins/loader.go +++ b/pkg/kubectl/plugins/loader.go @@ -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) diff --git a/pkg/kubectl/plugins/loader_test.go b/pkg/kubectl/plugins/loader_test.go index 83fc088d378..96a0b4f7255 100644 --- a/pkg/kubectl/plugins/loader_test.go +++ b/pkg/kubectl/plugins/loader_test.go @@ -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) } diff --git a/pkg/kubectl/plugins/plugins.go b/pkg/kubectl/plugins/plugins.go index eab72b5467d..4b65b98f246 100644 --- a/pkg/kubectl/plugins/plugins.go +++ b/pkg/kubectl/plugins/plugins.go @@ -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 } diff --git a/pkg/kubectl/plugins/runner.go b/pkg/kubectl/plugins/runner.go index dd60f84db41..b30472193bc 100644 --- a/pkg/kubectl/plugins/runner.go +++ b/pkg/kubectl/plugins/runner.go @@ -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) diff --git a/pkg/kubectl/plugins/runner_test.go b/pkg/kubectl/plugins/runner_test.go index 1304b71cec2..c91ad2fb13d 100644 --- a/pkg/kubectl/plugins/runner_test.go +++ b/pkg/kubectl/plugins/runner_test.go @@ -61,8 +61,9 @@ func TestExecRunner(t *testing.T) { } ctx := RunningContext{ - Out: outBuf, - WorkingDir: ".", + Out: outBuf, + WorkingDir: ".", + EnvProvider: &EmptyEnvProvider{}, } runner := &ExecPluginRunner{} diff --git a/test/fixtures/pkg/kubectl/plugins/env/env.sh b/test/fixtures/pkg/kubectl/plugins/env/env.sh new file mode 100755 index 00000000000..b7d005519d3 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/env/env.sh @@ -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 diff --git a/test/fixtures/pkg/kubectl/plugins/env/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/env/plugin.yaml new file mode 100644 index 00000000000..f877c2f56d3 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/env/plugin.yaml @@ -0,0 +1,3 @@ +name: env +shortDesc: "The plugin envs plugin" +command: "./env.sh" diff --git a/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml new file mode 100644 index 00000000000..889e7a6a75e --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml @@ -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 +