diff --git a/docs/.generated_docs b/docs/.generated_docs index fac2e50ee0..a06f8132fd 100644 --- a/docs/.generated_docs +++ b/docs/.generated_docs @@ -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 diff --git a/docs/man/man1/kubectl-plugin.1 b/docs/man/man1/kubectl-plugin.1 new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/man/man1/kubectl-plugin.1 @@ -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. diff --git a/docs/user-guide/kubectl/kubectl_plugin.md b/docs/user-guide/kubectl/kubectl_plugin.md new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_plugin.md @@ -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. diff --git a/docs/yaml/kubectl/kubectl_plugin.yaml b/docs/yaml/kubectl/kubectl_plugin.yaml new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/yaml/kubectl/kubectl_plugin.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. diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index e3c752a78d..e5161b212e 100644 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -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 } diff --git a/pkg/kubectl/BUILD b/pkg/kubectl/BUILD index 33e5aa70f8..c2bf133174 100644 --- a/pkg/kubectl/BUILD +++ b/pkg/kubectl/BUILD @@ -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", ], diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index 122a337084..b99cb0951c 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -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", diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index d5e2271562..622ad9693f 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -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()) diff --git a/pkg/kubectl/cmd/plugin.go b/pkg/kubectl/cmd/plugin.go new file mode 100644 index 0000000000..808eb5508f --- /dev/null +++ b/pkg/kubectl/cmd/plugin.go @@ -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) + } + }, + } +} diff --git a/pkg/kubectl/cmd/plugin_test.go b/pkg/kubectl/cmd/plugin_test.go new file mode 100644 index 0000000000..bada4900c8 --- /dev/null +++ b/pkg/kubectl/cmd/plugin_test.go @@ -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()) + } + } +} diff --git a/pkg/kubectl/cmd/templates/normalizers.go b/pkg/kubectl/cmd/templates/normalizers.go index eedd089c20..db7b17a7d6 100644 --- a/pkg/kubectl/cmd/templates/normalizers.go +++ b/pkg/kubectl/cmd/templates/normalizers.go @@ -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 } diff --git a/pkg/kubectl/cmd/testing/BUILD b/pkg/kubectl/cmd/testing/BUILD index d03414078a..0f4c9785b0 100644 --- a/pkg/kubectl/cmd/testing/BUILD +++ b/pkg/kubectl/cmd/testing/BUILD @@ -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", diff --git a/pkg/kubectl/cmd/testing/fake.go b/pkg/kubectl/cmd/testing/fake.go index 9c40a3487e..393e7954f7 100644 --- a/pkg/kubectl/cmd/testing/fake.go +++ b/pkg/kubectl/cmd/testing/fake.go @@ -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 diff --git a/pkg/kubectl/cmd/util/BUILD b/pkg/kubectl/cmd/util/BUILD index a51ab0c68f..09a60ce991 100644 --- a/pkg/kubectl/cmd/util/BUILD +++ b/pkg/kubectl/cmd/util/BUILD @@ -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", diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 975896b6c9..5b9dfdd485 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -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 { diff --git a/pkg/kubectl/cmd/util/factory_builder.go b/pkg/kubectl/cmd/util/factory_builder.go index c8fc81b84a..f685ce9331 100644 --- a/pkg/kubectl/cmd/util/factory_builder.go +++ b/pkg/kubectl/cmd/util/factory_builder.go @@ -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{} +} diff --git a/pkg/kubectl/plugins/BUILD b/pkg/kubectl/plugins/BUILD new file mode 100644 index 0000000000..233e90fc02 --- /dev/null +++ b/pkg/kubectl/plugins/BUILD @@ -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"], +) diff --git a/pkg/kubectl/plugins/examples/aging/aging.rb b/pkg/kubectl/plugins/examples/aging/aging.rb new file mode 100755 index 0000000000..ed9a87fffc --- /dev/null +++ b/pkg/kubectl/plugins/examples/aging/aging.rb @@ -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 + diff --git a/pkg/kubectl/plugins/examples/aging/plugin.yaml b/pkg/kubectl/plugins/examples/aging/plugin.yaml new file mode 100644 index 0000000000..07dc2022a6 --- /dev/null +++ b/pkg/kubectl/plugins/examples/aging/plugin.yaml @@ -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 diff --git a/pkg/kubectl/plugins/examples/hello/plugin.yaml b/pkg/kubectl/plugins/examples/hello/plugin.yaml new file mode 100644 index 0000000000..6df1241063 --- /dev/null +++ b/pkg/kubectl/plugins/examples/hello/plugin.yaml @@ -0,0 +1,3 @@ +name: "hello" +shortDesc: "I say hello!" +command: "echo Hello plugins!" diff --git a/pkg/kubectl/plugins/loader.go b/pkg/kubectl/plugins/loader.go new file mode 100644 index 0000000000..6a34a150d7 --- /dev/null +++ b/pkg/kubectl/plugins/loader.go @@ -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 +} diff --git a/pkg/kubectl/plugins/loader_test.go b/pkg/kubectl/plugins/loader_test.go new file mode 100644 index 0000000000..83fc088d37 --- /dev/null +++ b/pkg/kubectl/plugins/loader_test.go @@ -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 +} diff --git a/pkg/kubectl/plugins/plugins.go b/pkg/kubectl/plugins/plugins.go new file mode 100644 index 0000000000..eab72b5467 --- /dev/null +++ b/pkg/kubectl/plugins/plugins.go @@ -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 diff --git a/pkg/kubectl/plugins/plugins_test.go b/pkg/kubectl/plugins/plugins_test.go new file mode 100644 index 0000000000..b7cb78b34e --- /dev/null +++ b/pkg/kubectl/plugins/plugins_test.go @@ -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) + } + } +} diff --git a/pkg/kubectl/plugins/runner.go b/pkg/kubectl/plugins/runner.go new file mode 100644 index 0000000000..c1c4233a0b --- /dev/null +++ b/pkg/kubectl/plugins/runner.go @@ -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() +} diff --git a/pkg/kubectl/plugins/runner_test.go b/pkg/kubectl/plugins/runner_test.go new file mode 100644 index 0000000000..9bf0bf4a09 --- /dev/null +++ b/pkg/kubectl/plugins/runner_test.go @@ -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) + } + } + +} diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/loader.go b/staging/src/k8s.io/client-go/tools/clientcmd/loader.go index 1fcc510382..6ac83b5c84 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/loader.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/loader.go @@ -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 diff --git a/test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml new file mode 100644 index 0000000000..270f0714e9 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml @@ -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!" diff --git a/test/fixtures/pkg/kubectl/plugins/error/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/error/plugin.yaml new file mode 100644 index 0000000000..c8c08a2694 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/error/plugin.yaml @@ -0,0 +1,3 @@ +name: "error" +shortDesc: "The tremendous plugin that always fails!" +command: "false" diff --git a/test/fixtures/pkg/kubectl/plugins/get/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/get/plugin.yaml new file mode 100644 index 0000000000..41b8ea9544 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/get/plugin.yaml @@ -0,0 +1,3 @@ +name: "get" +shortDesc: "The wonderful new plugin-based get!" +command: "echo new-get" diff --git a/test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml new file mode 100644 index 0000000000..de32d95c72 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml @@ -0,0 +1,2 @@ +name: "incomplete" +shortDesc: "Incomplete plugin" diff --git a/test/fixtures/pkg/kubectl/plugins2/hello/hello.sh b/test/fixtures/pkg/kubectl/plugins2/hello/hello.sh new file mode 100755 index 0000000000..86ade76047 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins2/hello/hello.sh @@ -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 "#######" diff --git a/test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml b/test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml new file mode 100644 index 0000000000..52e80dc59f --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml @@ -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