mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 04:08:16 +00:00 
			
		
		
		
	Basic support for kubectl plugins
This commit is contained in:
		@@ -3679,5 +3679,49 @@ __EOF__
 | 
				
			|||||||
    kube::test::get_object_assert csr "{{range.items}}{{$id_field}}{{end}}" ''
 | 
					    kube::test::get_object_assert csr "{{range.items}}{{$id_field}}{{end}}" ''
 | 
				
			||||||
  fi
 | 
					  fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ###########
 | 
				
			||||||
 | 
					  # Plugins #
 | 
				
			||||||
 | 
					  ###########
 | 
				
			||||||
 | 
					  kube::log::status "Testing kubectl plugins"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # single plugins path
 | 
				
			||||||
 | 
					  output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl -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_not_string "${output_message}" 'The hello plugin'
 | 
				
			||||||
 | 
					  kube::test::if_has_not_string "${output_message}" 'Incomplete plugin'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # when overriding existing command, both appear in help. TODO handle this to not register plugins that override existing cmd.
 | 
				
			||||||
 | 
					  kube::test::if_has_string "${output_message}" 'get\s\+Display one or many resources'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # multiple plugins path
 | 
				
			||||||
 | 
					  output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/:test/fixtures/pkg/kubectl/plugins2/ kubectl -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 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 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 echo 2>&1)
 | 
				
			||||||
 | 
					  kube::test::if_has_string "${output_message}" 'This plugin works!'
 | 
				
			||||||
 | 
					  output_message=$(! KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/ kubectl hello 2>&1)
 | 
				
			||||||
 | 
					  kube::test::if_has_string "${output_message}" 'unknown command'
 | 
				
			||||||
 | 
					  output_message=$(! KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins/ kubectl error 2>&1)
 | 
				
			||||||
 | 
					  kube::test::if_has_string "${output_message}" 'error: exit status 1'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  kube::test::clear_all
 | 
					  kube::test::clear_all
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -190,6 +190,7 @@ filegroup(
 | 
				
			|||||||
        ":package-srcs",
 | 
					        ":package-srcs",
 | 
				
			||||||
        "//pkg/kubectl/cmd:all-srcs",
 | 
					        "//pkg/kubectl/cmd:all-srcs",
 | 
				
			||||||
        "//pkg/kubectl/metricsutil:all-srcs",
 | 
					        "//pkg/kubectl/metricsutil:all-srcs",
 | 
				
			||||||
 | 
					        "//pkg/kubectl/plugins:all-srcs",
 | 
				
			||||||
        "//pkg/kubectl/resource:all-srcs",
 | 
					        "//pkg/kubectl/resource:all-srcs",
 | 
				
			||||||
        "//pkg/kubectl/testing:all-srcs",
 | 
					        "//pkg/kubectl/testing:all-srcs",
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,6 +51,7 @@ go_library(
 | 
				
			|||||||
        "logs.go",
 | 
					        "logs.go",
 | 
				
			||||||
        "options.go",
 | 
					        "options.go",
 | 
				
			||||||
        "patch.go",
 | 
					        "patch.go",
 | 
				
			||||||
 | 
					        "plugin.go",
 | 
				
			||||||
        "portforward.go",
 | 
					        "portforward.go",
 | 
				
			||||||
        "proxy.go",
 | 
					        "proxy.go",
 | 
				
			||||||
        "replace.go",
 | 
					        "replace.go",
 | 
				
			||||||
@@ -93,6 +94,7 @@ go_library(
 | 
				
			|||||||
        "//pkg/kubectl/cmd/util:go_default_library",
 | 
					        "//pkg/kubectl/cmd/util:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/cmd/util/editor:go_default_library",
 | 
					        "//pkg/kubectl/cmd/util/editor:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/metricsutil:go_default_library",
 | 
					        "//pkg/kubectl/metricsutil:go_default_library",
 | 
				
			||||||
 | 
					        "//pkg/kubectl/plugins:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/resource:go_default_library",
 | 
					        "//pkg/kubectl/resource:go_default_library",
 | 
				
			||||||
        "//pkg/kubelet/types:go_default_library",
 | 
					        "//pkg/kubelet/types:go_default_library",
 | 
				
			||||||
        "//pkg/printers:go_default_library",
 | 
					        "//pkg/printers:go_default_library",
 | 
				
			||||||
@@ -173,6 +175,7 @@ go_test(
 | 
				
			|||||||
        "label_test.go",
 | 
					        "label_test.go",
 | 
				
			||||||
        "logs_test.go",
 | 
					        "logs_test.go",
 | 
				
			||||||
        "patch_test.go",
 | 
					        "patch_test.go",
 | 
				
			||||||
 | 
					        "plugin_test.go",
 | 
				
			||||||
        "portforward_test.go",
 | 
					        "portforward_test.go",
 | 
				
			||||||
        "replace_test.go",
 | 
					        "replace_test.go",
 | 
				
			||||||
        "rollingupdate_test.go",
 | 
					        "rollingupdate_test.go",
 | 
				
			||||||
@@ -207,6 +210,7 @@ go_test(
 | 
				
			|||||||
        "//pkg/kubectl:go_default_library",
 | 
					        "//pkg/kubectl:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/cmd/testing:go_default_library",
 | 
					        "//pkg/kubectl/cmd/testing:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/cmd/util:go_default_library",
 | 
					        "//pkg/kubectl/cmd/util:go_default_library",
 | 
				
			||||||
 | 
					        "//pkg/kubectl/plugins:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/resource:go_default_library",
 | 
					        "//pkg/kubectl/resource:go_default_library",
 | 
				
			||||||
        "//pkg/printers:go_default_library",
 | 
					        "//pkg/printers:go_default_library",
 | 
				
			||||||
        "//pkg/printers/internalversion:go_default_library",
 | 
					        "//pkg/printers/internalversion:go_default_library",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -347,6 +347,24 @@ func NewKubectlCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob
 | 
				
			|||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						pluginRunner := f.PluginRunner()
 | 
				
			||||||
 | 
						if len(loadedPlugins) > 0 {
 | 
				
			||||||
 | 
							pluginCmds := []*cobra.Command{}
 | 
				
			||||||
 | 
							for _, p := range loadedPlugins {
 | 
				
			||||||
 | 
								pluginCmds = append(pluginCmds, NewCmdForPlugin(p, pluginRunner, in, out, err))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							groups = append(groups, templates.CommandGroup{
 | 
				
			||||||
 | 
								Message:  "Plugins:",
 | 
				
			||||||
 | 
								Commands: pluginCmds,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	groups.Add(cmds)
 | 
						groups.Add(cmds)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	filters := []string{
 | 
						filters := []string{
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										54
									
								
								pkg/kubectl/cmd/plugin.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								pkg/kubectl/cmd/plugin.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					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 (
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"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"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										111
									
								
								pkg/kubectl/cmd/plugin_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								pkg/kubectl/cmd/plugin_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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.
 | 
					// LongDesc normalizes a command's long description to follow the conventions.
 | 
				
			||||||
func LongDesc(s string) string {
 | 
					func LongDesc(s string) string {
 | 
				
			||||||
 | 
						if len(s) == 0 {
 | 
				
			||||||
 | 
							return s
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return normalizer{s}.heredoc().markdown().trim().string
 | 
						return normalizer{s}.heredoc().markdown().trim().string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Examples normalizes a command's examples to follow the conventions.
 | 
					// Examples normalizes a command's examples to follow the conventions.
 | 
				
			||||||
func Examples(s string) string {
 | 
					func Examples(s string) string {
 | 
				
			||||||
 | 
						if len(s) == 0 {
 | 
				
			||||||
 | 
							return s
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return normalizer{s}.trim().indent().string
 | 
						return normalizer{s}.trim().indent().string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ go_library(
 | 
				
			|||||||
        "//pkg/kubectl:go_default_library",
 | 
					        "//pkg/kubectl:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/cmd/util:go_default_library",
 | 
					        "//pkg/kubectl/cmd/util:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/cmd/util/openapi:go_default_library",
 | 
					        "//pkg/kubectl/cmd/util/openapi:go_default_library",
 | 
				
			||||||
 | 
					        "//pkg/kubectl/plugins:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/resource:go_default_library",
 | 
					        "//pkg/kubectl/resource:go_default_library",
 | 
				
			||||||
        "//pkg/printers:go_default_library",
 | 
					        "//pkg/printers:go_default_library",
 | 
				
			||||||
        "//vendor/github.com/emicklei/go-restful/swagger:go_default_library",
 | 
					        "//vendor/github.com/emicklei/go-restful/swagger:go_default_library",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,6 +43,7 @@ import (
 | 
				
			|||||||
	"k8s.io/kubernetes/pkg/kubectl"
 | 
						"k8s.io/kubernetes/pkg/kubectl"
 | 
				
			||||||
	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
 | 
						cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
 | 
						"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
 | 
				
			||||||
 | 
						"k8s.io/kubernetes/pkg/kubectl/plugins"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/kubectl/resource"
 | 
						"k8s.io/kubernetes/pkg/kubectl/resource"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/printers"
 | 
						"k8s.io/kubernetes/pkg/printers"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -481,6 +482,14 @@ func (f *FakeFactory) SuggestedPodTemplateResources() []schema.GroupResource {
 | 
				
			|||||||
	return []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 {
 | 
					type fakeMixedFactory struct {
 | 
				
			||||||
	cmdutil.Factory
 | 
						cmdutil.Factory
 | 
				
			||||||
	tf        *TestFactory
 | 
						tf        *TestFactory
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,6 +38,7 @@ go_library(
 | 
				
			|||||||
        "//pkg/controller:go_default_library",
 | 
					        "//pkg/controller:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl:go_default_library",
 | 
					        "//pkg/kubectl:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/cmd/util/openapi:go_default_library",
 | 
					        "//pkg/kubectl/cmd/util/openapi:go_default_library",
 | 
				
			||||||
 | 
					        "//pkg/kubectl/plugins:go_default_library",
 | 
				
			||||||
        "//pkg/kubectl/resource:go_default_library",
 | 
					        "//pkg/kubectl/resource:go_default_library",
 | 
				
			||||||
        "//pkg/printers:go_default_library",
 | 
					        "//pkg/printers:go_default_library",
 | 
				
			||||||
        "//pkg/printers/internalversion: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"
 | 
						coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/kubectl"
 | 
						"k8s.io/kubernetes/pkg/kubectl"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
 | 
						"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
 | 
				
			||||||
 | 
						"k8s.io/kubernetes/pkg/kubectl/plugins"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/kubectl/resource"
 | 
						"k8s.io/kubernetes/pkg/kubectl/resource"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/printers"
 | 
						"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
 | 
						PrintObject(cmd *cobra.Command, mapper meta.RESTMapper, obj runtime.Object, out io.Writer) error
 | 
				
			||||||
	// One stop shopping for a Builder
 | 
						// One stop shopping for a Builder
 | 
				
			||||||
	NewBuilder() *resource.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 {
 | 
					func getGroupVersionKinds(gvks []schema.GroupVersionKind, group string) []schema.GroupVersionKind {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,12 +21,14 @@ package util
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/api/meta"
 | 
						"k8s.io/apimachinery/pkg/api/meta"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
						"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/runtime"
 | 
						"k8s.io/apimachinery/pkg/runtime"
 | 
				
			||||||
 | 
						"k8s.io/kubernetes/pkg/kubectl/plugins"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/kubectl/resource"
 | 
						"k8s.io/kubernetes/pkg/kubectl/resource"
 | 
				
			||||||
	"k8s.io/kubernetes/pkg/printers"
 | 
						"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))
 | 
						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{}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										48
									
								
								pkg/kubectl/plugins/BUILD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								pkg/kubectl/plugins/BUILD
									
									
									
									
									
										Normal file
									
								
							@@ -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"],
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										69
									
								
								pkg/kubectl/plugins/examples/aging/aging.rb
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										69
									
								
								pkg/kubectl/plugins/examples/aging/aging.rb
									
									
									
									
									
										Executable file
									
								
							@@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										8
									
								
								pkg/kubectl/plugins/examples/aging/plugin.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								pkg/kubectl/plugins/examples/aging/plugin.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
							
								
								
									
										3
									
								
								pkg/kubectl/plugins/examples/hello/plugin.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								pkg/kubectl/plugins/examples/hello/plugin.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					name: "hello"
 | 
				
			||||||
 | 
					shortDesc: "I say hello!"
 | 
				
			||||||
 | 
					command: "echo Hello plugins!"
 | 
				
			||||||
							
								
								
									
										192
									
								
								pkg/kubectl/plugins/loader.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								pkg/kubectl/plugins/loader.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										197
									
								
								pkg/kubectl/plugins/loader_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								pkg/kubectl/plugins/loader_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										58
									
								
								pkg/kubectl/plugins/plugins.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								pkg/kubectl/plugins/plugins.go
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
							
								
								
									
										70
									
								
								pkg/kubectl/plugins/plugins_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								pkg/kubectl/plugins/plugins_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										69
									
								
								pkg/kubectl/plugins/runner.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								pkg/kubectl/plugins/runner.go
									
									
									
									
									
										Normal file
									
								
							@@ -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()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										71
									
								
								pkg/kubectl/plugins/runner_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								pkg/kubectl/plugins/runner_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -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"
 | 
						RecommendedSchemaName       = "schema"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var RecommendedHomeFile = path.Join(homedir.HomeDir(), RecommendedHomeDir, RecommendedFileName)
 | 
					var (
 | 
				
			||||||
var RecommendedSchemaFile = path.Join(homedir.HomeDir(), RecommendedHomeDir, RecommendedSchemaName)
 | 
						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.
 | 
					// 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
 | 
					// Any future changes to RecommendedHomeFile and related are expected to add a migration rule here, in order to make
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								test/fixtures/pkg/kubectl/plugins/echo/plugin.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -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!"
 | 
				
			||||||
							
								
								
									
										3
									
								
								test/fixtures/pkg/kubectl/plugins/error/plugin.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/pkg/kubectl/plugins/error/plugin.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					name: "error"
 | 
				
			||||||
 | 
					shortDesc: "The tremendous plugin that always fails!"
 | 
				
			||||||
 | 
					command: "false"
 | 
				
			||||||
							
								
								
									
										3
									
								
								test/fixtures/pkg/kubectl/plugins/get/plugin.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/fixtures/pkg/kubectl/plugins/get/plugin.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					name: "get"
 | 
				
			||||||
 | 
					shortDesc: "The wonderful new plugin-based get!"
 | 
				
			||||||
 | 
					command: "echo new-get"
 | 
				
			||||||
							
								
								
									
										2
									
								
								test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								test/fixtures/pkg/kubectl/plugins/incomplete/plugin.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					name: "incomplete"
 | 
				
			||||||
 | 
					shortDesc: "Incomplete plugin"
 | 
				
			||||||
							
								
								
									
										19
									
								
								test/fixtures/pkg/kubectl/plugins2/hello/hello.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										19
									
								
								test/fixtures/pkg/kubectl/plugins2/hello/hello.sh
									
									
									
									
										vendored
									
									
										Executable file
									
								
							@@ -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 "#######"
 | 
				
			||||||
							
								
								
									
										7
									
								
								test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								test/fixtures/pkg/kubectl/plugins2/hello/plugin.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
		Reference in New Issue
	
	Block a user