From da85262f70b51da64e82a8ca560a1364e97d26c1 Mon Sep 17 00:00:00 2001 From: Fabiano Franz Date: Fri, 5 May 2017 19:22:24 -0300 Subject: [PATCH] Adds support to a tree hierarchy of kubectl plugins --- hack/make-rules/test-cmd-util.sh | 15 ++++ pkg/kubectl/plugins/loader.go | 11 ++- pkg/kubectl/plugins/loader_test.go | 70 +++++++++++++++++-- pkg/kubectl/plugins/plugins.go | 31 +++++--- .../pkg/kubectl/plugins/tree/plugin.yaml | 13 ++++ 5 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 234d9aa02fa..517be3361e9 100644 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -3769,6 +3769,21 @@ __EOF__ 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' + # plugin tree + output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin tree 2>&1) + kube::test::if_has_string "${output_message}" 'Plugin with a tree of commands' + kube::test::if_has_string "${output_message}" 'child1\s\+The first child of a tree' + kube::test::if_has_string "${output_message}" 'child2\s\+The second child of a tree' + kube::test::if_has_string "${output_message}" 'child3\s\+The third child of a tree' + output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin tree child1 --help 2>&1) + kube::test::if_has_string "${output_message}" 'The first child of a tree' + kube::test::if_has_not_string "${output_message}" 'The second child' + kube::test::if_has_not_string "${output_message}" 'child2' + output_message=$(KUBECTL_PLUGINS_PATH=test/fixtures/pkg/kubectl/plugins kubectl plugin tree child1 2>&1) + kube::test::if_has_string "${output_message}" 'child one' + kube::test::if_has_not_string "${output_message}" 'child1' + kube::test::if_has_not_string "${output_message}" 'The first child' + ################# # Impersonation # ################# diff --git a/pkg/kubectl/plugins/loader.go b/pkg/kubectl/plugins/loader.go index 9c1f54f3a99..74899e1d5ec 100644 --- a/pkg/kubectl/plugins/loader.go +++ b/pkg/kubectl/plugins/loader.go @@ -88,8 +88,15 @@ func (l *DirectoryPluginLoader) Load() (Plugins, error) { return nil } - plugin.Dir = filepath.Dir(path) - plugin.DescriptorName = fileInfo.Name() + var setSource func(path string, fileInfo os.FileInfo, p *Plugin) + setSource = func(path string, fileInfo os.FileInfo, p *Plugin) { + p.Dir = filepath.Dir(path) + p.DescriptorName = fileInfo.Name() + for _, child := range p.Tree { + setSource(path, fileInfo, child) + } + } + setSource(path, fileInfo, plugin) glog.V(6).Infof("Plugin loaded: %s", plugin.Name) list = append(list, plugin) diff --git a/pkg/kubectl/plugins/loader_test.go b/pkg/kubectl/plugins/loader_test.go index 83fc088d378..96a0b4f7255 100644 --- a/pkg/kubectl/plugins/loader_test.go +++ b/pkg/kubectl/plugins/loader_test.go @@ -27,7 +27,7 @@ import ( ) func TestSuccessfulDirectoryPluginLoader(t *testing.T) { - tmp, err := setupValidPlugins(3) + tmp, err := setupValidPlugins(3, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -55,6 +55,9 @@ func TestSuccessfulDirectoryPluginLoader(t *testing.T) { if m, _ := regexp.MatchString("^echo plugin[123]$", plugin.Command); !m { t.Errorf("Unexpected plugin command %s", plugin.Command) } + if count := len(plugin.Tree); count != 0 { + t.Errorf("Unexpected number of loaded child plugins, wanted 0, got %d", count) + } } } @@ -107,7 +110,7 @@ func TestUnexistentDirectoryPluginLoader(t *testing.T) { } func TestPluginsEnvVarPluginLoader(t *testing.T) { - tmp, err := setupValidPlugins(1) + tmp, err := setupValidPlugins(1, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -172,19 +175,78 @@ shortDesc: The incomplete test plugin` } } -func setupValidPlugins(count int) (string, error) { +func TestDirectoryTreePluginLoader(t *testing.T) { + tmp, err := setupValidPlugins(1, 2) + 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 != 1 { + t.Errorf("Unexpected number of loaded plugins, wanted 1, got %d", count) + } + + for _, plugin := range plugins { + if m, _ := regexp.MatchString("^plugin1$", plugin.Name); !m { + t.Errorf("Unexpected plugin name %s", plugin.Name) + } + if m, _ := regexp.MatchString("^The plugin1 test plugin$", plugin.ShortDesc); !m { + t.Errorf("Unexpected plugin short desc %s", plugin.ShortDesc) + } + if m, _ := regexp.MatchString("^echo plugin1$", plugin.Command); !m { + t.Errorf("Unexpected plugin command %s", plugin.Command) + } + if count := len(plugin.Tree); count != 2 { + t.Errorf("Unexpected number of loaded child plugins, wanted 2, got %d", count) + } + for _, child := range plugin.Tree { + if m, _ := regexp.MatchString("^child[12]$", child.Name); !m { + t.Errorf("Unexpected plugin child name %s", child.Name) + } + if m, _ := regexp.MatchString("^The child[12] test plugin child of plugin1 of House Targaryen$", child.ShortDesc); !m { + t.Errorf("Unexpected plugin child short desc %s", child.ShortDesc) + } + if m, _ := regexp.MatchString("^echo child[12]$", child.Command); !m { + t.Errorf("Unexpected plugin child command %s", child.Command) + } + } + } +} + +func setupValidPlugins(nPlugins, nChildren 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++ { + for i := 1; i <= nPlugins; 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 nChildren > 0 { + descriptor += ` +tree:` + } + + for j := 1; j <= nChildren; j++ { + child := fmt.Sprintf("child%d", i) + descriptor += fmt.Sprintf(` + - name: %[1]s + shortDesc: The %[1]s test plugin child of %[2]s of House Targaryen + command: echo %[1]s`, child, name) + } + if err := os.Mkdir(filepath.Join(tmp, name), 0755); err != nil { return "", fmt.Errorf("unexpected os.Mkdir error: %v", err) } diff --git a/pkg/kubectl/plugins/plugins.go b/pkg/kubectl/plugins/plugins.go index eab72b5467d..4b65b98f246 100644 --- a/pkg/kubectl/plugins/plugins.go +++ b/pkg/kubectl/plugins/plugins.go @@ -16,7 +16,10 @@ limitations under the License. package plugins -import "fmt" +import ( + "fmt" + "strings" +) // Plugin is the representation of a CLI extension (plugin). type Plugin struct { @@ -28,11 +31,12 @@ type Plugin struct { // 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"` + Name string `json:"name"` + ShortDesc string `json:"shortDesc"` + LongDesc string `json:"longDesc,omitempty"` + Example string `json:"example,omitempty"` + Command string `json:"command"` + Tree []*Plugin `json:"tree,omitempty"` } // PluginSource holds the location of a given plugin in the filesystem. @@ -41,12 +45,23 @@ type Source struct { DescriptorName string `json:"-"` } -var IncompleteError = fmt.Errorf("incomplete plugin descriptor: name, shortDesc and command fields are required") +var ( + IncompleteError = fmt.Errorf("incomplete plugin descriptor: name, shortDesc and command fields are required") + InvalidNameError = fmt.Errorf("plugin name can't contain spaces") +) func (p Plugin) Validate() error { - if len(p.Name) == 0 || len(p.ShortDesc) == 0 || len(p.Command) == 0 { + if len(p.Name) == 0 || len(p.ShortDesc) == 0 || (len(p.Command) == 0 && len(p.Tree) == 0) { return IncompleteError } + if strings.Index(p.Name, " ") > -1 { + return InvalidNameError + } + for _, child := range p.Tree { + if err := child.Validate(); err != nil { + return err + } + } return nil } diff --git a/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml b/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml new file mode 100644 index 00000000000..1c4df956843 --- /dev/null +++ b/test/fixtures/pkg/kubectl/plugins/tree/plugin.yaml @@ -0,0 +1,13 @@ +name: "tree" +shortDesc: "Plugin with a tree of commands" +tree: + - name: "child1" + shortDesc: "The first child of a tree" + command: echo child1 + - name: "child2" + shortDesc: "The second child of a tree" + command: echo child2 + - name: "child3" + shortDesc: "The third child of a tree" + command: echo child3 +