mirror of https://github.com/k3s-io/k3s
Merge pull request #37499 from fabianofranz/kubectl_plugins
Automatic merge from submit-queue kubectl binary plugins **What this PR does / why we need it**: Introduces the ability to extend `kubectl` by adding third-party plugins that will be exposed through `kubectl`. Plugins are executable commands written in any language. To be included as a plugin, a binary or script file has to 1. be located under one of the supported plugin path locations: 1.1 `~/.kubectl/plugins` dir 1.2. one or more directory set in the `KUBECTL_PLUGINS_PATH` env var 1.3. the `kubectl/plugins` dir under one or more directory set in the `XDG_DATA_DIRS` env var, which defaults to `/usr/local/share:/usr/share` 2. in any of the plugin path above, have a subfolder with the plugin file(s) 3. in the subfolder, contain at least a `plugin.yaml` file that describes the plugin Example: ``` $ cat ~/.kube/plugins/myplugin/plugin.yaml name: "myplugin" shortDesc: "My plugin's short description" command: "echo Hello plugins!" $ kubectl myplugin Hello plugins! ``` ~~In case the plugin declares `tunnel: true`, the plugin engine will pass the `KUBECTL_PLUGIN_API_HOST` env var when calling the plugin binary. Plugins can then access the Kube REST API in "http://$KUBECTL_PLUGIN_API_HOST/api" using the same context currently in use by `kubectl`.~~ Test plugins are provided in `pkg/kubectl/plugins/examples`. Just copy (or symlink) the files to `~/.kube/plugins` to test. **Which issue this PR fixes**: Related to the discussions in the proposal document: https://github.com/kubernetes/kubernetes/pull/30086 and https://github.com/kubernetes/community/pull/122. **Release note**: ```release-note Introduces the ability to extend kubectl by adding third-party plugins. Developer preview, please refer to the documentation for instructions about how to use it. ```pull/6/head
commit
d4ece0abc3
|
@ -73,6 +73,7 @@ docs/man/man1/kubectl-label.1
|
|||
docs/man/man1/kubectl-logs.1
|
||||
docs/man/man1/kubectl-options.1
|
||||
docs/man/man1/kubectl-patch.1
|
||||
docs/man/man1/kubectl-plugin.1
|
||||
docs/man/man1/kubectl-port-forward.1
|
||||
docs/man/man1/kubectl-proxy.1
|
||||
docs/man/man1/kubectl-replace.1
|
||||
|
@ -162,6 +163,7 @@ docs/user-guide/kubectl/kubectl_label.md
|
|||
docs/user-guide/kubectl/kubectl_logs.md
|
||||
docs/user-guide/kubectl/kubectl_options.md
|
||||
docs/user-guide/kubectl/kubectl_patch.md
|
||||
docs/user-guide/kubectl/kubectl_plugin.md
|
||||
docs/user-guide/kubectl/kubectl_port-forward.md
|
||||
docs/user-guide/kubectl/kubectl_proxy.md
|
||||
docs/user-guide/kubectl/kubectl_replace.md
|
||||
|
@ -211,6 +213,7 @@ docs/yaml/kubectl/kubectl_label.yaml
|
|||
docs/yaml/kubectl/kubectl_logs.yaml
|
||||
docs/yaml/kubectl/kubectl_options.yaml
|
||||
docs/yaml/kubectl/kubectl_patch.yaml
|
||||
docs/yaml/kubectl/kubectl_plugin.yaml
|
||||
docs/yaml/kubectl/kubectl_port-forward.yaml
|
||||
docs/yaml/kubectl/kubectl_proxy.yaml
|
||||
docs/yaml/kubectl/kubectl_replace.yaml
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -3679,5 +3679,55 @@ __EOF__
|
|||
kube::test::get_object_assert csr "{{range.items}}{{$id_field}}{{end}}" ''
|
||||
fi
|
||||
|
||||
###########
|
||||
# Plugins #
|
||||
###########
|
||||
kube::log::status "Testing kubectl plugins"
|
||||
|
||||
# top-level plugin command
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl -h 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'plugin\s\+Runs a command-line plugin'
|
||||
|
||||
# no plugins
|
||||
output_message=$(! kubectl plugin 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'no plugins installed'
|
||||
|
||||
# single plugins path
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'echo\s\+Echoes for test-cmd'
|
||||
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
|
||||
output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl plugin -h 2>&1)
|
||||
kube::test::if_has_string "${output_message}" 'echo\s\+Echoes for test-cmd'
|
||||
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_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'
|
||||
|
||||
kube::test::clear_all
|
||||
}
|
||||
|
|
|
@ -190,6 +190,7 @@ filegroup(
|
|||
":package-srcs",
|
||||
"//pkg/kubectl/cmd:all-srcs",
|
||||
"//pkg/kubectl/metricsutil:all-srcs",
|
||||
"//pkg/kubectl/plugins:all-srcs",
|
||||
"//pkg/kubectl/resource:all-srcs",
|
||||
"//pkg/kubectl/testing:all-srcs",
|
||||
],
|
||||
|
|
|
@ -51,6 +51,7 @@ go_library(
|
|||
"logs.go",
|
||||
"options.go",
|
||||
"patch.go",
|
||||
"plugin.go",
|
||||
"portforward.go",
|
||||
"proxy.go",
|
||||
"replace.go",
|
||||
|
@ -93,6 +94,7 @@ go_library(
|
|||
"//pkg/kubectl/cmd/util:go_default_library",
|
||||
"//pkg/kubectl/cmd/util/editor:go_default_library",
|
||||
"//pkg/kubectl/metricsutil:go_default_library",
|
||||
"//pkg/kubectl/plugins:go_default_library",
|
||||
"//pkg/kubectl/resource:go_default_library",
|
||||
"//pkg/kubelet/types:go_default_library",
|
||||
"//pkg/printers:go_default_library",
|
||||
|
@ -173,6 +175,7 @@ go_test(
|
|||
"label_test.go",
|
||||
"logs_test.go",
|
||||
"patch_test.go",
|
||||
"plugin_test.go",
|
||||
"portforward_test.go",
|
||||
"replace_test.go",
|
||||
"rollingupdate_test.go",
|
||||
|
@ -207,6 +210,7 @@ go_test(
|
|||
"//pkg/kubectl:go_default_library",
|
||||
"//pkg/kubectl/cmd/testing:go_default_library",
|
||||
"//pkg/kubectl/cmd/util:go_default_library",
|
||||
"//pkg/kubectl/plugins:go_default_library",
|
||||
"//pkg/kubectl/resource:go_default_library",
|
||||
"//pkg/printers:go_default_library",
|
||||
"//pkg/printers/internalversion:go_default_library",
|
||||
|
|
|
@ -368,6 +368,7 @@ func NewKubectlCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob
|
|||
}
|
||||
|
||||
cmds.AddCommand(cmdconfig.NewCmdConfig(clientcmd.NewDefaultPathOptions(), out, err))
|
||||
cmds.AddCommand(NewCmdPlugin(f, in, out, err))
|
||||
cmds.AddCommand(NewCmdVersion(f, out))
|
||||
cmds.AddCommand(NewCmdApiVersions(f, out))
|
||||
cmds.AddCommand(NewCmdOptions())
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
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"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/plugins"
|
||||
"k8s.io/kubernetes/pkg/util/i18n"
|
||||
)
|
||||
|
||||
var (
|
||||
plugin_long = templates.LongDesc(`
|
||||
Runs a command-line plugin.
|
||||
|
||||
Plugins are subcommands that are not part of the major command-line distribution
|
||||
and can even be provided by third-parties. Please refer to the documentation and
|
||||
examples for more information about how to install and write your own plugins.`)
|
||||
)
|
||||
|
||||
// NewCmdPlugin creates the command that is the top-level for plugin commands.
|
||||
func NewCmdPlugin(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cobra.Command {
|
||||
// Loads plugins and create commands for each plugin identified
|
||||
loadedPlugins, loadErr := f.PluginLoader().Load()
|
||||
if loadErr != nil {
|
||||
glog.V(1).Infof("Unable to load plugins: %v", loadErr)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "plugin NAME",
|
||||
Short: i18n.T("Runs a command-line plugin"),
|
||||
Long: plugin_long,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(loadedPlugins) == 0 {
|
||||
cmdutil.CheckErr(fmt.Errorf("no plugins installed."))
|
||||
}
|
||||
cmdutil.DefaultSubCommandRun(err)(cmd, args)
|
||||
},
|
||||
}
|
||||
|
||||
if len(loadedPlugins) > 0 {
|
||||
pluginRunner := f.PluginRunner()
|
||||
for _, p := range loadedPlugins {
|
||||
cmd.AddCommand(NewCmdForPlugin(p, pluginRunner, in, out, err))
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if !plugin.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &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 err := runner.Run(plugin, ctx); err != nil {
|
||||
cmdutil.CheckErr(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"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 {
|
||||
inBuf := bytes.NewBuffer([]byte{})
|
||||
outBuf := bytes.NewBuffer([]byte{})
|
||||
errBuf := bytes.NewBuffer([]byte{})
|
||||
|
||||
cmdutil.BehaviorOnFatal(func(str string, code int) {
|
||||
errBuf.Write([]byte(str))
|
||||
})
|
||||
|
||||
runner := &mockPluginRunner{
|
||||
success: test.expectedSuccess,
|
||||
}
|
||||
|
||||
cmd := NewCmdForPlugin(test.plugin, runner, inBuf, outBuf, errBuf)
|
||||
if cmd == nil {
|
||||
if !test.expectedNilCmd {
|
||||
t.Fatalf("%s: command was unexpectedly not registered", test.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,11 +28,17 @@ const Indentation = ` `
|
|||
|
||||
// LongDesc normalizes a command's long description to follow the conventions.
|
||||
func LongDesc(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return normalizer{s}.heredoc().markdown().trim().string
|
||||
}
|
||||
|
||||
// Examples normalizes a command's examples to follow the conventions.
|
||||
func Examples(s string) string {
|
||||
if len(s) == 0 {
|
||||
return s
|
||||
}
|
||||
return normalizer{s}.trim().indent().string
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ go_library(
|
|||
"//pkg/kubectl:go_default_library",
|
||||
"//pkg/kubectl/cmd/util:go_default_library",
|
||||
"//pkg/kubectl/cmd/util/openapi:go_default_library",
|
||||
"//pkg/kubectl/plugins:go_default_library",
|
||||
"//pkg/kubectl/resource:go_default_library",
|
||||
"//pkg/printers:go_default_library",
|
||||
"//vendor/github.com/emicklei/go-restful/swagger:go_default_library",
|
||||
|
|
|
@ -43,6 +43,7 @@ import (
|
|||
"k8s.io/kubernetes/pkg/kubectl"
|
||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
"k8s.io/kubernetes/pkg/kubectl/plugins"
|
||||
"k8s.io/kubernetes/pkg/kubectl/resource"
|
||||
"k8s.io/kubernetes/pkg/printers"
|
||||
)
|
||||
|
@ -481,6 +482,14 @@ func (f *FakeFactory) SuggestedPodTemplateResources() []schema.GroupResource {
|
|||
return []schema.GroupResource{}
|
||||
}
|
||||
|
||||
func (f *FakeFactory) PluginLoader() plugins.PluginLoader {
|
||||
return &plugins.DummyPluginLoader{}
|
||||
}
|
||||
|
||||
func (f *FakeFactory) PluginRunner() plugins.PluginRunner {
|
||||
return &plugins.ExecPluginRunner{}
|
||||
}
|
||||
|
||||
type fakeMixedFactory struct {
|
||||
cmdutil.Factory
|
||||
tf *TestFactory
|
||||
|
|
|
@ -38,6 +38,7 @@ go_library(
|
|||
"//pkg/controller:go_default_library",
|
||||
"//pkg/kubectl:go_default_library",
|
||||
"//pkg/kubectl/cmd/util/openapi:go_default_library",
|
||||
"//pkg/kubectl/plugins:go_default_library",
|
||||
"//pkg/kubectl/resource:go_default_library",
|
||||
"//pkg/printers:go_default_library",
|
||||
"//pkg/printers/internalversion:go_default_library",
|
||||
|
|
|
@ -52,6 +52,7 @@ import (
|
|||
coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
|
||||
"k8s.io/kubernetes/pkg/kubectl"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
"k8s.io/kubernetes/pkg/kubectl/plugins"
|
||||
"k8s.io/kubernetes/pkg/kubectl/resource"
|
||||
"k8s.io/kubernetes/pkg/printers"
|
||||
)
|
||||
|
@ -240,6 +241,10 @@ type BuilderFactory interface {
|
|||
PrintObject(cmd *cobra.Command, mapper meta.RESTMapper, obj runtime.Object, out io.Writer) error
|
||||
// One stop shopping for a Builder
|
||||
NewBuilder() *resource.Builder
|
||||
// PluginLoader provides the implementation to be used to load cli plugins.
|
||||
PluginLoader() plugins.PluginLoader
|
||||
// PluginRunner provides the implementation to be used to run cli plugins.
|
||||
PluginRunner() plugins.PluginRunner
|
||||
}
|
||||
|
||||
func getGroupVersionKinds(gvks []schema.GroupVersionKind, group string) []schema.GroupVersionKind {
|
||||
|
|
|
@ -21,12 +21,14 @@ package util
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kubernetes/pkg/kubectl/plugins"
|
||||
"k8s.io/kubernetes/pkg/kubectl/resource"
|
||||
"k8s.io/kubernetes/pkg/printers"
|
||||
)
|
||||
|
@ -130,3 +132,22 @@ func (f *ring2Factory) NewBuilder() *resource.Builder {
|
|||
|
||||
return resource.NewBuilder(mapper, categoryExpander, typer, resource.ClientMapperFunc(f.objectMappingFactory.ClientForMapping), f.clientAccessFactory.Decoder(true))
|
||||
}
|
||||
|
||||
// PluginLoader loads plugins from a path set by the KUBECTL_PLUGINS_PATH env var.
|
||||
// If this env var is not set, it defaults to
|
||||
// "~/.kube/plugins", plus
|
||||
// "./kubectl/plugins" directory under the "data dir" directory specified by the XDG
|
||||
// system directory structure spec for the given platform.
|
||||
func (f *ring2Factory) PluginLoader() plugins.PluginLoader {
|
||||
if len(os.Getenv("KUBECTL_PLUGINS_PATH")) > 0 {
|
||||
return plugins.PluginsEnvVarPluginLoader()
|
||||
}
|
||||
return plugins.TolerantMultiPluginLoader{
|
||||
plugins.XDGDataPluginLoader(),
|
||||
plugins.UserDirPluginLoader(),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *ring2Factory) PluginRunner() plugins.PluginRunner {
|
||||
return &plugins.ExecPluginRunner{}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"loader.go",
|
||||
"plugins.go",
|
||||
"runner.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/ghodss/yaml:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/k8s.io/client-go/tools/clientcmd: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 = [
|
||||
"loader_test.go",
|
||||
"plugins_test.go",
|
||||
"runner_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -0,0 +1,69 @@
|
|||
#!/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
|
||||
|
||||
pods_json = `kubectl 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
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
name: "aging"
|
||||
shortDesc: "Aging shows pods by age"
|
||||
longDesc: >
|
||||
Aging shows pods from the current namespace by age.
|
||||
Once we have plugin support for global flags through
|
||||
env vars (planned for V1) we'll be able to switch
|
||||
between namespaces using the --namespace flag.
|
||||
command: ./aging.rb
|
|
@ -0,0 +1,3 @@
|
|||
name: "hello"
|
||||
shortDesc: "I say hello!"
|
||||
command: "echo Hello plugins!"
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
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"
|
||||
)
|
||||
|
||||
const PluginDescriptorFilename = "plugin.yaml"
|
||||
|
||||
// PluginLoader is capable of loading a list of plugin descriptions.
|
||||
type PluginLoader interface {
|
||||
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 fileInfo.IsDir() || fileInfo.Name() != PluginDescriptorFilename || walkErr != nil {
|
||||
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
|
||||
}
|
||||
|
||||
plugin.Dir = filepath.Dir(path)
|
||||
plugin.DescriptorName = fileInfo.Name()
|
||||
|
||||
glog.V(6).Infof("Plugin loaded: %s", plugin.Name)
|
||||
list = append(list, plugin)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// UserDirPluginLoader is 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 is 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
|
||||
}
|
||||
|
||||
// PluginsEnvVarPluginLoader is a PluginLoader that loads plugins from one or more
|
||||
// directories specified by the KUBECTL_PLUGINS_PATH env var.
|
||||
func PluginsEnvVarPluginLoader() PluginLoader {
|
||||
return PathFromEnvVarPluginLoader("KUBECTL_PLUGINS_PATH")
|
||||
}
|
||||
|
||||
// XDGDataPluginLoader is 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 XDGDataPluginLoader() PluginLoader {
|
||||
envVarName := "XDG_DATA_DIRS"
|
||||
if len(os.Getenv(envVarName)) > 0 {
|
||||
return PathFromEnvVarPluginLoader(envVarName, "kubectl", "plugins")
|
||||
}
|
||||
return TolerantMultiPluginLoader{
|
||||
&DirectoryPluginLoader{
|
||||
Directory: "/usr/local/share",
|
||||
},
|
||||
&DirectoryPluginLoader{
|
||||
Directory: "/usr/share",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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
|
||||
|
||||
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 loads nothing.
|
||||
type DummyPluginLoader struct{}
|
||||
|
||||
func (l *DummyPluginLoader) Load() (Plugins, error) {
|
||||
return Plugins{}, nil
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 TestPluginsEnvVarPluginLoader(t *testing.T) {
|
||||
tmp, err := setupValidPlugins(1)
|
||||
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 := PluginsEnvVarPluginLoader()
|
||||
|
||||
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 setupValidPlugins(count 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++ {
|
||||
name := fmt.Sprintf("plugin%d", i)
|
||||
descriptor := fmt.Sprintf(`
|
||||
name: %[1]s
|
||||
shortDesc: The %[1]s test plugin
|
||||
command: echo %[1]s`, 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
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
// Plugin is the representation of a CLI extension (plugin).
|
||||
type Plugin struct {
|
||||
Description
|
||||
Source
|
||||
Context RunningContext `json:"-"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// PluginSource holds the location of a given plugin in the filesystem.
|
||||
type Source struct {
|
||||
Dir string `json:"-"`
|
||||
DescriptorName string `json:"-"`
|
||||
}
|
||||
|
||||
var IncompleteError = fmt.Errorf("incomplete plugin descriptor: name, shortDesc and command fields are required")
|
||||
|
||||
func (p Plugin) Validate() error {
|
||||
if len(p.Name) == 0 || len(p.ShortDesc) == 0 || len(p.Command) == 0 {
|
||||
return IncompleteError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Plugin) IsValid() bool {
|
||||
return p.Validate() == nil
|
||||
}
|
||||
|
||||
// Plugins is a list of plugins.
|
||||
type Plugins []*Plugin
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
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 (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
tests := []struct {
|
||||
plugin Plugin
|
||||
expectedErr string
|
||||
expectedValid bool
|
||||
}{
|
||||
{
|
||||
plugin: Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
Command: "echo 1",
|
||||
},
|
||||
},
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
plugin: Plugin{
|
||||
Description: Description{
|
||||
Name: "test",
|
||||
ShortDesc: "The test",
|
||||
},
|
||||
},
|
||||
expectedErr: "incomplete",
|
||||
},
|
||||
{
|
||||
plugin: Plugin{},
|
||||
expectedErr: "incomplete",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if is := test.plugin.IsValid(); test.expectedValid != is {
|
||||
t.Errorf("%s: expected valid=%v, got %v", test.plugin.Name, test.expectedValid, is)
|
||||
}
|
||||
err := test.plugin.Validate()
|
||||
if len(test.expectedErr) > 0 {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected error, got none", test.plugin.Name)
|
||||
} else if !strings.Contains(err.Error(), test.expectedErr) {
|
||||
t.Errorf("%s: expected error containing %q, got %v", test.plugin.Name, test.expectedErr, err)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("%s: expected no error, got %v", test.plugin.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
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 (
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
In io.Reader
|
||||
Out io.Writer
|
||||
ErrOut io.Writer
|
||||
Args []string
|
||||
Env []string
|
||||
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(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
|
||||
|
||||
cmd.Env = ctx.Env
|
||||
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()
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
outBuf := bytes.NewBuffer([]byte{})
|
||||
|
||||
plugin := &Plugin{
|
||||
Description: Description{
|
||||
Name: test.name,
|
||||
ShortDesc: "Test Runner Plugin",
|
||||
Command: test.command,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := RunningContext{
|
||||
Out: outBuf,
|
||||
WorkingDir: ".",
|
||||
}
|
||||
|
||||
runner := &ExecPluginRunner{}
|
||||
err := runner.Run(plugin, ctx)
|
||||
|
||||
if outBuf.String() != test.expectedMsg {
|
||||
t.Errorf("%s: unexpected output: %q", test.name, outBuf.String())
|
||||
}
|
||||
|
||||
if err != nil && err.Error() != test.expectedErr {
|
||||
t.Errorf("%s: unexpected err output: %v", test.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -47,8 +47,11 @@ const (
|
|||
RecommendedSchemaName = "schema"
|
||||
)
|
||||
|
||||
var RecommendedHomeFile = path.Join(homedir.HomeDir(), RecommendedHomeDir, RecommendedFileName)
|
||||
var RecommendedSchemaFile = path.Join(homedir.HomeDir(), RecommendedHomeDir, RecommendedSchemaName)
|
||||
var (
|
||||
RecommendedConfigDir = path.Join(homedir.HomeDir(), RecommendedHomeDir)
|
||||
RecommendedHomeFile = path.Join(RecommendedConfigDir, RecommendedFileName)
|
||||
RecommendedSchemaFile = path.Join(RecommendedConfigDir, RecommendedSchemaName)
|
||||
)
|
||||
|
||||
// currentMigrationRules returns a map that holds the history of recommended home directories used in previous versions.
|
||||
// Any future changes to RecommendedHomeFile and related are expected to add a migration rule here, in order to make
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
name: "echo"
|
||||
shortDesc: "Echoes for test-cmd"
|
||||
longDesc: "Long description for the test-cmd echo plugin"
|
||||
command: "echo This plugin works!"
|
|
@ -0,0 +1,3 @@
|
|||
name: "error"
|
||||
shortDesc: "The tremendous plugin that always fails!"
|
||||
command: "false"
|
|
@ -0,0 +1,3 @@
|
|||
name: "get"
|
||||
shortDesc: "The wonderful new plugin-based get!"
|
||||
command: "echo new-get"
|
|
@ -0,0 +1,2 @@
|
|||
name: "incomplete"
|
||||
shortDesc: "Incomplete plugin"
|
|
@ -0,0 +1,19 @@
|
|||
#!/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.
|
||||
|
||||
echo "#######"
|
||||
echo "#hello#"
|
||||
echo "#######"
|
|
@ -0,0 +1,7 @@
|
|||
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