diff --git a/docs/cmd/kn_plugin.md b/docs/cmd/kn_plugin.md index 335a038b12..5d17d2657a 100644 --- a/docs/cmd/kn_plugin.md +++ b/docs/cmd/kn_plugin.md @@ -32,5 +32,5 @@ kn plugin [flags] ### SEE ALSO * [kn](kn.md) - Knative client -* [kn plugin list](kn_plugin_list.md) - List all visible plugin executables +* [kn plugin list](kn_plugin_list.md) - List plugins diff --git a/docs/cmd/kn_plugin_list.md b/docs/cmd/kn_plugin_list.md index cff91c1a69..4fe71426a6 100644 --- a/docs/cmd/kn_plugin_list.md +++ b/docs/cmd/kn_plugin_list.md @@ -1,16 +1,16 @@ ## kn plugin list -List all visible plugin executables +List plugins ### Synopsis -List all visible plugin executables. +List all installed plugins. -Available plugin files are those that are: +Available plugins 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 +- begin with "kn-" +- Kn's plugin directory ~/.kn/plugins +- Anywhere in the execution $PATH (if lookupInPath config variable is enabled) ``` kn plugin list [flags] @@ -23,6 +23,7 @@ kn plugin list [flags] --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") + --verbose verbose output ``` ### Options inherited from parent commands diff --git a/go.mod b/go.mod index f3e4e36950..8a1904faf9 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/modern-go/reflect2 v1.0.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect - github.com/pkg/errors v0.8.1 // indirect + github.com/pkg/errors v0.8.1 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 github.com/spf13/viper v1.3.1 diff --git a/pkg/kn/commands/plugin/path_verifier.go b/pkg/kn/commands/plugin/path_verifier.go deleted file mode 100644 index 5ef806bf97..0000000000 --- a/pkg/kn/commands/plugin/path_verifier.go +++ /dev/null @@ -1,102 +0,0 @@ -// 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 deleted file mode 100644 index e407c68824..0000000000 --- a/pkg/kn/commands/plugin/path_verifier_test.go +++ /dev/null @@ -1,112 +0,0 @@ -// 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_handler.go b/pkg/kn/commands/plugin/plugin_handler.go index 28bedee678..cf3536839d 100644 --- a/pkg/kn/commands/plugin/plugin_handler.go +++ b/pkg/kn/commands/plugin/plugin_handler.go @@ -22,6 +22,8 @@ import ( "path/filepath" "strings" "syscall" + + "github.com/mitchellh/go-homedir" ) // PluginHandler is capable of parsing command line arguments @@ -62,7 +64,7 @@ func (h *DefaultPluginHandler) Lookup(name string) (string, bool) { pluginPath := fmt.Sprintf("%s-%s", prefix, name) // Try to find plugin in pluginsDir - pluginDir, err := ExpandPath(h.PluginsDir) + pluginDir, err := homedir.Expand(h.PluginsDir) if err != nil { return "", false } diff --git a/pkg/kn/commands/plugin/plugin_list.go b/pkg/kn/commands/plugin/plugin_list.go index b8823c9d66..eefcac2ab4 100644 --- a/pkg/kn/commands/plugin/plugin_list.go +++ b/pkg/kn/commands/plugin/plugin_list.go @@ -15,18 +15,17 @@ 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" + "github.com/knative/client/pkg/kn/commands" + + "github.com/mitchellh/go-homedir" ) // ValidPluginFilenamePrefixes controls the prefix for all kn plugins @@ -34,103 +33,90 @@ 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, - }, - } + plFlags := pluginListFlags{} pluginListCommand := &cobra.Command{ Use: "list", - Short: "List all visible plugin executables", - Long: `List all visible plugin executables. + Short: "List plugins", + Long: `List all installed plugins. -Available plugin files are those that are: +Available plugins 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`, +- begin with "kn-" +- Kn's plugin directory ~/.kn/plugins +- Anywhere in the execution $PATH (if lookupInPath config variable is enabled)`, 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 + return listPlugins(cmd, plFlags) }, } AddPluginFlags(pluginListCommand) BindPluginsFlagToViper(pluginListCommand) - - pluginFlags.AddPluginFlags(pluginListCommand) + plFlags.AddPluginListFlags(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), - } +// List plugins by looking up in plugin directory and path +func listPlugins(cmd *cobra.Command, flags pluginListFlags) error { - pluginPath, err := ExpandPath(commands.Cfg.PluginsDir) + pluginPath, err := homedir.Expand(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) + pluginsFound, eaw := lookupPlugins(pluginPath) + + out := cmd.OutOrStdout() + + if flags.verbose { + fmt.Fprintf(out, "The following plugins are available, using options:\n") + fmt.Fprintf(out, " - plugins dir: '%s'%s\n", commands.Cfg.PluginsDir, extraLabelIfPathNotExists(pluginPath)) + fmt.Fprintf(out, " - lookup plugins in path: '%t'\n", commands.Cfg.LookupPluginsInPath) + } + + if len(pluginsFound) == 0 { + if flags.verbose { + fmt.Fprintf(out, "No plugins found in path %s\n", pluginPath) + } + return nil + } - return nil + verifier := newPluginVerifier(cmd.Root()) + for _, plugin := range pluginsFound { + name := plugin + if flags.nameOnly { + name = filepath.Base(plugin) + } + fmt.Fprintf(out, "%s\n", name) + eaw = verifier.verify(eaw, plugin) + } + eaw.printWarningsAndErrors(out) + return eaw.combinedError() } -func (o *PluginFlags) run() error { - pluginsFound := false - isFirstFile := true - pluginErrors := []error{} - pluginWarnings := 0 +func lookupPlugins(pluginPath string) ([]string, errorsAndWarnings) { + pluginsFound := make([]string, 0) + eaw := errorsAndWarnings{} + + for _, dir := range uniquePathsList(filepath.SplitList(pluginPath)) { + + files, err := ioutil.ReadDir(dir) - for _, dir := range uniquePathsList(o.PluginPaths) { - if dir == "" { + // Ignore non-existing directories + if os.IsNotExist(err) { 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)) + eaw.addError("unable to read directory '%s' from your plugin path: %v", dir, err) continue } + // Check for plugins within given directory for _, f := range files { if f.IsDir() { continue @@ -138,71 +124,27 @@ func (o *PluginFlags) run() error { 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) + pluginsFound = append(pluginsFound, filepath.Join(dir, f.Name())) } - return fmt.Errorf("%s", errs.String()) } - - return nil + return pluginsFound, eaw } -// 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 +func hasValidPrefix(filepath string, validPrefixes []string) bool { + for _, prefix := range validPrefixes { + if !strings.HasPrefix(filepath, prefix+"-") { + continue + } + return true } - - return strings.Replace(path, "~", home, -1), nil + return false } // 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{} + var newPaths []string for _, p := range paths { if seen[p] { continue @@ -213,12 +155,14 @@ func uniquePathsList(paths []string) []string { return newPaths } -func hasValidPrefix(filepath string, validPrefixes []string) bool { - for _, prefix := range validPrefixes { - if !strings.HasPrefix(filepath, prefix+"-") { - continue - } - return true +// create an info label which can be appended to an verbose output +func extraLabelIfPathNotExists(path string) string { + _, err := os.Stat(path) + if err == nil { + return "" } - return false + if os.IsNotExist(err) { + return " (does not exist)" + } + return "" } diff --git a/pkg/kn/commands/plugin/plugin_flags.go b/pkg/kn/commands/plugin/plugin_list_flags.go similarity index 69% rename from pkg/kn/commands/plugin/plugin_flags.go rename to pkg/kn/commands/plugin/plugin_list_flags.go index b15852084f..8e219b1e04 100644 --- a/pkg/kn/commands/plugin/plugin_flags.go +++ b/pkg/kn/commands/plugin/plugin_list_flags.go @@ -16,20 +16,16 @@ 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 +// pluginListFlags contains all plugin commands flags +type pluginListFlags struct { + nameOnly bool + verbose bool } // 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") +func (p *pluginListFlags) AddPluginListFlags(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") + command.Flags().BoolVar(&p.verbose, "verbose", false, "verbose output") } diff --git a/pkg/kn/commands/plugin/plugin_flags_test.go b/pkg/kn/commands/plugin/plugin_list_flags_test.go similarity index 73% rename from pkg/kn/commands/plugin/plugin_flags_test.go rename to pkg/kn/commands/plugin/plugin_list_flags_test.go index c4ecc9ee0c..9684b4b000 100644 --- a/pkg/kn/commands/plugin/plugin_flags_test.go +++ b/pkg/kn/commands/plugin/plugin_list_flags_test.go @@ -21,27 +21,27 @@ import ( "gotest.tools/assert" ) -func TestAddPluginFlags(t *testing.T) { +func TestAddPluginListFlags(t *testing.T) { var ( - pluginFlags *PluginFlags - cmd *cobra.Command + pFlags *pluginListFlags + cmd *cobra.Command ) - setup := func() { - pluginFlags = &PluginFlags{} + t.Run("adds plugin flag", func(t *testing.T) { + pFlags = &pluginListFlags{} cmd = &cobra.Command{} - } - - t.Run("adds plugin flag", func(t *testing.T) { - setup() - pluginFlags.AddPluginFlags(cmd) + pFlags.AddPluginListFlags(cmd) - assert.Assert(t, pluginFlags != nil) + assert.Assert(t, pFlags != nil) assert.Assert(t, cmd.Flags() != nil) nameOnly, err := cmd.Flags().GetBool("name-only") - assert.Assert(t, err == nil) + assert.NilError(t, err) assert.Assert(t, nameOnly == false) + + verbose, err := cmd.Flags().GetBool("verbose") + assert.NilError(t, err) + assert.Assert(t, verbose == false) }) } diff --git a/pkg/kn/commands/plugin/plugin_list_test.go b/pkg/kn/commands/plugin/plugin_list_test.go index 5c16dad3d0..2c8ca6cda8 100644 --- a/pkg/kn/commands/plugin/plugin_list_test.go +++ b/pkg/kn/commands/plugin/plugin_list_test.go @@ -15,190 +15,191 @@ package plugin import ( + "bytes" "fmt" "io/ioutil" "os" "path/filepath" + "runtime" "strings" "testing" "github.com/knative/client/pkg/kn/commands" + "github.com/knative/client/pkg/util" + "github.com/spf13/cobra" "gotest.tools/assert" ) -func TestPluginList(t *testing.T) { - var ( - rootCmd, pluginCmd, pluginListCmd *cobra.Command - tmpPathDir, pluginsDir, pluginsDirFlag string - err error - ) +type testContext struct { + pluginsDir string + pathDir string + rootCmd *cobra.Command + out *bytes.Buffer + origPath string +} - setup := func(t *testing.T) { - knParams := &commands.KnParams{} - pluginCmd = NewPluginCommand(knParams) - assert.Assert(t, pluginCmd != nil) +func (ctx *testContext) execute(args ...string) error { + ctx.rootCmd.SetArgs(append(args, fmt.Sprintf("--plugins-dir=%s", ctx.pluginsDir))) + return ctx.rootCmd.Execute() +} - rootCmd, _, _ = commands.CreateTestKnCommand(pluginCmd, knParams) - assert.Assert(t, rootCmd != nil) +func (ctx *testContext) output() string { + return ctx.out.String() +} - pluginListCmd = FindSubCommand(t, pluginCmd, "list") - assert.Assert(t, pluginListCmd != nil) +func (ctx *testContext) cleanup() { + os.RemoveAll(ctx.pluginsDir) + os.RemoveAll(ctx.pathDir) + os.Setenv("PATH", ctx.origPath) +} - tmpPathDir, err = ioutil.TempDir("", "plugin_list") - assert.Assert(t, err == nil) +func (ctx *testContext) createTestPlugin(pluginName string, fileMode os.FileMode, inPath bool) error { + path := ctx.pluginsDir + if inPath { + path = ctx.pathDir + } + return ctx.createTestPluginWithPath(pluginName, fileMode, path) +} - pluginsDir = filepath.Join(tmpPathDir, "plugins") - pluginsDirFlag = fmt.Sprintf("--plugins-dir=%s", pluginsDir) +func (ctx *testContext) createTestPluginWithPath(pluginName string, fileMode os.FileMode, path string) error { + if runtime.GOOS == "windows" { + pluginName += ".bat" } + fullPath := filepath.Join(path, pluginName) + return ioutil.WriteFile(fullPath, []byte(KnTestPluginScript), fileMode) +} + +func TestPluginList(t *testing.T) { - cleanup := func(t *testing.T) { - err = os.RemoveAll(tmpPathDir) - assert.Assert(t, err == nil) + setup := func(t *testing.T) *testContext { + knParams := &commands.KnParams{} + pluginCmd := NewPluginCommand(knParams) + + rootCmd, _, out := commands.CreateTestKnCommand(pluginCmd, knParams) + pluginsDir, err := ioutil.TempDir("", "plugin-list-plugindir") + assert.NilError(t, err) + pathDir, err := ioutil.TempDir("", "plugin-list-pathdir") + assert.NilError(t, err) + + origPath := os.Getenv("PATH") + assert.NilError(t, os.Setenv("PATH", pathDir)) + + return &testContext{ + rootCmd: rootCmd, + out: out, + pluginsDir: pluginsDir, + pathDir: pathDir, + origPath: origPath, + } } t.Run("creates a new cobra.Command", func(t *testing.T) { - setup(t) - defer cleanup(t) + pluginCmd := NewPluginCommand(&commands.KnParams{}) + pluginListCmd := FindSubCommand(t, pluginCmd, "list") + assert.Assert(t, pluginListCmd != nil) 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.Short == "List plugins") + assert.Assert(t, strings.Contains(pluginListCmd.Long, "List all installed plugins")) 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 + t.Run("no plugins installed", func(t *testing.T) { - beforeEach := func(t *testing.T) { - err = os.Setenv("PATH", tmpPathDir) - assert.Assert(t, err == nil) - } + t.Run("warns user that no plugins found in verbose mode", func(t *testing.T) { + ctx := setup(t) + defer ctx.cleanup() + err := ctx.execute("plugin", "list", "--lookup-plugins-in-path=true", "--verbose") + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(ctx.output(), "No plugins found")) + }) - 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("no output when no plugins found", func(t *testing.T) { + ctx := setup(t) + defer ctx.cleanup() + err := ctx.execute("plugin", "list", "--lookup-plugins-in-path=true") + assert.NilError(t, err) + assert.Equal(t, ctx.output(), "") }) }) 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) + ctx := setup(t) + defer ctx.cleanup() + + err := ctx.createTestPlugin(KnTestPluginName, FileModeExecutable, true) + assert.NilError(t, err) + + err = ctx.execute("plugin", "list", "--lookup-plugins-in-path=true") + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(ctx.output(), KnTestPluginName)) }) }) 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:")) + ctx := setup(t) + defer ctx.cleanup() + + err := ctx.createTestPlugin(KnTestPluginName, FileModeReadable, false) + assert.NilError(t, err) + + err = ctx.execute("plugin", "list", "--lookup-plugins-in-path=false") + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(ctx.output(), "WARNING", "not executable", "current user")) }) }) 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) + t.Run("warns user about second (in $PATH) plugin shadowing first", func(t *testing.T) { + ctx := setup(t) + defer ctx.cleanup() - err = os.Setenv("PATH", tmpPathDir+string(os.PathListSeparator)+tmpPathDir2) - assert.Assert(t, err == nil) + err := ctx.createTestPlugin(KnTestPluginName, FileModeExecutable, true) + assert.NilError(t, err) - pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir2) - assert.Assert(t, pluginPath != "") - } + tmpPathDir2, err := ioutil.TempDir("", "plugins_list") + assert.NilError(t, err) + defer os.RemoveAll(tmpPathDir2) - afterEach := func(t *testing.T) { - err = os.RemoveAll(tmpPathDir) - assert.Assert(t, err == nil) + err = os.Setenv("PATH", ctx.pathDir+string(os.PathListSeparator)+tmpPathDir2) + assert.NilError(t, err) - err = os.RemoveAll(tmpPathDir2) - assert.Assert(t, err == nil) - } + err = ctx.createTestPluginWithPath(KnTestPluginName, FileModeExecutable, tmpPathDir2) + assert.NilError(t, err) - 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")) + err = ctx.execute("plugin", "list", "--lookup-plugins-in-path=true") + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(ctx.output(), "WARNING", "shadowed", "ignored")) }) }) t.Run("with plugins with name of existing command", func(t *testing.T) { - var fakeCmd *cobra.Command + t.Run("warns user about overwriting existing command", func(t *testing.T) { + ctx := setup(t) + defer ctx.cleanup() - beforeEach := func(t *testing.T) { - fakeCmd = &cobra.Command{ + fakeCmd := &cobra.Command{ Use: "fake", } - rootCmd.AddCommand(fakeCmd) + ctx.rootCmd.AddCommand(fakeCmd) + defer ctx.rootCmd.RemoveCommand(fakeCmd) - pluginPath = CreateTestPluginInPath(t, "kn-fake", KnTestPluginScript, FileModeExecutable, tmpPathDir) - assert.Assert(t, pluginPath != "") + err := ctx.createTestPlugin("kn-fake", FileModeExecutable, true) + assert.NilError(t, err) - 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")) + err = ctx.execute("plugin", "list", "--lookup-plugins-in-path=true") + assert.ErrorContains(t, err, "overwrite", "built-in") + assert.Assert(t, util.ContainsAll(ctx.output(), "ERROR", "overwrite", "built-in")) }) }) }) @@ -206,35 +207,24 @@ func TestPluginList(t *testing.T) { }) 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) + ctx := setup(t) + defer ctx.cleanup() + + err := ctx.createTestPlugin(KnTestPluginName, FileModeExecutable, false) - rootCmd.SetArgs([]string{"plugin", "list", pluginsDirFlag}) - err = rootCmd.Execute() - assert.Assert(t, err == nil) + err = ctx.execute("plugin", "list") + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(ctx.output(), KnTestPluginName)) }) t.Run("no plugins installed", func(t *testing.T) { - setup(t) - defer cleanup(t) + ctx := setup(t) + defer ctx.cleanup() - rootCmd.SetArgs([]string{"plugin", "list", pluginsDirFlag}) - err = rootCmd.Execute() - assert.Assert(t, err != nil) + err := ctx.execute("plugin", "list") + assert.NilError(t, err) + assert.Equal(t, ctx.output(), "") }) }) } diff --git a/pkg/kn/commands/plugin/plugin_test_helper.go b/pkg/kn/commands/plugin/plugin_test_helper.go index d3bad5463b..7af57a74d6 100644 --- a/pkg/kn/commands/plugin/plugin_test_helper.go +++ b/pkg/kn/commands/plugin/plugin_test_helper.go @@ -18,6 +18,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" "testing" "github.com/spf13/cobra" @@ -56,8 +57,12 @@ func CreateTestPlugin(t *testing.T, name, script string, fileMode os.FileMode) s // 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) + fullPath := filepath.Join(path, name) + if runtime.GOOS == "windows" { + fullPath += ".bat" + } + err := ioutil.WriteFile(fullPath, []byte(script), fileMode) + assert.NilError(t, err) return filepath.Join(path, name) } diff --git a/pkg/kn/commands/plugin/plugin_verifier.go b/pkg/kn/commands/plugin/plugin_verifier.go new file mode 100644 index 0000000000..4d4207f086 --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_verifier.go @@ -0,0 +1,287 @@ +// 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" + "os" + "path/filepath" + "runtime" + "strings" + "syscall" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// pluginVerifier verifies that existing kn commands are not overriden +type pluginVerifier struct { + root *cobra.Command + seenPlugins map[string]string +} + +// collect errors and warnings on the way +type errorsAndWarnings struct { + errors []string + warnings []string +} + +// Create new verifier +func newPluginVerifier(root *cobra.Command) *pluginVerifier { + return &pluginVerifier{ + root: root, + seenPlugins: make(map[string]string), + } +} + +// permission bits for execute +const ( + UserExecute = 1 << 6 + GroupExecute = 1 << 3 + OtherExecute = 1 << 0 +) + +// 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. +// This method is not idempotent and must be called for each path only once. +func (v *pluginVerifier) verify(eaw errorsAndWarnings, path string) errorsAndWarnings { + if v.root == nil { + return eaw.addError("unable to verify path with nil root") + } + + // Verify that plugin actually exists + fileInfo, err := os.Stat(path) + if err != nil { + if err == os.ErrNotExist { + return eaw.addError("cannot find plugin in %s", path) + } + return eaw.addError("cannot stat %s: %v", path, err) + } + + eaw = v.addErrorIfWrongPrefix(eaw, path) + eaw = v.addWarningIfNotExecutable(eaw, path, fileInfo) + eaw = v.addWarningIfAlreadySeen(eaw, path) + eaw = v.addErrorIfOverwritingExistingCommand(eaw, path) + + // Remember each verified plugin for duplicate check + v.seenPlugins[filepath.Base(path)] = path + + return eaw +} + +func (v *pluginVerifier) addWarningIfAlreadySeen(eaw errorsAndWarnings, path string) errorsAndWarnings { + fileName := filepath.Base(path) + if existingPath, ok := v.seenPlugins[fileName]; ok { + return eaw.addWarning("%s is ignored because it is shadowed by a equally named plugin: %s.", path, existingPath) + } + return eaw +} + +func (v *pluginVerifier) addErrorIfOverwritingExistingCommand(eaw errorsAndWarnings, path string) errorsAndWarnings { + fileName := filepath.Base(path) + cmds := strings.Split(fileName, "-") + if len(cmds) < 2 { + return eaw.addError("%s is not a valid plugin filename as its missing a prefix", fileName) + } + cmds = cmds[1:] + + // Check both, commands with underscore and with dash because plugins can be called with both + overwrittenCommands := make(map[string]bool) + for _, c := range [][]string{cmds, convertUnderscoresToDashes(cmds)} { + cmd, _, err := v.root.Find(c) + if err == nil { + overwrittenCommands[cmd.CommandPath()] = true + } + } + for command := range overwrittenCommands { + eaw.addError("%s overwrites existing built-in command: %s", fileName, command) + } + return eaw +} + +func (v *pluginVerifier) addErrorIfWrongPrefix(eaw errorsAndWarnings, path string) errorsAndWarnings { + fileName := filepath.Base(path) + // Only pick the first prefix as it is very like that it will be reduced to + // a single prefix anyway (PR pending) + prefix := ValidPluginFilenamePrefixes[0] + if !strings.HasPrefix(fileName, prefix+"-") { + eaw.addWarning("%s plugin doesn't start with plugin prefix %s", fileName, prefix) + } + return eaw +} + +func (v *pluginVerifier) addWarningIfNotExecutable(eaw errorsAndWarnings, path string, fileInfo os.FileInfo) errorsAndWarnings { + if runtime.GOOS == "windows" { + return checkForWindowsExecutable(eaw, fileInfo, path) + } + + mode := fileInfo.Mode() + if !mode.IsRegular() && !isSymlink(mode) { + return eaw.addWarning("%s is not a file", path) + } + perms := uint32(mode.Perm()) + + var sys *syscall.Stat_t + var ok bool + if sys, ok = fileInfo.Sys().(*syscall.Stat_t); !ok { + // We can check the files' owner/group + return eaw.addWarning("cannot check owner/group of file %s", path) + } + + isOwner := checkIfUserIsFileOwner(sys.Uid) + isInGroup, err := checkIfUserInGroup(sys.Gid) + if err != nil { + return eaw.addError("cannot get group ids for checking executable status of file %s", path) + } + + // User is owner and owner can execute + if canOwnerExecute(perms, isOwner) { + return eaw + } + + // User is in group which can execute, but user is not file owner + if canGroupExecute(perms, isOwner, isInGroup) { + return eaw + } + + // All can execute, and the user is not file owner and not in the file's perm group + if canOtherExecute(perms, isOwner, isInGroup) { + return eaw + } + + return eaw.addWarning("%s is not executable by current user", path) +} + +func checkForWindowsExecutable(eaw errorsAndWarnings, fileInfo os.FileInfo, path string) errorsAndWarnings { + fileExt := strings.ToLower(filepath.Ext(fileInfo.Name())) + + switch fileExt { + case ".bat", ".cmd", ".com", ".exe", ".ps1": + return eaw + } + return eaw.addWarning("%s is not executable as it does not have the proper extension", path) +} + +func checkIfUserInGroup(gid uint32) (bool, error) { + groups, err := os.Getgroups() + if err != nil { + return false, err + } + for _, g := range groups { + if int(gid) == g { + return true, nil + } + } + return false, nil +} + +func checkIfUserIsFileOwner(uid uint32) bool { + if int(uid) == os.Getuid() { + return true + } + return false +} + +// Check if all can execute, and the user is not file owner and not in the file's perm group +func canOtherExecute(perms uint32, isOwner bool, isInGroup bool) bool { + if perms&OtherExecute != 0 { + if os.Getuid() == 0 { + return true + } + if !isOwner && !isInGroup { + return true + } + } + return false +} + +// Check if user is owner and owner can execute +func canOwnerExecute(perms uint32, isOwner bool) bool { + if perms&UserExecute != 0 { + if os.Getuid() == 0 { + return true + } + if isOwner { + return true + } + } + return false +} + +// Check if user is in group which can execute, but user is not file owner +func canGroupExecute(perms uint32, isOwner bool, isInGroup bool) bool { + if perms&GroupExecute != 0 { + if os.Getuid() == 0 { + return true + } + if !isOwner && isInGroup { + return true + } + } + return false +} + +func (eaw *errorsAndWarnings) addError(format string, args ...interface{}) errorsAndWarnings { + eaw.errors = append(eaw.errors, fmt.Sprintf(format, args...)) + return *eaw +} + +func (eaw *errorsAndWarnings) addWarning(format string, args ...interface{}) errorsAndWarnings { + eaw.warnings = append(eaw.warnings, fmt.Sprintf(format, args...)) + return *eaw +} + +func (eaw *errorsAndWarnings) printWarningsAndErrors(out io.Writer) { + printSection(out, "ERROR", eaw.errors) + printSection(out, "WARNING", eaw.warnings) +} + +func (eaw *errorsAndWarnings) combinedError() error { + if len(eaw.errors) == 0 { + return nil + } + return errors.New(strings.Join(eaw.errors, ",")) +} + +func printSection(out io.Writer, label string, values []string) { + if len(values) > 0 { + printLabelWithConditionalPluralS(out, label, len(values)) + for _, value := range values { + fmt.Fprintf(out, " - %s\n", value) + } + } +} + +func printLabelWithConditionalPluralS(out io.Writer, label string, nr int) { + if nr == 1 { + fmt.Fprintf(out, "%s:\n", label) + } else { + fmt.Fprintf(out, "%ss:\n", label) + } +} + +func convertUnderscoresToDashes(cmds []string) []string { + ret := make([]string, len(cmds)) + for i := range cmds { + ret[i] = strings.ReplaceAll(cmds[i], "_", "-") + } + return ret +} + +func isSymlink(mode os.FileMode) bool { + return mode&os.ModeSymlink != 0 +} diff --git a/pkg/kn/commands/plugin/plugin_verifier_test.go b/pkg/kn/commands/plugin/plugin_verifier_test.go new file mode 100644 index 0000000000..b6007b6fe9 --- /dev/null +++ b/pkg/kn/commands/plugin/plugin_verifier_test.go @@ -0,0 +1,228 @@ +// 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 ( + "errors" + "io/ioutil" + "os" + "os/exec" + "os/user" + "path/filepath" + "runtime" + "strconv" + "testing" + + "github.com/knative/client/pkg/kn/commands" + "github.com/knative/client/pkg/util" + + "github.com/spf13/cobra" + "gotest.tools/assert" +) + +func TestPluginVerifier(t *testing.T) { + + var ( + pluginPath string + rootCmd *cobra.Command + verifier *pluginVerifier + ) + + setup := func(t *testing.T) { + knParams := &commands.KnParams{} + rootCmd, _, _ = commands.CreateTestKnCommand(NewPluginCommand(knParams), knParams) + verifier = newPluginVerifier(rootCmd) + } + + 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 + eaw := errorsAndWarnings{} + eaw = verifier.verify(eaw, pluginPath) + assert.Assert(t, len(eaw.errors) == 1) + assert.Assert(t, len(eaw.warnings) == 0) + assert.Assert(t, util.ContainsAll(eaw.errors[0], "nil root")) + }) + }) + + t.Run("with root command", func(t *testing.T) { + t.Run("whether plugin in path is executable (unix only)", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skip test for windows as the permission check are for Unix only") + return + } + + setup(t) + defer cleanup(t) + + pluginDir, err := ioutil.TempDir("", "plugin") + assert.NilError(t, err) + defer os.RemoveAll(pluginDir) + pluginPath := filepath.Join(pluginDir, "kn-execution-test") + err = ioutil.WriteFile(pluginPath, []byte("#!/bin/sh\ntrue"), 0644) + assert.NilError(t, err, "can't create test plugin") + + for _, uid := range getExecTestUids() { + for _, gid := range getExecTestGids() { + for _, userPerm := range []int{0, UserExecute} { + for _, groupPerm := range []int{0, GroupExecute} { + for _, otherPerm := range []int{0, OtherExecute} { + perm := os.FileMode(userPerm | groupPerm | otherPerm + 0444) + assert.NilError(t, prepareFile(pluginPath, uid, gid, perm), "prepare plugin file, uid: %d, gid: %d, perm: %03o", uid, gid, perm) + + eaw := errorsAndWarnings{} + eaw = newPluginVerifier(rootCmd).verify(eaw, pluginPath) + + if isExecutable(pluginPath) { + assert.Assert(t, len(eaw.warnings) == 0, "executable: perm %03o | uid %d | gid %d | %v", perm, uid, gid, eaw.warnings) + assert.Assert(t, len(eaw.errors) == 0) + } else { + assert.Assert(t, len(eaw.warnings) == 1, "not executable: perm %03o | uid %d | gid %d | %v", perm, uid, gid, eaw.warnings) + assert.Assert(t, len(eaw.errors) == 0) + assert.Assert(t, util.ContainsAll(eaw.warnings[0], pluginPath)) + } + + } + } + } + } + } + }) + + 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) { + eaw := errorsAndWarnings{} + eaw = verifier.verify(eaw, shadowPluginPath) + assert.Assert(t, len(eaw.errors) == 0) + assert.Assert(t, len(eaw.warnings) == 1) + assert.Assert(t, util.ContainsAll(eaw.warnings[0], "shadowed", "ignored")) + }) + }) + }) + + 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) { + eaw := errorsAndWarnings{} + eaw = verifier.verify(eaw, overwritingPluginPath) + assert.Assert(t, len(eaw.errors) == 1) + assert.Assert(t, len(eaw.warnings) == 0) + assert.Assert(t, util.ContainsAll(eaw.errors[0], "overwrite", "kn-plugin")) + }) + }) + }) +} + +func isExecutable(plugin string) bool { + _, err := exec.Command(plugin).Output() + return err == nil +} + +func getExecTestUids() []int { + currentUser := os.Getuid() + // Only root can switch ownership of a file + if currentUser == 0 { + foreignUser, err := lookupForeignUser() + if err == nil { + return []int{currentUser, foreignUser} + } + } + return []int{currentUser} +} + +func getExecTestGids() []int { + currentUser := os.Getuid() + currentGroup := os.Getgid() + // Only root can switch group of a file + if currentUser == 0 { + foreignGroup, err := lookupForeignGroup() + if err == nil { + return []int{currentGroup, foreignGroup} + } + } + return []int{currentGroup} +} + +func lookupForeignUser() (int, error) { + for _, probe := range []string{"daemon", "nobody", "_unknown"} { + u, err := user.Lookup(probe) + if err != nil { + continue + } + uid, err := strconv.Atoi(u.Uid) + if err != nil { + continue + } + if uid != os.Getuid() { + return uid, nil + } + } + return 0, errors.New("could not find foreign user") +} + +func lookupForeignGroup() (int, error) { + gids, err := os.Getgroups() + if err != nil { + return 0, err + } +OUTER: + for _, probe := range []string{"daemon", "wheel", "nobody", "nogroup", "admin"} { + group, err := user.LookupGroup(probe) + if err != nil { + continue + } + gid, err := strconv.Atoi(group.Gid) + if err != nil { + continue + } + + for _, g := range gids { + if gid == g { + continue OUTER + } + } + return gid, nil + } + return 0, errors.New("could not find a foreign group") +} + +func prepareFile(file string, uid int, gid int, perm os.FileMode) error { + err := os.Chown(file, uid, gid) + if err != nil { + return err + } + return os.Chmod(file, perm) +}