From 331080910cea2823e3438de6fce669860f82c66f Mon Sep 17 00:00:00 2001 From: "dr.max" Date: Fri, 26 Jul 2019 13:29:49 -0700 Subject: [PATCH] Implements Kn plugins re-using some code from kubectl plugins. (#249) This version contains the following: 1. wraps the main root Kn command to support plugin 2. plugins are any executable in kn's config new pluginDir variable which defaults to $PATH 3. plugins must have name kn-* 4. 'kn plugin list' sub-command to list found kn plugins 5. skips any kn plugins found with name that match core commands, e.g., kn-service would be ignored 6. can execute any valid kn plugins found, e.g., `kn valid` where the plugin file `kn-valid` is in path specified in 2. 7. unit tests (using gotest.tools) And is missing: 1. integration tests 2. plugin install command 3. plugin repository command 4. plugin / Knative server version negotiation 5. anything else we agree on in plugin req doc I plan to create issues for the things missing so we don't end up with an even bigger PR. It's already big as is but is a good MVP as per plugins requirement doc. --- cmd/kn/main.go | 16 +- docs/cmd/kn.md | 9 +- docs/cmd/kn_plugin.md | 35 +++ docs/cmd/kn_plugin_list.md | 38 +++ docs/cmd/kn_revision.md | 2 +- docs/cmd/kn_revision_delete.md | 2 +- docs/cmd/kn_revision_describe.md | 2 +- docs/cmd/kn_revision_list.md | 2 +- docs/cmd/kn_route.md | 2 +- docs/cmd/kn_route_describe.md | 2 +- docs/cmd/kn_route_list.md | 2 +- docs/cmd/kn_service.md | 2 +- docs/cmd/kn_service_create.md | 2 +- docs/cmd/kn_service_delete.md | 2 +- docs/cmd/kn_service_describe.md | 2 +- docs/cmd/kn_service_list.md | 2 +- docs/cmd/kn_service_update.md | 2 +- docs/cmd/kn_version.md | 2 +- pkg/kn/commands/plugin/path_verifier.go | 102 ++++++++ pkg/kn/commands/plugin/path_verifier_test.go | 112 ++++++++ pkg/kn/commands/plugin/plugin.go | 54 ++++ pkg/kn/commands/plugin/plugin_flags.go | 35 +++ pkg/kn/commands/plugin/plugin_flags_test.go | 47 ++++ pkg/kn/commands/plugin/plugin_handler.go | 136 ++++++++++ pkg/kn/commands/plugin/plugin_handler_test.go | 178 +++++++++++++ pkg/kn/commands/plugin/plugin_list.go | 224 ++++++++++++++++ pkg/kn/commands/plugin/plugin_list_test.go | 240 ++++++++++++++++++ pkg/kn/commands/plugin/plugin_test.go | 74 ++++++ pkg/kn/commands/plugin/plugin_test_helper.go | 69 +++++ .../{help_testing.go => testing_helper.go} | 10 + pkg/kn/commands/testing_helper_test.go | 56 ++++ pkg/kn/commands/types.go | 10 +- pkg/kn/core/root.go | 125 ++++++++- pkg/kn/core/root_test.go | 119 +++++++-- test/e2e/plugin_workflow_test.go | 129 ++++++++++ vendor/modules.txt | 2 +- 36 files changed, 1804 insertions(+), 44 deletions(-) create mode 100644 docs/cmd/kn_plugin.md create mode 100644 docs/cmd/kn_plugin_list.md create mode 100644 pkg/kn/commands/plugin/path_verifier.go create mode 100644 pkg/kn/commands/plugin/path_verifier_test.go create mode 100644 pkg/kn/commands/plugin/plugin.go create mode 100644 pkg/kn/commands/plugin/plugin_flags.go create mode 100644 pkg/kn/commands/plugin/plugin_flags_test.go create mode 100644 pkg/kn/commands/plugin/plugin_handler.go create mode 100644 pkg/kn/commands/plugin/plugin_handler_test.go create mode 100644 pkg/kn/commands/plugin/plugin_list.go create mode 100644 pkg/kn/commands/plugin/plugin_list_test.go create mode 100644 pkg/kn/commands/plugin/plugin_test.go create mode 100644 pkg/kn/commands/plugin/plugin_test_helper.go rename pkg/kn/commands/{help_testing.go => testing_helper.go} (87%) create mode 100644 pkg/kn/commands/testing_helper_test.go create mode 100644 test/e2e/plugin_workflow_test.go diff --git a/cmd/kn/main.go b/cmd/kn/main.go index 154f510560..878fa7311d 100644 --- a/cmd/kn/main.go +++ b/cmd/kn/main.go @@ -19,12 +19,26 @@ import ( "os" "github.com/knative/client/pkg/kn/core" + "github.com/spf13/viper" ) +func init() { + core.InitializeConfig() +} + +var err error + func main() { - err := core.NewKnCommand().Execute() + defer cleanup() + err = core.NewDefaultKnCommand().Execute() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } + +func cleanup() { + if err == nil { + viper.WriteConfig() + } +} diff --git a/docs/cmd/kn.md b/docs/cmd/kn.md index af684aa3cd..44c5ab7c4d 100644 --- a/docs/cmd/kn.md +++ b/docs/cmd/kn.md @@ -12,13 +12,16 @@ Manage your Knative building blocks: ### Options ``` - --config string config file (default is $HOME/.kn/config.yaml) - -h, --help help for kn - --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --config string kn config file (default is $HOME/.kn/config.yaml) + -h, --help help for kn + --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --lookup-plugins-in-path look for kn plugins in $PATH + --plugins-dir string kn plugins directory (default "~/.kn/plugins") ``` ### SEE ALSO +* [kn plugin](kn_plugin.md) - Plugin command group * [kn revision](kn_revision.md) - Revision command group * [kn route](kn_route.md) - Route command group * [kn service](kn_service.md) - Service command group diff --git a/docs/cmd/kn_plugin.md b/docs/cmd/kn_plugin.md new file mode 100644 index 0000000000..42ec057818 --- /dev/null +++ b/docs/cmd/kn_plugin.md @@ -0,0 +1,35 @@ +## kn plugin + +Plugin command group + +### Synopsis + +Provides utilities for interacting and managing with kn plugins. + +Plugins provide extended functionality that is not part of the core kn command-line distribution. +Please refer to the documentation and examples for more information about how write your own plugins. + +``` +kn plugin [flags] +``` + +### Options + +``` + -h, --help help for plugin + --lookup-plugins-in-path look for kn plugins in $PATH + --plugins-dir string kn plugins directory (default "~/.kn/plugins") +``` + +### Options inherited from parent commands + +``` + --config string kn config file (default is $HOME/.kn/config.yaml) + --kubeconfig string kubectl config file (default is $HOME/.kube/config) +``` + +### SEE ALSO + +* [kn](kn.md) - Knative client +* [kn plugin list](kn_plugin_list.md) - List all visible plugin executables + diff --git a/docs/cmd/kn_plugin_list.md b/docs/cmd/kn_plugin_list.md new file mode 100644 index 0000000000..138f61f7db --- /dev/null +++ b/docs/cmd/kn_plugin_list.md @@ -0,0 +1,38 @@ +## kn plugin list + +List all visible plugin executables + +### Synopsis + +List all visible plugin executables. + +Available plugin files are those that are: +- executable +- begin with "kn- +- anywhere on the path specified in Kn's config pluginDir variable, which: + * can be overridden with the --plugin-dir flag + +``` +kn plugin list [flags] +``` + +### Options + +``` + -h, --help help for list + --lookup-plugins-in-path look for kn plugins in $PATH + --name-only If true, display only the binary name of each plugin, rather than its full path + --plugins-dir string kn plugins directory (default "~/.kn/plugins") +``` + +### Options inherited from parent commands + +``` + --config string kn config file (default is $HOME/.kn/config.yaml) + --kubeconfig string kubectl config file (default is $HOME/.kube/config) +``` + +### SEE ALSO + +* [kn plugin](kn_plugin.md) - Plugin command group + diff --git a/docs/cmd/kn_revision.md b/docs/cmd/kn_revision.md index 452fdf3125..0c506cb27c 100644 --- a/docs/cmd/kn_revision.md +++ b/docs/cmd/kn_revision.md @@ -19,7 +19,7 @@ kn revision [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_revision_delete.md b/docs/cmd/kn_revision_delete.md index e333fb2639..c99ae4441c 100644 --- a/docs/cmd/kn_revision_delete.md +++ b/docs/cmd/kn_revision_delete.md @@ -28,7 +28,7 @@ kn revision delete NAME [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_revision_describe.md b/docs/cmd/kn_revision_describe.md index d99efee6ce..bd92407175 100644 --- a/docs/cmd/kn_revision_describe.md +++ b/docs/cmd/kn_revision_describe.md @@ -23,7 +23,7 @@ kn revision describe NAME [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_revision_list.md b/docs/cmd/kn_revision_list.md index 903931dfda..3d356f1978 100644 --- a/docs/cmd/kn_revision_list.md +++ b/docs/cmd/kn_revision_list.md @@ -42,7 +42,7 @@ kn revision list [name] [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_route.md b/docs/cmd/kn_route.md index 425e0b8291..46696f7060 100644 --- a/docs/cmd/kn_route.md +++ b/docs/cmd/kn_route.md @@ -19,7 +19,7 @@ kn route [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_route_describe.md b/docs/cmd/kn_route_describe.md index d9d542ee3a..f1da10275f 100644 --- a/docs/cmd/kn_route_describe.md +++ b/docs/cmd/kn_route_describe.md @@ -23,7 +23,7 @@ kn route describe NAME [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_route_list.md b/docs/cmd/kn_route_list.md index 07babbbfb1..68fd7073be 100644 --- a/docs/cmd/kn_route_list.md +++ b/docs/cmd/kn_route_list.md @@ -38,7 +38,7 @@ kn route list NAME [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_service.md b/docs/cmd/kn_service.md index 5b4a36f95c..f5a4c7c9a9 100644 --- a/docs/cmd/kn_service.md +++ b/docs/cmd/kn_service.md @@ -19,7 +19,7 @@ kn service [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_service_create.md b/docs/cmd/kn_service_create.md index 4768f0e38e..4f0a2bed05 100644 --- a/docs/cmd/kn_service_create.md +++ b/docs/cmd/kn_service_create.md @@ -60,7 +60,7 @@ kn service create NAME --image IMAGE [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_service_delete.md b/docs/cmd/kn_service_delete.md index f7c8602604..9d3ca0054b 100644 --- a/docs/cmd/kn_service_delete.md +++ b/docs/cmd/kn_service_delete.md @@ -31,7 +31,7 @@ kn service delete NAME [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_service_describe.md b/docs/cmd/kn_service_describe.md index 6c77c5f918..54645adb43 100644 --- a/docs/cmd/kn_service_describe.md +++ b/docs/cmd/kn_service_describe.md @@ -23,7 +23,7 @@ kn service describe NAME [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_service_list.md b/docs/cmd/kn_service_list.md index 3bbc2ffd9c..9d7b08eeda 100644 --- a/docs/cmd/kn_service_list.md +++ b/docs/cmd/kn_service_list.md @@ -38,7 +38,7 @@ kn service list [name] [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_service_update.md b/docs/cmd/kn_service_update.md index ea73cd5971..707a37652e 100644 --- a/docs/cmd/kn_service_update.md +++ b/docs/cmd/kn_service_update.md @@ -47,7 +47,7 @@ kn service update NAME [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/docs/cmd/kn_version.md b/docs/cmd/kn_version.md index a3114865a2..f64f90212b 100644 --- a/docs/cmd/kn_version.md +++ b/docs/cmd/kn_version.md @@ -19,7 +19,7 @@ kn version [flags] ### Options inherited from parent commands ``` - --config string config file (default is $HOME/.kn/config.yaml) + --config string kn config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) ``` diff --git a/pkg/kn/commands/plugin/path_verifier.go b/pkg/kn/commands/plugin/path_verifier.go new file mode 100644 index 0000000000..5ef806bf97 --- /dev/null +++ b/pkg/kn/commands/plugin/path_verifier.go @@ -0,0 +1,102 @@ +// Copyright © 2018 The Knative 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 plugin + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/spf13/cobra" +) + +// PathVerifier receives a path and determines if it is valid or not +type PathVerifier interface { + // Verify determines if a given path is valid + Verify(path string) []error +} + +// CommandOverrideVerifier verifies that existing kn commands are not overriden +type CommandOverrideVerifier struct { + Root *cobra.Command + SeenPlugins map[string]string +} + +// Verify implements PathVerifier and determines if a given path +// is valid depending on whether or not it overwrites an existing +// kn command path, or a previously seen plugin. +func (v *CommandOverrideVerifier) Verify(path string) []error { + if v.Root == nil { + return []error{fmt.Errorf("unable to verify path with nil root")} + } + + // extract the plugin binary name + segs := strings.Split(path, string(os.PathSeparator)) + binName := segs[len(segs)-1] + + cmdPath := strings.Split(binName, "-") + if len(cmdPath) > 1 { + // the first argument is always "kn" for a plugin binary + cmdPath = cmdPath[1:] + } + + errors := []error{} + isExec, err := isExecutable(path) + if err == nil && !isExec { + errors = append(errors, fmt.Errorf("warning: %s identified as a kn plugin, but it is not executable", path)) + } else if err != nil { + errors = append(errors, fmt.Errorf("error: unable to identify %s as an executable file: %v", path, err)) + } + + if existingPath, ok := v.SeenPlugins[binName]; ok { + errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named plugin: %s", path, existingPath)) + } else { + v.SeenPlugins[binName] = path + } + + cmd, _, err := v.Root.Find(cmdPath) + if err == nil { + errors = append(errors, fmt.Errorf("warning: %s overwrites existing command: %q", binName, cmd.CommandPath())) + } + + return errors +} + +// Private functions + +func isExecutable(fullPath string) (bool, error) { + info, err := os.Stat(fullPath) + if err != nil { + return false, err + } + + if runtime.GOOS == "windows" { + fileExt := strings.ToLower(filepath.Ext(fullPath)) + + switch fileExt { + case ".bat", ".cmd", ".com", ".exe", ".ps1": + return true, nil + } + return false, nil + } + + if m := info.Mode(); !m.IsDir() && m&0111 != 0 { + return true, nil + } + + return false, nil +} diff --git a/pkg/kn/commands/plugin/path_verifier_test.go b/pkg/kn/commands/plugin/path_verifier_test.go new file mode 100644 index 0000000000..e407c68824 --- /dev/null +++ b/pkg/kn/commands/plugin/path_verifier_test.go @@ -0,0 +1,112 @@ +// Copyright © 2018 The Knative 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 plugin + +import ( + "fmt" + "strings" + "testing" + + "github.com/knative/client/pkg/kn/commands" + "github.com/spf13/cobra" + "gotest.tools/assert" +) + +func TestCommandOverrideVerifier(t *testing.T) { + var ( + pluginPath string + rootCmd *cobra.Command + verifier *CommandOverrideVerifier + ) + + setup := func(t *testing.T) { + knParams := &commands.KnParams{} + rootCmd, _, _ = commands.CreateTestKnCommand(NewPluginCommand(knParams), knParams) + verifier = &CommandOverrideVerifier{ + Root: rootCmd, + SeenPlugins: make(map[string]string), + } + } + + cleanup := func(t *testing.T) { + if pluginPath != "" { + DeleteTestPlugin(t, pluginPath) + } + } + + t.Run("with nil root command", func(t *testing.T) { + t.Run("returns error verifying path", func(t *testing.T) { + setup(t) + defer cleanup(t) + verifier.Root = nil + + errs := verifier.Verify(pluginPath) + assert.Assert(t, len(errs) == 1) + assert.Assert(t, errs[0] != nil) + assert.Assert(t, strings.Contains(errs[0].Error(), "unable to verify path with nil root")) + }) + }) + + t.Run("with root command", func(t *testing.T) { + t.Run("when plugin in path not executable", func(t *testing.T) { + setup(t) + defer cleanup(t) + pluginPath = CreateTestPlugin(t, KnTestPluginName, KnTestPluginScript, FileModeReadable) + + t.Run("fails with not executable error", func(t *testing.T) { + errs := verifier.Verify(pluginPath) + assert.Assert(t, len(errs) == 1) + assert.Assert(t, errs[0] != nil) + errorMsg := fmt.Sprintf("warning: %s identified as a kn plugin, but it is not executable", pluginPath) + assert.Assert(t, strings.Contains(errs[0].Error(), errorMsg)) + }) + }) + + t.Run("when kn plugin in path is executable", func(t *testing.T) { + setup(t) + defer cleanup(t) + pluginPath = CreateTestPlugin(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable) + + t.Run("when kn plugin in path shadows another", func(t *testing.T) { + var shadowPluginPath = CreateTestPlugin(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable) + verifier.SeenPlugins[KnTestPluginName] = pluginPath + defer DeleteTestPlugin(t, shadowPluginPath) + + t.Run("fails with overshadowed error", func(t *testing.T) { + errs := verifier.Verify(shadowPluginPath) + assert.Assert(t, len(errs) == 1) + assert.Assert(t, errs[0] != nil) + errorMsg := fmt.Sprintf("warning: %s is overshadowed by a similarly named plugin: %s", shadowPluginPath, pluginPath) + assert.Assert(t, strings.Contains(errs[0].Error(), errorMsg)) + }) + }) + }) + + t.Run("when kn plugin in path overwrites existing command", func(t *testing.T) { + setup(t) + defer cleanup(t) + var overwritingPluginPath = CreateTestPlugin(t, "kn-plugin", KnTestPluginScript, FileModeExecutable) + defer DeleteTestPlugin(t, overwritingPluginPath) + + t.Run("fails with overwrites error", func(t *testing.T) { + errs := verifier.Verify(overwritingPluginPath) + assert.Assert(t, len(errs) == 1) + assert.Assert(t, errs[0] != nil) + errorMsg := fmt.Sprintf("warning: %s overwrites existing command: %q", "kn-plugin", "kn plugin") + assert.Assert(t, strings.Contains(errs[0].Error(), errorMsg)) + }) + }) + }) +} diff --git a/pkg/kn/commands/plugin/plugin.go b/pkg/kn/commands/plugin/plugin.go new file mode 100644 index 0000000000..690912ed40 --- /dev/null +++ b/pkg/kn/commands/plugin/plugin.go @@ -0,0 +1,54 @@ +// Copyright © 2018 The Knative 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 plugin + +import ( + "github.com/knative/client/pkg/kn/commands" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func NewPluginCommand(p *commands.KnParams) *cobra.Command { + pluginCmd := &cobra.Command{ + Use: "plugin", + Short: "Plugin command group", + Long: `Provides utilities for interacting and managing with kn plugins. + +Plugins provide extended functionality that is not part of the core kn command-line distribution. +Please refer to the documentation and examples for more information about how write your own plugins.`, + } + + AddPluginFlags(pluginCmd) + BindPluginsFlagToViper(pluginCmd) + + pluginCmd.AddCommand(NewPluginListCommand(p)) + + return pluginCmd +} + +// AddPluginFlags plugins-dir and lookup-plugins-in-path to cmd +func AddPluginFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&commands.Cfg.PluginsDir, "plugins-dir", "~/.kn/plugins", "kn plugins directory") + cmd.Flags().BoolVar(&commands.Cfg.LookupPluginsInPath, "lookup-plugins-in-path", false, "look for kn plugins in $PATH") +} + +// BindPluginsFlagToViper bind and set default with viper for plugins flags +func BindPluginsFlagToViper(cmd *cobra.Command) { + viper.BindPFlag("pluginsDir", cmd.Flags().Lookup("plugins-dir")) + viper.BindPFlag("lookupPluginsInPath", cmd.Flags().Lookup("lookup-plugins-in-path")) + + viper.SetDefault("pluginsDir", "~/.kn/plugins") + viper.SetDefault("lookupPluginsInPath", false) +} diff --git a/pkg/kn/commands/plugin/plugin_flags.go b/pkg/kn/commands/plugin/plugin_flags.go new file mode 100644 index 0000000000..b15852084f --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_flags.go @@ -0,0 +1,35 @@ +// Copyright © 2018 The Knative 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 plugin + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +// PluginFlags contains all PLugin commands flags +type PluginFlags struct { + NameOnly bool + + Verifier PathVerifier + PluginPaths []string + + genericclioptions.IOStreams +} + +// AddPluginFlags adds the various flags to plugin command +func (p *PluginFlags) AddPluginFlags(command *cobra.Command) { + command.Flags().BoolVar(&p.NameOnly, "name-only", false, "If true, display only the binary name of each plugin, rather than its full path") +} diff --git a/pkg/kn/commands/plugin/plugin_flags_test.go b/pkg/kn/commands/plugin/plugin_flags_test.go new file mode 100644 index 0000000000..c4ecc9ee0c --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_flags_test.go @@ -0,0 +1,47 @@ +// Copyright © 2018 The Knative 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 plugin + +import ( + "testing" + + "github.com/spf13/cobra" + "gotest.tools/assert" +) + +func TestAddPluginFlags(t *testing.T) { + var ( + pluginFlags *PluginFlags + cmd *cobra.Command + ) + + setup := func() { + pluginFlags = &PluginFlags{} + + cmd = &cobra.Command{} + } + + t.Run("adds plugin flag", func(t *testing.T) { + setup() + pluginFlags.AddPluginFlags(cmd) + + assert.Assert(t, pluginFlags != nil) + assert.Assert(t, cmd.Flags() != nil) + + nameOnly, err := cmd.Flags().GetBool("name-only") + assert.Assert(t, err == nil) + assert.Assert(t, nameOnly == false) + }) +} diff --git a/pkg/kn/commands/plugin/plugin_handler.go b/pkg/kn/commands/plugin/plugin_handler.go new file mode 100644 index 0000000000..28bedee678 --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_handler.go @@ -0,0 +1,136 @@ +// Copyright © 2019 The Knative 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 plugin + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" +) + +// PluginHandler is capable of parsing command line arguments +// and performing executable filename lookups to search +// for valid plugin files, and execute found plugins. +type PluginHandler interface { + // exists at the given filename, or a boolean false. + // Lookup will iterate over a list of given prefixes + // in order to recognize valid plugin filenames. + // The first filepath to match a prefix is returned. + Lookup(name string) (string, bool) + // Execute receives an executable's filepath, a slice + // of arguments, and a slice of environment variables + // to relay to the executable. + Execute(executablePath string, cmdArgs, environment []string) error +} + +// DefaultPluginHandler implements PluginHandler +type DefaultPluginHandler struct { + ValidPrefixes []string + PluginsDir string + LookupPluginsInPath bool +} + +// NewDefaultPluginHandler instantiates the DefaultPluginHandler with a list of +// given filename prefixes used to identify valid plugin filenames. +func NewDefaultPluginHandler(validPrefixes []string, pluginsDir string, lookupPluginsInPath bool) *DefaultPluginHandler { + return &DefaultPluginHandler{ + ValidPrefixes: validPrefixes, + PluginsDir: pluginsDir, + LookupPluginsInPath: lookupPluginsInPath, + } +} + +// Lookup implements PluginHandler +func (h *DefaultPluginHandler) Lookup(name string) (string, bool) { + for _, prefix := range h.ValidPrefixes { + pluginPath := fmt.Sprintf("%s-%s", prefix, name) + + // Try to find plugin in pluginsDir + pluginDir, err := ExpandPath(h.PluginsDir) + if err != nil { + return "", false + } + + pluginDirPluginPath := filepath.Join(pluginDir, pluginPath) + _, err = os.Stat(pluginDirPluginPath) + if !os.IsNotExist(err) { + return pluginDirPluginPath, true + } + + // No plugins found in pluginsDir, try in PATH of that's an option + if h.LookupPluginsInPath { + pluginPath, err = exec.LookPath(pluginPath) + if err != nil { + continue + } + + if pluginPath != "" { + return pluginPath, true + } + } + } + + return "", false +} + +// Execute implements PluginHandler +func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error { + return syscall.Exec(executablePath, cmdArgs, environment) +} + +// HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find +// a plugin executable that satisfies the given arguments. +func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string) error { + remainingArgs := []string{} + + for idx := range cmdArgs { + if strings.HasPrefix(cmdArgs[idx], "-") { + continue + } + remainingArgs = append(remainingArgs, strings.Replace(cmdArgs[idx], "-", "_", -1)) + } + + foundBinaryPath := "" + + // attempt to find binary, starting at longest possible name with given cmdArgs + for len(remainingArgs) > 0 { + path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-")) + if !found { + remainingArgs = remainingArgs[:len(remainingArgs)-1] + continue + } + + foundBinaryPath = path + break + } + + if len(foundBinaryPath) == 0 { + return errors.New("Could not find plugin to execute") + } + + // invoke cmd binary relaying the current environment and args given + // remainingArgs will always have at least one element. + // execve will make remainingArgs[0] the "binary name". + err := pluginHandler.Execute(foundBinaryPath, append([]string{foundBinaryPath}, cmdArgs[len(remainingArgs):]...), os.Environ()) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/kn/commands/plugin/plugin_handler_test.go b/pkg/kn/commands/plugin/plugin_handler_test.go new file mode 100644 index 0000000000..3198475337 --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_handler_test.go @@ -0,0 +1,178 @@ +// Copyright © 2018 The Knative 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 plugin + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/assert" +) + +func TestPluginHandler(t *testing.T) { + var ( + pluginHandler, tPluginHandler PluginHandler + pluginPath, pluginName, tmpPathDir, pluginsDir string + lookupPluginsInPath bool + err error + ) + + setup := func(t *testing.T) { + tmpPathDir, err = ioutil.TempDir("", "plugin_list") + assert.Assert(t, err == nil) + pluginsDir = tmpPathDir + } + + cleanup := func(t *testing.T) { + err = os.RemoveAll(tmpPathDir) + assert.Assert(t, err == nil) + } + + beforeEach := func(t *testing.T) { + pluginName = "fake" + pluginPath = CreateTestPluginInPath(t, "kn-"+pluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir) + assert.Assert(t, pluginPath != "") + + pluginHandler = &DefaultPluginHandler{ + ValidPrefixes: []string{"kn"}, + PluginsDir: pluginsDir, + LookupPluginsInPath: lookupPluginsInPath, + } + assert.Assert(t, pluginHandler != nil) + + tPluginHandler = NewTestPluginHandler(pluginHandler) + assert.Assert(t, tPluginHandler != nil) + } + + t.Run("#NewDefaultPluginHandler", func(t *testing.T) { + setup(t) + defer cleanup(t) + + pHandler := NewDefaultPluginHandler([]string{"kn"}, pluginPath, false) + assert.Assert(t, pHandler != nil) + }) + + t.Run("#Lookup", func(t *testing.T) { + t.Run("when plugin in pluginsDir", func(t *testing.T) { + t.Run("returns the first filepath matching prefix", func(t *testing.T) { + setup(t) + defer cleanup(t) + beforeEach(t) + + path, exists := pluginHandler.Lookup(pluginName) + assert.Assert(t, path != "", fmt.Sprintf("no path when Lookup(%s)", pluginName)) + assert.Assert(t, exists == true, fmt.Sprintf("could not Lookup(%s)", pluginName)) + }) + + t.Run("returns empty filepath when no matching prefix found", func(t *testing.T) { + setup(t) + defer cleanup(t) + + path, exists := pluginHandler.Lookup("bogus-plugin-name") + assert.Assert(t, path == "", fmt.Sprintf("unexpected plugin: kn-bogus-plugin-name")) + assert.Assert(t, exists == false, fmt.Sprintf("unexpected plugin: kn-bogus-plugin-name")) + }) + }) + + t.Run("when plugin is in $PATH", func(t *testing.T) { + t.Run("--lookup-plugins-in-path=true", func(t *testing.T) { + setup(t) + defer cleanup(t) + + pluginsDir = filepath.Join(tmpPathDir, "bogus") + err = os.Setenv("PATH", tmpPathDir) + assert.Assert(t, err == nil) + lookupPluginsInPath = true + + beforeEach(t) + + path, exists := pluginHandler.Lookup(pluginName) + assert.Assert(t, path != "", fmt.Sprintf("no path when Lookup(%s)", pluginName)) + assert.Assert(t, exists == true, fmt.Sprintf("could not Lookup(%s)", pluginName)) + }) + + t.Run("--lookup-plugins-in-path=false", func(t *testing.T) { + setup(t) + defer cleanup(t) + + pluginsDir = filepath.Join(tmpPathDir, "bogus") + err = os.Setenv("PATH", tmpPathDir) + assert.Assert(t, err == nil) + lookupPluginsInPath = false + + beforeEach(t) + + path, exists := pluginHandler.Lookup(pluginName) + assert.Assert(t, path == "") + assert.Assert(t, exists == false) + }) + }) + }) + + t.Run("#Execute", func(t *testing.T) { + t.Run("fails executing bogus plugin name", func(t *testing.T) { + setup(t) + defer cleanup(t) + beforeEach(t) + + bogusPath := filepath.Join(filepath.Dir(pluginPath), "kn-bogus-plugin-name") + err = pluginHandler.Execute(bogusPath, []string{bogusPath}, os.Environ()) + assert.Assert(t, err != nil, fmt.Sprintf("bogus plugin in path %s unexpectedly executed OK", bogusPath)) + }) + }) + + t.Run("HandlePluginCommand", func(t *testing.T) { + t.Run("sucess handling", func(t *testing.T) { + setup(t) + defer cleanup(t) + beforeEach(t) + + err = HandlePluginCommand(tPluginHandler, []string{pluginName}) + assert.Assert(t, err == nil, fmt.Sprintf("test plugin %s failed executing", fmt.Sprintf("kn-%s", pluginName))) + }) + + t.Run("fails handling", func(t *testing.T) { + setup(t) + defer cleanup(t) + + err = HandlePluginCommand(tPluginHandler, []string{"bogus"}) + assert.Assert(t, err != nil, fmt.Sprintf("test plugin %s expected to fail executing", "bogus")) + }) + }) +} + +// TestPluginHandler - needed to mock Execute() call + +type testPluginHandler struct { + pluginHandler PluginHandler +} + +func NewTestPluginHandler(pluginHandler PluginHandler) PluginHandler { + return &testPluginHandler{ + pluginHandler: pluginHandler, + } +} + +func (tHandler *testPluginHandler) Lookup(name string) (string, bool) { + return tHandler.pluginHandler.Lookup(name) +} + +func (tHandler *testPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error { + // Always success (avoids doing syscall.Exec which exits tests framework) + return nil +} diff --git a/pkg/kn/commands/plugin/plugin_list.go b/pkg/kn/commands/plugin/plugin_list.go new file mode 100644 index 0000000000..b8823c9d66 --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_list.go @@ -0,0 +1,224 @@ +// Copyright © 2019 The Knative 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 plugin + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/knative/client/pkg/kn/commands" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + + homedir "github.com/mitchellh/go-homedir" +) + +// ValidPluginFilenamePrefixes controls the prefix for all kn plugins +var ValidPluginFilenamePrefixes = []string{"kn"} + +// NewPluginListCommand creates a new `kn plugin list` command +func NewPluginListCommand(p *commands.KnParams) *cobra.Command { + pluginFlags := PluginFlags{ + IOStreams: genericclioptions.IOStreams{ + In: os.Stdin, + Out: os.Stdout, + ErrOut: os.Stderr, + }, + } + + pluginListCommand := &cobra.Command{ + Use: "list", + Short: "List all visible plugin executables", + Long: `List all visible plugin executables. + +Available plugin files are those that are: +- executable +- begin with "kn- +- anywhere on the path specified in Kn's config pluginDir variable, which: + * can be overridden with the --plugin-dir flag`, + RunE: func(cmd *cobra.Command, args []string) error { + err := pluginFlags.complete(cmd) + if err != nil { + return err + } + + err = pluginFlags.run() + if err != nil { + return err + } + + return nil + }, + } + + AddPluginFlags(pluginListCommand) + BindPluginsFlagToViper(pluginListCommand) + + pluginFlags.AddPluginFlags(pluginListCommand) + + return pluginListCommand +} + +// ExpandPath to a canonical path (need to see if Golang has a better option) +func ExpandPath(path string) (string, error) { + if strings.Contains(path, "~") { + var err error + path, err = expandHomeDir(path) + if err != nil { + return "", err + } + } + return path, nil +} + +// Private + +func (o *PluginFlags) complete(cmd *cobra.Command) error { + o.Verifier = &CommandOverrideVerifier{ + Root: cmd.Root(), + SeenPlugins: make(map[string]string, 0), + } + + pluginPath, err := ExpandPath(commands.Cfg.PluginsDir) + if err != nil { + return err + } + + if commands.Cfg.LookupPluginsInPath { + pluginPath = pluginPath + string(os.PathListSeparator) + os.Getenv("PATH") + } + + o.PluginPaths = filepath.SplitList(pluginPath) + + return nil +} + +func (o *PluginFlags) run() error { + pluginsFound := false + isFirstFile := true + pluginErrors := []error{} + pluginWarnings := 0 + + for _, dir := range uniquePathsList(o.PluginPaths) { + if dir == "" { + continue + } + + files, err := ioutil.ReadDir(dir) + if err != nil { + if _, ok := err.(*os.PathError); ok { + fmt.Fprintf(o.ErrOut, "Unable read directory '%s' from your plugins path: %v. Skipping...", dir, err) + continue + } + + pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to read directory '%s' from your plugin path: %v", dir, err)) + continue + } + + for _, f := range files { + if f.IsDir() { + continue + } + if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) { + continue + } + + if isFirstFile { + fmt.Fprintf(o.ErrOut, "The following compatible plugins are available, using options:\n") + fmt.Fprintf(o.ErrOut, " - plugins dir: '%s'\n", commands.Cfg.PluginsDir) + fmt.Fprintf(o.ErrOut, " - lookup plugins in path: '%t'\n\n", commands.Cfg.LookupPluginsInPath) + pluginsFound = true + isFirstFile = false + } + + pluginPath := f.Name() + if !o.NameOnly { + pluginPath = filepath.Join(dir, pluginPath) + } + + fmt.Fprintf(o.Out, "%s\n", pluginPath) + if errs := o.Verifier.Verify(filepath.Join(dir, f.Name())); len(errs) != 0 { + for _, err := range errs { + fmt.Fprintf(o.ErrOut, " - %s\n", err) + pluginWarnings++ + } + } + } + } + + if !pluginsFound { + pluginErrors = append(pluginErrors, fmt.Errorf("warning: unable to find any kn plugins in your plugin path: '%s'", o.PluginPaths)) + } + + if pluginWarnings > 0 { + if pluginWarnings == 1 { + pluginErrors = append(pluginErrors, fmt.Errorf("error: one plugin warning was found")) + } else { + pluginErrors = append(pluginErrors, fmt.Errorf("error: %v plugin warnings were found", pluginWarnings)) + } + } + if len(pluginErrors) > 0 { + fmt.Fprintln(o.ErrOut) + errs := bytes.NewBuffer(nil) + for _, e := range pluginErrors { + fmt.Fprintln(errs, e) + } + return fmt.Errorf("%s", errs.String()) + } + + return nil +} + +// Private + +// expandHomeDir replaces the ~ with the home directory value +func expandHomeDir(path string) (string, error) { + home, err := homedir.Dir() + if err != nil { + fmt.Fprintln(os.Stderr, err) + return "", err + } + + return strings.Replace(path, "~", home, -1), nil +} + +// uniquePathsList deduplicates a given slice of strings without +// sorting or otherwise altering its order in any way. +func uniquePathsList(paths []string) []string { + seen := map[string]bool{} + newPaths := []string{} + for _, p := range paths { + if seen[p] { + continue + } + seen[p] = true + newPaths = append(newPaths, p) + } + return newPaths +} + +func hasValidPrefix(filepath string, validPrefixes []string) bool { + for _, prefix := range validPrefixes { + if !strings.HasPrefix(filepath, prefix+"-") { + continue + } + return true + } + return false +} diff --git a/pkg/kn/commands/plugin/plugin_list_test.go b/pkg/kn/commands/plugin/plugin_list_test.go new file mode 100644 index 0000000000..5c16dad3d0 --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_list_test.go @@ -0,0 +1,240 @@ +// Copyright © 2018 The Knative 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 plugin + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/knative/client/pkg/kn/commands" + "github.com/spf13/cobra" + "gotest.tools/assert" +) + +func TestPluginList(t *testing.T) { + var ( + rootCmd, pluginCmd, pluginListCmd *cobra.Command + tmpPathDir, pluginsDir, pluginsDirFlag string + err error + ) + + setup := func(t *testing.T) { + knParams := &commands.KnParams{} + pluginCmd = NewPluginCommand(knParams) + assert.Assert(t, pluginCmd != nil) + + rootCmd, _, _ = commands.CreateTestKnCommand(pluginCmd, knParams) + assert.Assert(t, rootCmd != nil) + + pluginListCmd = FindSubCommand(t, pluginCmd, "list") + assert.Assert(t, pluginListCmd != nil) + + tmpPathDir, err = ioutil.TempDir("", "plugin_list") + assert.Assert(t, err == nil) + + pluginsDir = filepath.Join(tmpPathDir, "plugins") + pluginsDirFlag = fmt.Sprintf("--plugins-dir=%s", pluginsDir) + } + + cleanup := func(t *testing.T) { + err = os.RemoveAll(tmpPathDir) + assert.Assert(t, err == nil) + } + + t.Run("creates a new cobra.Command", func(t *testing.T) { + setup(t) + defer cleanup(t) + + assert.Assert(t, pluginListCmd != nil) + assert.Assert(t, pluginListCmd.Use == "list") + assert.Assert(t, pluginListCmd.Short == "List all visible plugin executables") + assert.Assert(t, strings.Contains(pluginListCmd.Long, "List all visible plugin executables")) + assert.Assert(t, pluginListCmd.Flags().Lookup("plugins-dir") != nil) + assert.Assert(t, pluginListCmd.RunE != nil) + }) + + t.Run("when pluginsDir does not include any plugins", func(t *testing.T) { + t.Run("when --lookup-plugins-in-path is true", func(t *testing.T) { + var pluginPath string + + beforeEach := func(t *testing.T) { + err = os.Setenv("PATH", tmpPathDir) + assert.Assert(t, err == nil) + } + + t.Run("no plugins installed", func(t *testing.T) { + setup(t) + defer cleanup(t) + beforeEach(t) + + t.Run("warns user that no plugins found", func(t *testing.T) { + rootCmd.SetArgs([]string{"plugin", "list", "--lookup-plugins-in-path=true", pluginsDirFlag}) + err = rootCmd.Execute() + assert.Assert(t, err != nil) + assert.Assert(t, strings.Contains(err.Error(), "warning: unable to find any kn plugins in your plugin path:")) + }) + }) + + t.Run("plugins installed", func(t *testing.T) { + t.Run("with valid plugin in $PATH", func(t *testing.T) { + beforeEach := func(t *testing.T) { + pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir) + assert.Assert(t, pluginPath != "") + + err = os.Setenv("PATH", tmpPathDir) + assert.Assert(t, err == nil) + } + + t.Run("list plugins in $PATH", func(t *testing.T) { + setup(t) + defer cleanup(t) + beforeEach(t) + + commands.CaptureStdout(t) + rootCmd.SetArgs([]string{"plugin", "list", "--lookup-plugins-in-path=true", pluginsDirFlag}) + err = rootCmd.Execute() + assert.Assert(t, err == nil) + }) + }) + + t.Run("with non-executable plugin", func(t *testing.T) { + beforeEach := func(t *testing.T) { + pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeReadable, tmpPathDir) + assert.Assert(t, pluginPath != "") + } + + t.Run("warns user plugin invalid", func(t *testing.T) { + setup(t) + defer cleanup(t) + beforeEach(t) + + rootCmd.SetArgs([]string{"plugin", "list", "--lookup-plugins-in-path=true", pluginsDirFlag}) + err = rootCmd.Execute() + assert.Assert(t, err != nil) + assert.Assert(t, strings.Contains(err.Error(), "warning: unable to find any kn plugins in your plugin path:")) + }) + }) + + t.Run("with plugins with same name", func(t *testing.T) { + var tmpPathDir2 string + + beforeEach := func(t *testing.T) { + pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir) + assert.Assert(t, pluginPath != "") + + tmpPathDir2, err = ioutil.TempDir("", "plugins_list") + assert.Assert(t, err == nil) + + err = os.Setenv("PATH", tmpPathDir+string(os.PathListSeparator)+tmpPathDir2) + assert.Assert(t, err == nil) + + pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir2) + assert.Assert(t, pluginPath != "") + } + + afterEach := func(t *testing.T) { + err = os.RemoveAll(tmpPathDir) + assert.Assert(t, err == nil) + + err = os.RemoveAll(tmpPathDir2) + assert.Assert(t, err == nil) + } + + t.Run("warns user about second (in $PATH) plugin shadowing first", func(t *testing.T) { + setup(t) + defer cleanup(t) + beforeEach(t) + defer afterEach(t) + + rootCmd.SetArgs([]string{"plugin", "list", "--lookup-plugins-in-path=true", pluginsDirFlag}) + err = rootCmd.Execute() + assert.Assert(t, err != nil) + assert.Assert(t, strings.Contains(err.Error(), "error: one plugin warning was found")) + }) + }) + + t.Run("with plugins with name of existing command", func(t *testing.T) { + var fakeCmd *cobra.Command + + beforeEach := func(t *testing.T) { + fakeCmd = &cobra.Command{ + Use: "fake", + } + rootCmd.AddCommand(fakeCmd) + + pluginPath = CreateTestPluginInPath(t, "kn-fake", KnTestPluginScript, FileModeExecutable, tmpPathDir) + assert.Assert(t, pluginPath != "") + + err = os.Setenv("PATH", tmpPathDir) + assert.Assert(t, err == nil) + } + + afterEach := func(t *testing.T) { + rootCmd.RemoveCommand(fakeCmd) + } + + t.Run("warns user about overwritting exising command", func(t *testing.T) { + setup(t) + defer cleanup(t) + beforeEach(t) + defer afterEach(t) + + rootCmd.SetArgs([]string{"plugin", "list", "--lookup-plugins-in-path=true", pluginsDirFlag}) + err = rootCmd.Execute() + assert.Assert(t, err != nil) + assert.Assert(t, strings.Contains(err.Error(), "error: one plugin warning was found")) + }) + }) + }) + }) + }) + + t.Run("when pluginsDir has plugins", func(t *testing.T) { + var pluginPath string + + beforeEach := func(t *testing.T) { + pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir) + assert.Assert(t, pluginPath != "") + + err = os.Setenv("PATH", "") + assert.Assert(t, err == nil) + + pluginsDirFlag = fmt.Sprintf("--plugins-dir=%s", tmpPathDir) + } + + t.Run("list plugins in --plugins-dir", func(t *testing.T) { + setup(t) + defer cleanup(t) + beforeEach(t) + + rootCmd.SetArgs([]string{"plugin", "list", pluginsDirFlag}) + err = rootCmd.Execute() + assert.Assert(t, err == nil) + }) + + t.Run("no plugins installed", func(t *testing.T) { + setup(t) + defer cleanup(t) + + rootCmd.SetArgs([]string{"plugin", "list", pluginsDirFlag}) + err = rootCmd.Execute() + assert.Assert(t, err != nil) + }) + }) +} diff --git a/pkg/kn/commands/plugin/plugin_test.go b/pkg/kn/commands/plugin/plugin_test.go new file mode 100644 index 0000000000..3c32bfaed2 --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_test.go @@ -0,0 +1,74 @@ +// Copyright © 2018 The Knative 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 plugin + +import ( + "strings" + "testing" + + "github.com/knative/client/pkg/kn/commands" + "github.com/spf13/cobra" + "gotest.tools/assert" +) + +const PluginCommandUsage = `Provides utilities for interacting and managing with kn plugins. + +Plugins provide extended functionality that is not part of the core kn command-line distribution. +Please refer to the documentation and examples for more information about how write your own plugins. + +Usage: + kn plugin [flags] + kn plugin [command] + +Available Commands: + list List all visible plugin executables + +Flags: + -h, --help help for plugin + --lookup-plugins-in-path look for kn plugins in $PATH + --plugins-dir string kn plugins directory (default "~/.kn/plugins") + +Global Flags: + --config string kn config file (default is $HOME/.kn/config.yaml) + --kubeconfig string kubectl config file (default is $HOME/.kube/config) + +Use "kn plugin [command] --help" for more information about a command.` + +func TestNewPluginCommand(t *testing.T) { + var ( + rootCmd, pluginCmd *cobra.Command + ) + + setup := func(t *testing.T) { + knParams := &commands.KnParams{} + pluginCmd = NewPluginCommand(knParams) + assert.Assert(t, pluginCmd != nil) + + rootCmd, _, _ = commands.CreateTestKnCommand(pluginCmd, knParams) + assert.Assert(t, rootCmd != nil) + } + + t.Run("creates a new cobra.Command", func(t *testing.T) { + setup(t) + + assert.Assert(t, pluginCmd != nil) + assert.Assert(t, pluginCmd.Use == "plugin") + assert.Assert(t, pluginCmd.Short == "Plugin command group") + assert.Assert(t, strings.Contains(pluginCmd.Long, "Provides utilities for interacting and managing with kn plugins.")) + assert.Assert(t, pluginCmd.Flags().Lookup("plugins-dir") != nil) + assert.Assert(t, pluginCmd.Flags().Lookup("lookup-plugins-in-path") != nil) + assert.Assert(t, pluginCmd.Args == nil) + }) +} diff --git a/pkg/kn/commands/plugin/plugin_test_helper.go b/pkg/kn/commands/plugin/plugin_test_helper.go new file mode 100644 index 0000000000..d3bad5463b --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_test_helper.go @@ -0,0 +1,69 @@ +// Copyright © 2018 The Knative 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 plugin + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + "gotest.tools/assert" +) + +const ( + KnTestPluginName = "kn-test" + KnTestPluginScript = `#!/bin/bash + +echo "I am a test Kn plugin" +exit 0 +` + FileModeReadable = 0644 + FileModeExecutable = 0777 +) + +// FindSubCommand return the sub-command by name +func FindSubCommand(t *testing.T, rootCmd *cobra.Command, name string) *cobra.Command { + for _, subCmd := range rootCmd.Commands() { + if subCmd.Name() == name { + return subCmd + } + } + + return nil +} + +// CreateTestPlugin with name, script, and fileMode and return the tmp random path +func CreateTestPlugin(t *testing.T, name, script string, fileMode os.FileMode) string { + path, err := ioutil.TempDir("", "plugin") + assert.Assert(t, err == nil) + + return CreateTestPluginInPath(t, name, script, fileMode, path) +} + +// CreateTestPluginInPath with name, path, script, and fileMode and return the tmp random path +func CreateTestPluginInPath(t *testing.T, name, script string, fileMode os.FileMode, path string) string { + err := ioutil.WriteFile(filepath.Join(path, name), []byte(script), fileMode) + assert.Assert(t, err == nil) + + return filepath.Join(path, name) +} + +// DeleteTestPlugin with path +func DeleteTestPlugin(t *testing.T, path string) { + err := os.RemoveAll(filepath.Dir(path)) + assert.Assert(t, err == nil) +} diff --git a/pkg/kn/commands/help_testing.go b/pkg/kn/commands/testing_helper.go similarity index 87% rename from pkg/kn/commands/help_testing.go rename to pkg/kn/commands/testing_helper.go index 0f3a84ded5..1b467eabc4 100644 --- a/pkg/kn/commands/help_testing.go +++ b/pkg/kn/commands/testing_helper.go @@ -24,6 +24,7 @@ import ( "github.com/knative/client/pkg/serving/v1alpha1" "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1/fake" "github.com/spf13/cobra" + "github.com/spf13/viper" "gotest.tools/assert" client_testing "k8s.io/client-go/testing" ) @@ -113,6 +114,15 @@ Eventing: Manage event subscriptions and channels. Connect up event sources.`, rootCmd.PersistentFlags().StringVar(&CfgFile, "config", "", "config file (default is $HOME/.kn.yaml)") rootCmd.PersistentFlags().StringVar(¶ms.KubeCfgPath, "kubeconfig", "", "kubectl config file (default is $HOME/.kube/config)") + rootCmd.Flags().StringVar(&Cfg.PluginsDir, "plugins-dir", "~/.kn/plugins", "kn plugins directory") + rootCmd.Flags().BoolVar(&Cfg.LookupPluginsInPath, "lookup-plugins-in-path", false, "look for kn plugins in $PATH") + + viper.BindPFlag("pluginsDir", rootCmd.Flags().Lookup("plugins-dir")) + viper.BindPFlag("lookupPluginsInPath", rootCmd.Flags().Lookup("lookup-plugins-in-path")) + + viper.SetDefault("pluginsDir", "~/.kn/plugins") + viper.SetDefault("lookupPluginsInPath", false) + rootCmd.AddCommand(subCommand) // For glog parse error. diff --git a/pkg/kn/commands/testing_helper_test.go b/pkg/kn/commands/testing_helper_test.go new file mode 100644 index 0000000000..fdbe59448b --- /dev/null +++ b/pkg/kn/commands/testing_helper_test.go @@ -0,0 +1,56 @@ +// Copyright © 2018 The Knative 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 commands + +import ( + "bytes" + "strings" + "testing" + + "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1/fake" + "github.com/spf13/cobra" + "gotest.tools/assert" +) + +func TestCreateTestKnCommand(t *testing.T) { + var ( + knCmd *cobra.Command + serving *fake.FakeServingV1alpha1 + buffer *bytes.Buffer + ) + + setup := func(t *testing.T) { + knParams := &KnParams{} + knCmd, serving, buffer = CreateTestKnCommand(&cobra.Command{Use: "fake"}, knParams) + assert.Assert(t, knCmd != nil) + assert.Assert(t, len(knCmd.Commands()) == 1) + assert.Assert(t, knCmd.Commands()[0].Use == "fake") + assert.Assert(t, serving != nil) + assert.Assert(t, buffer != nil) + } + + t.Run("creates a new kn cobra.Command", func(t *testing.T) { + setup(t) + + assert.Assert(t, knCmd != nil) + assert.Assert(t, knCmd.Use == "kn") + assert.Assert(t, knCmd.Short == "Knative client") + assert.Assert(t, strings.Contains(knCmd.Long, "Manage your Knative building blocks:")) + assert.Assert(t, knCmd.RunE == nil) + assert.Assert(t, knCmd.DisableAutoGenTag == true) + assert.Assert(t, knCmd.SilenceUsage == true) + assert.Assert(t, knCmd.SilenceErrors == true) + }) +} diff --git a/pkg/kn/commands/types.go b/pkg/kn/commands/types.go index bd06d1f84a..3cc9ef8925 100644 --- a/pkg/kn/commands/types.go +++ b/pkg/kn/commands/types.go @@ -22,7 +22,6 @@ import ( "path/filepath" serving_kn_v1alpha1 "github.com/knative/client/pkg/serving/v1alpha1" - serving_v1alpha1_client "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1" "k8s.io/client-go/tools/clientcmd" ) @@ -30,6 +29,15 @@ import ( // CfgFile is Kn's config file is the path for the Kubernetes config var CfgFile string +// Cfg is Kn's configuration values +var Cfg Config + +// Config contains the variables for the Kn config +type Config struct { + PluginsDir string + LookupPluginsInPath bool +} + // Parameters for creating commands. Useful for inserting mocks for testing. type KnParams struct { Output io.Writer diff --git a/pkg/kn/core/root.go b/pkg/kn/core/root.go index da5a7dc6a3..2f5485fe7f 100644 --- a/pkg/kn/core/root.go +++ b/pkg/kn/core/root.go @@ -18,24 +18,74 @@ import ( "errors" "flag" "fmt" + "io" "os" "path" + "strconv" + "strings" "github.com/knative/client/pkg/kn/commands" + "github.com/knative/client/pkg/kn/commands/plugin" "github.com/knative/client/pkg/kn/commands/revision" "github.com/knative/client/pkg/kn/commands/route" "github.com/knative/client/pkg/kn/commands/service" - "github.com/mitchellh/go-homedir" + homedir "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" "github.com/spf13/viper" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" ) -var cfgFile string -var kubeCfgFile string +// NewDefaultKnCommand creates the default `kn` command with a default plugin handler +func NewDefaultKnCommand() *cobra.Command { + rootCmd := NewKnCommand() -// NewKnCommand creates new rootCmd represents the base command when called without any subcommands + // Needed since otherwise --plugins-dir and --lookup-plugins-in-path + // will not be accounted for since the plugin is not a Cobra command + // and will not be parsed + pluginsDir, lookupPluginsInPath, err := extractKnPluginFlags(os.Args) + if err != nil { + panic("Invalid plugin flag value") + } + + pluginHandler := plugin.NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes, + pluginsDir, lookupPluginsInPath) + + return NewDefaultKnCommandWithArgs(rootCmd, pluginHandler, + os.Args, os.Stdin, + os.Stdout, os.Stderr) +} + +// NewDefaultKnCommandWithArgs creates the `kn` command with arguments +func NewDefaultKnCommandWithArgs(rootCmd *cobra.Command, + pluginHandler plugin.PluginHandler, + args []string, + in io.Reader, + out, + errOut io.Writer) *cobra.Command { + if pluginHandler == nil { + return rootCmd + } + + if len(args) > 1 { + cmdPathPieces := args[1:] + cmdPathPieces = removeKnPluginFlags(cmdPathPieces) // Plugin does not need these flags + + // only look for suitable extension executables if + // the specified command does not already exist + if _, _, err := rootCmd.Find(cmdPathPieces); err != nil { + err := plugin.HandlePluginCommand(pluginHandler, cmdPathPieces) + if err != nil { + fmt.Fprintf(errOut, "%v\n", err) + os.Exit(1) + } + } + } + + return rootCmd +} + +// NewKnCommand creates the rootCmd which is the base command when called without any subcommands func NewKnCommand(params ...commands.KnParams) *cobra.Command { var p *commands.KnParams if len(params) == 0 { @@ -67,11 +117,18 @@ func NewKnCommand(params ...commands.KnParams) *cobra.Command { if p.Output != nil { rootCmd.SetOutput(p.Output) } - rootCmd.PersistentFlags().StringVar(&commands.CfgFile, "config", "", "config file (default is $HOME/.kn/config.yaml)") + + // Persistent flags + rootCmd.PersistentFlags().StringVar(&commands.CfgFile, "config", "", "kn config file (default is $HOME/.kn/config.yaml)") rootCmd.PersistentFlags().StringVar(&p.KubeCfgPath, "kubeconfig", "", "kubectl config file (default is $HOME/.kube/config)") + plugin.AddPluginFlags(rootCmd) + plugin.BindPluginsFlagToViper(rootCmd) + + // root child commands rootCmd.AddCommand(service.NewServiceCommand(p)) rootCmd.AddCommand(revision.NewRevisionCommand(p)) + rootCmd.AddCommand(plugin.NewPluginCommand(p)) rootCmd.AddCommand(route.NewRouteCommand(p)) rootCmd.AddCommand(commands.NewCompletionCommand(p)) rootCmd.AddCommand(commands.NewVersionCommand(p)) @@ -81,9 +138,15 @@ func NewKnCommand(params ...commands.KnParams) *cobra.Command { // For glog parse error. flag.CommandLine.Parse([]string{}) + return rootCmd } +// InitializeConfig initializes the kubeconfig used by all commands +func InitializeConfig() { + cobra.OnInitialize(initConfig) +} + // EmptyAndUnknownSubCommands adds a RunE to all commands that are groups to // deal with errors when called with empty or unknown sub command func EmptyAndUnknownSubCommands(cmd *cobra.Command) { @@ -106,6 +169,8 @@ func EmptyAndUnknownSubCommands(cmd *cobra.Command) { } } +// Private + // initConfig reads in config file and ENV variables if set. func initConfig() { if commands.CfgFile != "" { @@ -119,7 +184,7 @@ func initConfig() { os.Exit(1) } - // Search config in home directory with name ".kn" (without extension). + // Search config in home directory with name ".kn" (without extension) viper.AddConfigPath(path.Join(home, ".kn")) viper.SetConfigName("config") } @@ -127,7 +192,51 @@ func initConfig() { viper.AutomaticEnv() // read in environment variables that match // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + err := viper.ReadInConfig() + if err == nil { + fmt.Fprintln(os.Stderr, "Using kn config file:", viper.ConfigFileUsed()) + } +} + +func extractKnPluginFlags(args []string) (string, bool, error) { + pluginsDir := "~/.kn/plugins" + lookupPluginsInPath := false + for _, arg := range args { + if strings.Contains(arg, "--plugins-dir") { + values := strings.Split(arg, "=") + if len(values) < 1 { + return "", false, errors.New("Invalid --plugins-dir flag value") + } + pluginsDir = values[1] + } + + if strings.Contains(arg, "--lookup-plugins-in-path") { + values := strings.Split(arg, "=") + if len(values) < 1 { + return "", false, errors.New("Invalid --lookup-plugins-in-path flag value") + } + + boolValue, err := strconv.ParseBool(values[1]) + if err != nil { + return "", false, err + } + + lookupPluginsInPath = boolValue + } } + return pluginsDir, lookupPluginsInPath, nil +} + +func removeKnPluginFlags(args []string) []string { + var remainingArgs []string + for _, arg := range args { + if strings.Contains(arg, "--plugins-dir") || + strings.Contains(arg, "--lookup-plugins-in-path") { + continue + } else { + remainingArgs = append(remainingArgs, arg) + } + } + + return remainingArgs } diff --git a/pkg/kn/core/root_test.go b/pkg/kn/core/root_test.go index 32bf6fd073..4ac3c2d6ab 100644 --- a/pkg/kn/core/root_test.go +++ b/pkg/kn/core/root_test.go @@ -15,52 +15,123 @@ package core import ( + "io/ioutil" + "os" "strings" "testing" "github.com/knative/client/pkg/kn/commands" + "github.com/knative/client/pkg/kn/commands/plugin" "github.com/spf13/cobra" "gotest.tools/assert" ) -func TestNewKnCommand(t *testing.T) { +func TestNewDefaultKnCommand(t *testing.T) { var rootCmd *cobra.Command - setup := func() { - rootCmd = NewKnCommand(commands.KnParams{}) + setup := func(t *testing.T) { + rootCmd = NewDefaultKnCommand() } - setup() - t.Run("returns a valid root command", func(t *testing.T) { - assert.Assert(t, rootCmd != nil) + setup(t) + + checkRootCmd(t, rootCmd) + }) +} + +func TestNewDefaultKnCommandWithArgs(t *testing.T) { + var ( + rootCmd *cobra.Command + pluginHandler plugin.PluginHandler + args []string + ) - assert.Equal(t, rootCmd.Name(), "kn") - assert.Equal(t, rootCmd.Short, "Knative client") - assert.Assert(t, strings.Contains(rootCmd.Long, "Manage your Knative building blocks:")) + setup := func(t *testing.T) { + rootCmd = NewDefaultKnCommandWithArgs(NewKnCommand(), pluginHandler, args, os.Stdin, os.Stdout, os.Stderr) + } + + t.Run("when pluginHandler is nil", func(t *testing.T) { + args = []string{} + setup(t) - assert.Assert(t, rootCmd.DisableAutoGenTag) - assert.Assert(t, rootCmd.SilenceUsage) - assert.Assert(t, rootCmd.SilenceErrors) + t.Run("returns a valid root command", func(t *testing.T) { + checkRootCmd(t, rootCmd) + }) + }) - assert.Assert(t, rootCmd.RunE == nil) + t.Run("when pluginHandler is not nil", func(t *testing.T) { + t.Run("when args empty", func(t *testing.T) { + args = []string{} + setup(t) + + t.Run("returns a valid root command", func(t *testing.T) { + checkRootCmd(t, rootCmd) + }) + }) + + t.Run("when args not empty", func(t *testing.T) { + var ( + pluginName, pluginPath, tmpPathDir string + err error + ) + + beforeEach := func(t *testing.T) { + tmpPathDir, err = ioutil.TempDir("", "plugin_list") + assert.Assert(t, err == nil) + + pluginName = "fake-plugin-name" + pluginPath = plugin.CreateTestPluginInPath(t, "kn-"+pluginName, plugin.KnTestPluginScript, plugin.FileModeExecutable, tmpPathDir) + } + + afterEach := func(t *testing.T) { + err = os.RemoveAll(tmpPathDir) + assert.Assert(t, err == nil) + } + + beforeEach(t) + args = []string{pluginPath, pluginName} + setup(t) + defer afterEach(t) + + t.Run("tries to handle args[1:] as plugin and return valid root command", func(t *testing.T) { + checkRootCmd(t, rootCmd) + }) + }) + }) +} + +func TestNewKnCommand(t *testing.T) { + var rootCmd *cobra.Command + + setup := func(t *testing.T) { + rootCmd = NewKnCommand(commands.KnParams{}) + } + + t.Run("returns a valid root command", func(t *testing.T) { + setup(t) + checkRootCmd(t, rootCmd) }) t.Run("sets the output params", func(t *testing.T) { + setup(t) assert.Assert(t, rootCmd.OutOrStdout() != nil) }) t.Run("sets the config and kubeconfig global flags", func(t *testing.T) { + setup(t) assert.Assert(t, rootCmd.PersistentFlags().Lookup("config") != nil) assert.Assert(t, rootCmd.PersistentFlags().Lookup("kubeconfig") != nil) }) t.Run("adds the top level commands: version and completion", func(t *testing.T) { + setup(t) checkCommand(t, "version", rootCmd) checkCommand(t, "completion", rootCmd) }) t.Run("adds the top level group commands", func(t *testing.T) { + setup(t) checkCommandGroup(t, "service", rootCmd) checkCommandGroup(t, "revision", rootCmd) }) @@ -69,7 +140,7 @@ func TestNewKnCommand(t *testing.T) { func TestEmptyAndUnknownSubCommands(t *testing.T) { var rootCmd, fakeCmd, fakeSubCmd *cobra.Command - setup := func() { + setup := func(t *testing.T) { rootCmd = NewKnCommand(commands.KnParams{}) fakeCmd = &cobra.Command{ Use: "fake-cmd-name", @@ -84,9 +155,8 @@ func TestEmptyAndUnknownSubCommands(t *testing.T) { assert.Assert(t, fakeSubCmd.RunE == nil) } - setup() - t.Run("deals with empty and unknown sub-commands for all group commands", func(t *testing.T) { + setup(t) EmptyAndUnknownSubCommands(rootCmd) checkCommand(t, "fake-sub-cmd-name", fakeCmd) checkCommandGroup(t, "fake-cmd-name", rootCmd) @@ -95,6 +165,23 @@ func TestEmptyAndUnknownSubCommands(t *testing.T) { // Private +func checkRootCmd(t *testing.T, rootCmd *cobra.Command) { + assert.Assert(t, rootCmd != nil) + + assert.Equal(t, rootCmd.Name(), "kn") + assert.Equal(t, rootCmd.Short, "Knative client") + assert.Assert(t, strings.Contains(rootCmd.Long, "Manage your Knative building blocks:")) + + assert.Assert(t, rootCmd.DisableAutoGenTag) + assert.Assert(t, rootCmd.SilenceUsage) + assert.Assert(t, rootCmd.SilenceErrors) + + assert.Assert(t, rootCmd.Flags().Lookup("plugins-dir") != nil) + assert.Assert(t, rootCmd.Flags().Lookup("lookup-plugins-in-path") != nil) + + assert.Assert(t, rootCmd.RunE == nil) +} + func checkCommand(t *testing.T, name string, rootCmd *cobra.Command) { cmd, _, err := rootCmd.Find([]string{"version"}) assert.Assert(t, err == nil) diff --git a/test/e2e/plugin_workflow_test.go b/test/e2e/plugin_workflow_test.go new file mode 100644 index 0000000000..cae48bd115 --- /dev/null +++ b/test/e2e/plugin_workflow_test.go @@ -0,0 +1,129 @@ +// Copyright 2019 The Knative 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 im +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build e2e + +package e2e + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/assert" +) + +const ( + TestPluginCode string = `#!/bin/bash + +echo "Hello Knative, I'm a Kn plugin" +echo " My plugin file is $0" +echo " I recieved arguments: $1 $2 $3 $4"` + + KnConfigDefault string = `pluginDir=~/.kn/plugins +lookuppluginsinpath=true` +) + +func TestPluginWorkflow(t *testing.T) { + var ( + knConfigDir, knPluginsDir string + knConfigPath, knPluginPath string + lookupPluginsInPath bool + err error + ) + + test := NewE2eTest(t) + setup := func(t *testing.T) { + test.Setup(t) + defer test.Teardown(t) + + knConfigDir, err := ioutil.TempDir("", "kn-config") + assert.Assert(t, err == nil) + + knPluginsDir = filepath.Join(knConfigDir, "plugins") + err = os.MkdirAll(knPluginsDir, 0666) + assert.Assert(t, err == nil) + + knConfigPath, err = test.createConfig(t, "config.yaml", "", knConfigDir) + assert.Assert(t, err == nil) + assert.Assert(t, knConfigPath != "") + } + + teardown := func(t *testing.T) { + err = os.RemoveAll(knConfigDir) + assert.Assert(t, err == nil) + } + + t.Run("when kn config is empty", func(t *testing.T) { + t.Run("when --plugin-dir", func(t *testing.T) { + beforeEach := func(t *testing.T) { + knPluginPath, err = test.createPlugin(t, "kn-hello", TestPluginCode, knPluginsDir) + assert.Assert(t, err == nil) + assert.Assert(t, knPluginPath != "") + } + + t.Run("when --plugins-dir has a plugin", func(t *testing.T) { + t.Run("when --lookup-plugins-in-path is false", func(t *testing.T) { + lookupPluginsInPath = false + + setup(t) + defer teardown(t) + beforeEach(t) + + assert.Assert(t, lookupPluginsInPath == false) + + t.Run("list plugin in --plugins-dir", func(t *testing.T) {}) + t.Run("execute plugin in --plugins-dir", func(t *testing.T) {}) + t.Run("not list plugin in $PATH", func(t *testing.T) {}) + }) + + t.Run("with --lookup-plugins-in-path is true", func(t *testing.T) { + lookupPluginsInPath = true + + setup(t) + defer teardown(t) + beforeEach(t) + + assert.Assert(t, lookupPluginsInPath == true) + + t.Run("list plugin in --plugins-dir", func(t *testing.T) {}) + t.Run("list plugin in $PATH", func(t *testing.T) {}) + t.Run("execute plugin in --plugins-dir", func(t *testing.T) {}) + t.Run("execute plugin in $PATH", func(t *testing.T) {}) + }) + }) + }) + }) +} + +// Private + +func (test *e2eTest) createConfig(t *testing.T, configName, configCode, configPath string) (string, error) { + //TODO + return "", nil +} + +func (test *e2eTest) createPlugin(t *testing.T, pluginName, pluginCode, pluginPath string) (string, error) { + //TODO + return "", nil +} + +func (test *e2eTest) listPlugin(t *testing.T, expectedPlugins []string) { + //TODO +} + +func (test *e2eTest) runPlugin(t *testing.T, pluginName string, args []string, expectedOutput string) { + //TODO +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 80d5e0a381..46ab91caf7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -139,9 +139,9 @@ golang.org/x/oauth2/jwt golang.org/x/sys/unix golang.org/x/sys/windows # golang.org/x/text v0.3.0 -golang.org/x/text/encoding/unicode golang.org/x/text/transform golang.org/x/text/unicode/norm +golang.org/x/text/encoding/unicode golang.org/x/text/encoding golang.org/x/text/encoding/internal golang.org/x/text/encoding/internal/identifier