diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 517be3361e9..86a181fc501 100644 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -3784,6 +3784,15 @@ __EOF__ 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/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 index 1c4df956843..889e7a6a75e 100644 --- a/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml +++ b/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml @@ -3,11 +3,11 @@ shortDesc: "Plugin with a tree of commands" tree: - name: "child1" shortDesc: "The first child of a tree" - command: echo child1 + command: echo child one - name: "child2" shortDesc: "The second child of a tree" - command: echo child2 + command: echo child two - name: "child3" shortDesc: "The third child of a tree" - command: echo child3 + command: echo child three