mirror of https://github.com/k3s-io/k3s
kubectl plugins have access config, global flags and environment
parent
da85262f70
commit
18cb56bf78
|
@ -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 #
|
||||
#################
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue