From a4c94aec9a8ad73d05ced74604639df8ec8b89c3 Mon Sep 17 00:00:00 2001 From: Michael Maximilien Date: Mon, 8 Jul 2019 18:01:00 -0700 Subject: [PATCH] Implements Kn plugins re-using some code from kubectl plugins. 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 | 12 +- docs/cmd/kn.md | 2 + docs/cmd/kn_completion.md | 1 + docs/cmd/kn_plugin.md | 34 +++ docs/cmd/kn_plugin_list.md | 38 +++ docs/cmd/kn_revision.md | 1 + docs/cmd/kn_revision_delete.md | 1 + docs/cmd/kn_revision_describe.md | 1 + docs/cmd/kn_revision_list.md | 1 + docs/cmd/kn_route.md | 1 + docs/cmd/kn_route_list.md | 1 + docs/cmd/kn_service.md | 1 + docs/cmd/kn_service_create.md | 1 + docs/cmd/kn_service_delete.md | 1 + docs/cmd/kn_service_describe.md | 1 + docs/cmd/kn_service_list.md | 1 + docs/cmd/kn_service_update.md | 1 + docs/cmd/kn_version.md | 1 + pkg/kn/commands/plugin/path_verifier.go | 102 ++++++++ pkg/kn/commands/plugin/path_verifier_test.go | 110 +++++++++ pkg/kn/commands/plugin/plugin.go | 33 +++ pkg/kn/commands/plugin/plugin_flags.go | 35 +++ pkg/kn/commands/plugin/plugin_flags_test.go | 47 ++++ pkg/kn/commands/plugin/plugin_handler.go | 109 +++++++++ pkg/kn/commands/plugin/plugin_list.go | 190 +++++++++++++++ pkg/kn/commands/plugin/plugin_list_test.go | 233 +++++++++++++++++++ pkg/kn/commands/plugin/plugin_test.go | 109 +++++++++ pkg/kn/commands/plugin/plugin_test_helper.go | 68 ++++++ pkg/kn/commands/test_helper.go | 66 ++++++ pkg/kn/commands/types.go | 4 +- pkg/kn/core/root.go | 50 +++- vendor/modules.txt | 4 +- 32 files changed, 1252 insertions(+), 8 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_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 diff --git a/cmd/kn/main.go b/cmd/kn/main.go index 154f510560..48a211eca5 100644 --- a/cmd/kn/main.go +++ b/cmd/kn/main.go @@ -19,12 +19,22 @@ import ( "os" "github.com/knative/client/pkg/kn/core" + "github.com/spf13/viper" ) +func init() { + core.InitializeConfig() +} + 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() { + viper.WriteConfig() +} diff --git a/docs/cmd/kn.md b/docs/cmd/kn.md index f172ab627d..5c030706c9 100644 --- a/docs/cmd/kn.md +++ b/docs/cmd/kn.md @@ -16,11 +16,13 @@ Eventing: Manage event subscriptions and channels. Connect up event sources. --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) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO * [kn completion](kn_completion.md) - Output shell completion code (default Bash) +* [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_completion.md b/docs/cmd/kn_completion.md index 356364bb0b..152e777efb 100644 --- a/docs/cmd/kn_completion.md +++ b/docs/cmd/kn_completion.md @@ -22,6 +22,7 @@ kn completion [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_plugin.md b/docs/cmd/kn_plugin.md new file mode 100644 index 0000000000..e9705ede50 --- /dev/null +++ b/docs/cmd/kn_plugin.md @@ -0,0 +1,34 @@ +## kn plugin + +Plugin command group + +### Synopsis + +Provides utilities for interacting with kn plugins. + +Plugins provide extended functionality that is not part of the major 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 +``` + +### Options inherited from parent commands + +``` + --config string config file (default is $HOME/.kn/config.yaml) + --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") +``` + +### 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..62683dcabc --- /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 specfied in Kn's config pluginDir variable, which: + * defaults to $PATH if not specified + * can be overridden with the --plugin-dir flag + +``` +kn plugin list [flags] +``` + +### Options + +``` + -h, --help help for list + --name-only If true, display only the binary name of each plugin, rather than its full path +``` + +### Options inherited from parent commands + +``` + --config string config file (default is $HOME/.kn/config.yaml) + --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") +``` + +### 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..07341ecabb 100644 --- a/docs/cmd/kn_revision.md +++ b/docs/cmd/kn_revision.md @@ -21,6 +21,7 @@ kn revision [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_revision_delete.md b/docs/cmd/kn_revision_delete.md index e333fb2639..7c34a2bd65 100644 --- a/docs/cmd/kn_revision_delete.md +++ b/docs/cmd/kn_revision_delete.md @@ -30,6 +30,7 @@ kn revision delete NAME [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_revision_describe.md b/docs/cmd/kn_revision_describe.md index d99efee6ce..d8ab6443f9 100644 --- a/docs/cmd/kn_revision_describe.md +++ b/docs/cmd/kn_revision_describe.md @@ -25,6 +25,7 @@ kn revision describe NAME [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_revision_list.md b/docs/cmd/kn_revision_list.md index 59b0d8afb8..54639ed802 100644 --- a/docs/cmd/kn_revision_list.md +++ b/docs/cmd/kn_revision_list.md @@ -38,6 +38,7 @@ kn revision list [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_route.md b/docs/cmd/kn_route.md index 7587f3e860..18a5714406 100644 --- a/docs/cmd/kn_route.md +++ b/docs/cmd/kn_route.md @@ -21,6 +21,7 @@ kn route [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_route_list.md b/docs/cmd/kn_route_list.md index 07babbbfb1..e3d513fe53 100644 --- a/docs/cmd/kn_route_list.md +++ b/docs/cmd/kn_route_list.md @@ -40,6 +40,7 @@ kn route list NAME [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_service.md b/docs/cmd/kn_service.md index 5b4a36f95c..bad1046087 100644 --- a/docs/cmd/kn_service.md +++ b/docs/cmd/kn_service.md @@ -21,6 +21,7 @@ kn service [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_service_create.md b/docs/cmd/kn_service_create.md index ac7d479690..382612d032 100644 --- a/docs/cmd/kn_service_create.md +++ b/docs/cmd/kn_service_create.md @@ -62,6 +62,7 @@ kn service create NAME --image IMAGE [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_service_delete.md b/docs/cmd/kn_service_delete.md index f7c8602604..710d7dca20 100644 --- a/docs/cmd/kn_service_delete.md +++ b/docs/cmd/kn_service_delete.md @@ -33,6 +33,7 @@ kn service delete NAME [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_service_describe.md b/docs/cmd/kn_service_describe.md index 6c77c5f918..aa93c1bb5d 100644 --- a/docs/cmd/kn_service_describe.md +++ b/docs/cmd/kn_service_describe.md @@ -25,6 +25,7 @@ kn service describe NAME [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_service_list.md b/docs/cmd/kn_service_list.md index e78a351187..b995e7f05d 100644 --- a/docs/cmd/kn_service_list.md +++ b/docs/cmd/kn_service_list.md @@ -26,6 +26,7 @@ kn service list [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_service_update.md b/docs/cmd/kn_service_update.md index c9fda2e651..b65abeb498 100644 --- a/docs/cmd/kn_service_update.md +++ b/docs/cmd/kn_service_update.md @@ -47,6 +47,7 @@ kn service update NAME [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/docs/cmd/kn_version.md b/docs/cmd/kn_version.md index a3114865a2..0c3c599d83 100644 --- a/docs/cmd/kn_version.md +++ b/docs/cmd/kn_version.md @@ -21,6 +21,7 @@ kn version [flags] ``` --config string config file (default is $HOME/.kn/config.yaml) --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) (default "$PATH") ``` ### SEE ALSO diff --git a/pkg/kn/commands/plugin/path_verifier.go b/pkg/kn/commands/plugin/path_verifier.go new file mode 100644 index 0000000000..bc4c4d67c2 --- /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, "/") + 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..159d09940e --- /dev/null +++ b/pkg/kn/commands/plugin/path_verifier_test.go @@ -0,0 +1,110 @@ +// 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" +) + +var ( + pluginPath string + rootCmd *cobra.Command + verifier *CommandOverrideVerifier +) + +var setup = func() { + knParams := &commands.KnParams{} + rootCmd, _, _ = commands.CreateTestKnCommand(NewPluginCommand(knParams), knParams) + verifier = &CommandOverrideVerifier{ + Root: rootCmd, + SeenPlugins: make(map[string]string), + } +} + +var cleanup = func(t *testing.T) { + if pluginPath != "" { + DeleteTestPlugin(t, pluginPath) + } +} + +func TestCommandOverrideVerifier_Verify_with_nil_root_command(t *testing.T) { + t.Run("returns error verifying path", func(t *testing.T) { + setup() + 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")) + }) +} + +func TestCommandOverrideVerifier_Verify_with_root_command(t *testing.T) { + t.Run("when plugin in path not executable", func(t *testing.T) { + setup() + 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() + 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() + 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..5efb8d776d --- /dev/null +++ b/pkg/kn/commands/plugin/plugin.go @@ -0,0 +1,33 @@ +// 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" +) + +func NewPluginCommand(p *commands.KnParams) *cobra.Command { + pluginCmd := &cobra.Command{ + Use: "plugin", + Short: "Plugin command group", + Long: `Provides utilities for interacting with kn plugins. + +Plugins provide extended functionality that is not part of the major kn command-line distribution. +Please refer to the documentation and examples for more information about how write your own plugins.`, + } + pluginCmd.AddCommand(NewPluginListCommand(p)) + return pluginCmd +} 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..4ae9317167 --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_handler.go @@ -0,0 +1,109 @@ +// 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 ( + "fmt" + "os" + "os/exec" + "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(filename 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 +} + +// NewDefaultPluginHandler instantiates the DefaultPluginHandler with a list of +// given filename prefixes used to identify valid plugin filenames. +func NewDefaultPluginHandler(validPrefixes []string) *DefaultPluginHandler { + return &DefaultPluginHandler{ + ValidPrefixes: validPrefixes, + } +} + +// Lookup implements PluginHandler +func (h *DefaultPluginHandler) Lookup(filename string) (string, bool) { + for _, prefix := range h.ValidPrefixes { + path, err := exec.LookPath(fmt.Sprintf("%s-%s", prefix, filename)) + if err != nil || len(path) == 0 { + continue + } + return path, 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 on the PATH that satisfies the given arguments. +func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string) error { + remainingArgs := []string{} // all "non-flag" arguments + + for idx := range cmdArgs { + if strings.HasPrefix(cmdArgs[idx], "-") { + break + } + 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 nil + } + + // 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". + if err := pluginHandler.Execute(foundBinaryPath, append([]string{foundBinaryPath}, cmdArgs[len(remainingArgs):]...), os.Environ()); err != nil { + return err + } + + 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..e751ca242f --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_list.go @@ -0,0 +1,190 @@ +// 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" +) + +// 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 specfied in Kn's config pluginDir variable, which: + * defaults to $PATH if not specified + * 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 + }, + } + + pluginFlags.AddPluginFlags(pluginListCommand) + + return pluginListCommand +} + +// Private + +func (o *PluginFlags) complete(cmd *cobra.Command) error { + o.Verifier = &CommandOverrideVerifier{ + Root: cmd.Root(), + SeenPlugins: make(map[string]string, 0), + } + + pluginPath := commands.PluginDir + if pluginPath == "$PATH" { + pluginPath = 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 %q from your pluginDir: %v. Skipping...", dir, err) + continue + } + + pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to read directory %q in your pluginDir: %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:\n\n") + 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 pluginDir")) + } + + 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 + +// 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..abb987ef2f --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_list_test.go @@ -0,0 +1,233 @@ +// 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" + "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 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) + } + + 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.RunE != nil) + }) + + t.Run("when using $PATH as plugin location", 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"}) + err = rootCmd.Execute() + assert.Assert(t, err != nil) + assert.Assert(t, strings.Contains(err.Error(), "warning: unable to find any kn plugins in your pluginDir")) + }) + }) + + 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) + + rootCmd.SetArgs([]string{"plugin", "list"}) + err = rootCmd.Execute() + assert.Assert(t, err == nil) + + //TODO: test output to contain the plugin + }) + }) + + 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"}) + err = rootCmd.Execute() + assert.Assert(t, err != nil) + assert.Assert(t, strings.Contains(err.Error(), "warning: unable to find any kn plugins in your pluginDir")) + }) + }) + + 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("", "plugin_list") + assert.Assert(t, err == nil) + + err = os.Setenv("PATH", tmpPathDir+":"+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"}) + 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"}) + err = rootCmd.Execute() + assert.Assert(t, err != nil) + assert.Assert(t, strings.Contains(err.Error(), "error: one plugin warning was found")) + }) + }) + }) + }) + + t.Run("when using pluginDir config variable", func(t *testing.T) { + 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) + } + + t.Run("list plugins in --plugin-dir", func(t *testing.T) { + setup(t) + defer cleanup(t) + beforeEach(t) + + rootCmd.SetArgs([]string{"plugin", "list", "--plugin-dir", tmpPathDir}) + err = rootCmd.Execute() + assert.Assert(t, err == nil) + + //TODO: test output to contain the plugin + }) + + t.Run("no plugins installed", func(t *testing.T) { + setup(t) + defer cleanup(t) + + rootCmd.SetArgs([]string{"plugin", "list", "--plugin-dir", tmpPathDir}) + err = rootCmd.Execute() + assert.Assert(t, err != nil) + + //TODO: test output is no plugin found + }) + }) +} diff --git a/pkg/kn/commands/plugin/plugin_test.go b/pkg/kn/commands/plugin/plugin_test.go new file mode 100644 index 0000000000..4ef968041b --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_test.go @@ -0,0 +1,109 @@ +// 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 with kn plugins. + +Plugins provide extended functionality that is not part of the major 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 + +Global Flags: + --config string config file (default is $HOME/.kn/config.yaml) + --kubeconfig string kubectl config file (default is $HOME/.kube/config) + --plugin-dir string kn plugin directory (default is value in kn config or $PATH) + +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 with kn plugins.")) + assert.Assert(t, pluginCmd.Args == nil) + }) + + t.Run("when called with known subcommand", func(t *testing.T) { + var fakeExecuted bool + + beforeEach := func(t *testing.T) { + pluginCmd.AddCommand(&cobra.Command{ + Use: "fake", + Short: "fake subcommand", + RunE: func(cmd *cobra.Command, args []string) error { + fakeExecuted = true + return nil + }, + }) + } + + t.Run("executes the subcommand RunE func", func(t *testing.T) { + setup(t) + beforeEach(t) + + rootCmd.SetArgs([]string{"plugin", "fake"}) + err := rootCmd.Execute() + assert.Assert(t, err == nil) + assert.Assert(t, fakeExecuted == true) + }) + + t.Run("reads flag --plugin-dir", func(t *testing.T) { + setup(t) + beforeEach(t) + + rootCmd.SetArgs([]string{"plugin", "fake", "--plugin-dir", "$PATH"}) + err := pluginCmd.Execute() + assert.Assert(t, err == nil) + + pluginDir, err := rootCmd.PersistentFlags().GetString("plugin-dir") + assert.Assert(t, err == nil) + assert.Assert(t, pluginDir == "$PATH") + }) + }) +} 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..364fd957fd --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_test_helper.go @@ -0,0 +1,68 @@ +// 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" +` + FileModeReadable = 0644 + FileModeExecutable = 777 +) + +// 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/test_helper.go b/pkg/kn/commands/test_helper.go index f4979dc924..7833ec7100 100644 --- a/pkg/kn/commands/test_helper.go +++ b/pkg/kn/commands/test_helper.go @@ -17,15 +17,32 @@ package commands import ( "bytes" "flag" + "io" + "os" + "strings" + "testing" "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" ) const FakeNamespace = "current" +var ( + oldStdout *os.File + stdout *os.File + output string + + readFile, writeFile *os.File + + origArgs []string +) + +// CreateTestKnCommand helper for creating test commands func CreateTestKnCommand(cmd *cobra.Command, knParams *KnParams) (*cobra.Command, *fake.FakeServingV1alpha1, *bytes.Buffer) { buf := new(bytes.Buffer) fakeServing := &fake.FakeServingV1alpha1{&client_testing.Fake{}} @@ -38,6 +55,51 @@ func CreateTestKnCommand(cmd *cobra.Command, knParams *KnParams) (*cobra.Command return knCommand, fakeServing, buf } +// TestContains is a test helper function, checking if a substring is present in given +// output string +func TestContains(t *testing.T, output string, sub []string, element string) { + for _, each := range sub { + if !strings.Contains(output, each) { + t.Errorf("Missing %s: %s", element, each) + } + } +} + +// CaptureStdout collects the current content of os.Stdout +func CaptureStdout(t *testing.T) { + oldStdout = os.Stdout + var err error + readFile, writeFile, err = os.Pipe() + assert.Assert(t, err == nil) + stdout = writeFile + os.Stdout = writeFile +} + +// ReleaseStdout releases the os.Stdout and restores to original +func ReleaseStdout(t *testing.T) { + output = ReadStdout(t) + os.Stdout = oldStdout +} + +// ReadStdout returns the collected os.Stdout content +func ReadStdout(t *testing.T) string { + outC := make(chan string) + go func() { + var buf bytes.Buffer + io.Copy(&buf, readFile) + outC <- buf.String() + }() + writeFile.Close() + output = <-outC + + CaptureStdout(t) + + return output +} + +// Private + +// newKnCommand needed since calling the one in core would cause a import cycle func newKnCommand(subCommand *cobra.Command, params *KnParams) *cobra.Command { rootCmd := &cobra.Command{ Use: "kn", @@ -61,8 +123,12 @@ Eventing: Manage event subscriptions and channels. Connect up event sources.`, rootCmd.SetOutput(params.Output) } rootCmd.PersistentFlags().StringVar(&CfgFile, "config", "", "config file (default is $HOME/.kn.yaml)") + rootCmd.PersistentFlags().StringVar(&PluginDir, "plugin-dir", "$PATH", "kn plugin directory (default is value in kn config or $PATH)") rootCmd.PersistentFlags().StringVar(¶ms.KubeCfgPath, "kubeconfig", "", "kubectl config file (default is $HOME/.kube/config)") + viper.BindPFlag("pluginDir", rootCmd.PersistentFlags().Lookup("plugin-dir")) + viper.SetDefault("pluginDir", "$PATH") + rootCmd.AddCommand(subCommand) // For glog parse error. diff --git a/pkg/kn/commands/types.go b/pkg/kn/commands/types.go index 80ff7afe38..96fd525e39 100644 --- a/pkg/kn/commands/types.go +++ b/pkg/kn/commands/types.go @@ -18,7 +18,6 @@ import ( "io" 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" ) @@ -26,6 +25,9 @@ import ( // CfgFile is Kn's config file is the path for the Kubernetes config var CfgFile string +// PluginDir is Kn's config string for plugin directory +var PluginDir string + // 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 4eec5685df..04b6c2a1e8 100644 --- a/pkg/kn/core/root.go +++ b/pkg/kn/core/root.go @@ -18,24 +18,52 @@ import ( "errors" "flag" "fmt" + "io" "os" "path" "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 { + return NewDefaultKnCommandWithArgs(plugin.NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes), os.Args, os.Stdin, os.Stdout, os.Stderr) +} + +// NewDefaultKnCommandWithArgs creates the `kn` command with arguments +func NewDefaultKnCommandWithArgs(pluginHandler plugin.PluginHandler, args []string, in io.Reader, out, errout io.Writer) *cobra.Command { + cmd := NewKnCommand() + + if pluginHandler == nil { + return cmd + } -// NewKnCommand creates new rootCmd represents the base command when called without any subcommands + if len(args) > 1 { + cmdPathPieces := args[1:] + + // only look for suitable extension executables if + // the specified command does not already exist + if _, _, err := cmd.Find(cmdPathPieces); err != nil { + if err := plugin.HandlePluginCommand(pluginHandler, cmdPathPieces); err != nil { + fmt.Fprintf(errout, "%v\n", err) + os.Exit(1) + } + } + } + + return cmd +} + +// 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 { @@ -68,11 +96,20 @@ Eventing: Manage event subscriptions and channels. Connect up event sources.`, if p.Output != nil { rootCmd.SetOutput(p.Output) } + + // Persistent flags rootCmd.PersistentFlags().StringVar(&commands.CfgFile, "config", "", "config file (default is $HOME/.kn/config.yaml)") + rootCmd.PersistentFlags().StringVar(&commands.PluginDir, "plugin-dir", "$PATH", "kn plugin directory (default is value in kn config or $PATH)") rootCmd.PersistentFlags().StringVar(&p.KubeCfgPath, "kubeconfig", "", "kubectl config file (default is $HOME/.kube/config)") + // bind and set default with viper + viper.BindPFlag("pluginDir", rootCmd.PersistentFlags().Lookup("plugin-dir")) + viper.SetDefault("pluginDir", "$PATH") + + // 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)) @@ -85,6 +122,11 @@ Eventing: Manage event subscriptions and channels. Connect up event sources.`, 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) { diff --git a/vendor/modules.txt b/vendor/modules.txt index d58400cdd1..0942902e92 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 @@ -167,8 +167,8 @@ gopkg.in/inf.v0 # gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 # gotest.tools v2.2.0+incompatible -gotest.tools/assert/cmp gotest.tools/assert +gotest.tools/assert/cmp gotest.tools/internal/format gotest.tools/internal/source gotest.tools/internal/difflib