Implements Kn plugins re-using some code from kubectl plugins. (#249)

This version contains the following:

1. wraps the main root Kn command to support plugin
2. plugins are any executable in kn's config new pluginDir
   variable which defaults to $PATH
3. plugins must have name kn-*
4. 'kn plugin list' sub-command to list found kn plugins
5. skips any kn plugins found with name that match core
   commands, e.g., kn-service would be ignored
6. can execute any valid kn plugins found, e.g.,
   `kn valid` where the plugin file `kn-valid` is in path
   specified in 2.
7. unit tests (using gotest.tools)

And is missing:

1. integration tests
2. plugin install command
3. plugin repository command
4. plugin / Knative server version negotiation
5. anything else we agree on in plugin req doc

I plan to create issues for the things missing so we don't
end up with an even bigger PR. It's already big as is but is a
good MVP as per plugins requirement doc.
This commit is contained in:
dr.max 2019-07-26 13:29:49 -07:00 committed by Knative Prow Robot
parent df816e63fe
commit 59b2855d04
35 changed files with 1676 additions and 45 deletions

View File

@ -19,12 +19,26 @@ import (
"os"
"github.com/knative/client/pkg/kn/core"
"github.com/spf13/viper"
)
func init() {
core.InitializeConfig()
}
var err error
func main() {
err := core.NewKnCommand().Execute()
defer cleanup()
err = core.NewDefaultKnCommand().Execute()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func cleanup() {
if err == nil {
viper.WriteConfig()
}
}

View File

@ -12,13 +12,16 @@ Manage your Knative building blocks:
### Options
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
-h, --help help for kn
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
--lookup-plugins-in-path look for kn plugins in $PATH
--plugins-dir string kn plugins directory (default "~/.kn/plugins")
```
### SEE ALSO
* [kn plugin](kn_plugin.md) - Plugin command group
* [kn revision](kn_revision.md) - Revision command group
* [kn route](kn_route.md) - Route command group
* [kn service](kn_service.md) - Service command group

35
docs/cmd/kn_plugin.md Normal file
View File

@ -0,0 +1,35 @@
## kn plugin
Plugin command group
### Synopsis
Provides utilities for interacting and managing with kn plugins.
Plugins provide extended functionality that is not part of the core kn command-line distribution.
Please refer to the documentation and examples for more information about how write your own plugins.
```
kn plugin [flags]
```
### Options
```
-h, --help help for plugin
--lookup-plugins-in-path look for kn plugins in $PATH
--plugins-dir string kn plugins directory (default "~/.kn/plugins")
```
### Options inherited from parent commands
```
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```
### SEE ALSO
* [kn](kn.md) - Knative client
* [kn plugin list](kn_plugin_list.md) - List all visible plugin executables

View File

@ -0,0 +1,38 @@
## kn plugin list
List all visible plugin executables
### Synopsis
List all visible plugin executables.
Available plugin files are those that are:
- executable
- begin with "kn-
- anywhere on the path specified in Kn's config pluginDir variable, which:
* can be overridden with the --plugin-dir flag
```
kn plugin list [flags]
```
### Options
```
-h, --help help for list
--lookup-plugins-in-path look for kn plugins in $PATH
--name-only If true, display only the binary name of each plugin, rather than its full path
--plugins-dir string kn plugins directory (default "~/.kn/plugins")
```
### Options inherited from parent commands
```
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```
### SEE ALSO
* [kn plugin](kn_plugin.md) - Plugin command group

View File

@ -19,7 +19,7 @@ kn revision [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -28,7 +28,7 @@ kn revision delete NAME [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -23,7 +23,7 @@ kn revision describe NAME [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -42,7 +42,7 @@ kn revision list [name] [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -19,7 +19,7 @@ kn route [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -23,7 +23,7 @@ kn route describe NAME [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -38,7 +38,7 @@ kn route list NAME [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -19,7 +19,7 @@ kn service [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -60,7 +60,7 @@ kn service create NAME --image IMAGE [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -31,7 +31,7 @@ kn service delete NAME [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -23,7 +23,7 @@ kn service describe NAME [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -38,7 +38,7 @@ kn service list [name] [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -47,7 +47,7 @@ kn service update NAME [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -19,7 +19,7 @@ kn version [flags]
### Options inherited from parent commands
```
--config string config file (default is $HOME/.kn/config.yaml)
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
```

View File

@ -0,0 +1,102 @@
// Copyright © 2018 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/spf13/cobra"
)
// PathVerifier receives a path and determines if it is valid or not
type PathVerifier interface {
// Verify determines if a given path is valid
Verify(path string) []error
}
// CommandOverrideVerifier verifies that existing kn commands are not overriden
type CommandOverrideVerifier struct {
Root *cobra.Command
SeenPlugins map[string]string
}
// Verify implements PathVerifier and determines if a given path
// is valid depending on whether or not it overwrites an existing
// kn command path, or a previously seen plugin.
func (v *CommandOverrideVerifier) Verify(path string) []error {
if v.Root == nil {
return []error{fmt.Errorf("unable to verify path with nil root")}
}
// extract the plugin binary name
segs := strings.Split(path, string(os.PathSeparator))
binName := segs[len(segs)-1]
cmdPath := strings.Split(binName, "-")
if len(cmdPath) > 1 {
// the first argument is always "kn" for a plugin binary
cmdPath = cmdPath[1:]
}
errors := []error{}
isExec, err := isExecutable(path)
if err == nil && !isExec {
errors = append(errors, fmt.Errorf("warning: %s identified as a kn plugin, but it is not executable", path))
} else if err != nil {
errors = append(errors, fmt.Errorf("error: unable to identify %s as an executable file: %v", path, err))
}
if existingPath, ok := v.SeenPlugins[binName]; ok {
errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named plugin: %s", path, existingPath))
} else {
v.SeenPlugins[binName] = path
}
cmd, _, err := v.Root.Find(cmdPath)
if err == nil {
errors = append(errors, fmt.Errorf("warning: %s overwrites existing command: %q", binName, cmd.CommandPath()))
}
return errors
}
// Private functions
func isExecutable(fullPath string) (bool, error) {
info, err := os.Stat(fullPath)
if err != nil {
return false, err
}
if runtime.GOOS == "windows" {
fileExt := strings.ToLower(filepath.Ext(fullPath))
switch fileExt {
case ".bat", ".cmd", ".com", ".exe", ".ps1":
return true, nil
}
return false, nil
}
if m := info.Mode(); !m.IsDir() && m&0111 != 0 {
return true, nil
}
return false, nil
}

View File

@ -0,0 +1,112 @@
// Copyright © 2018 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"fmt"
"strings"
"testing"
"github.com/knative/client/pkg/kn/commands"
"github.com/spf13/cobra"
"gotest.tools/assert"
)
func TestCommandOverrideVerifier(t *testing.T) {
var (
pluginPath string
rootCmd *cobra.Command
verifier *CommandOverrideVerifier
)
setup := func(t *testing.T) {
knParams := &commands.KnParams{}
rootCmd, _, _ = commands.CreateTestKnCommand(NewPluginCommand(knParams), knParams)
verifier = &CommandOverrideVerifier{
Root: rootCmd,
SeenPlugins: make(map[string]string),
}
}
cleanup := func(t *testing.T) {
if pluginPath != "" {
DeleteTestPlugin(t, pluginPath)
}
}
t.Run("with nil root command", func(t *testing.T) {
t.Run("returns error verifying path", func(t *testing.T) {
setup(t)
defer cleanup(t)
verifier.Root = nil
errs := verifier.Verify(pluginPath)
assert.Assert(t, len(errs) == 1)
assert.Assert(t, errs[0] != nil)
assert.Assert(t, strings.Contains(errs[0].Error(), "unable to verify path with nil root"))
})
})
t.Run("with root command", func(t *testing.T) {
t.Run("when plugin in path not executable", func(t *testing.T) {
setup(t)
defer cleanup(t)
pluginPath = CreateTestPlugin(t, KnTestPluginName, KnTestPluginScript, FileModeReadable)
t.Run("fails with not executable error", func(t *testing.T) {
errs := verifier.Verify(pluginPath)
assert.Assert(t, len(errs) == 1)
assert.Assert(t, errs[0] != nil)
errorMsg := fmt.Sprintf("warning: %s identified as a kn plugin, but it is not executable", pluginPath)
assert.Assert(t, strings.Contains(errs[0].Error(), errorMsg))
})
})
t.Run("when kn plugin in path is executable", func(t *testing.T) {
setup(t)
defer cleanup(t)
pluginPath = CreateTestPlugin(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable)
t.Run("when kn plugin in path shadows another", func(t *testing.T) {
var shadowPluginPath = CreateTestPlugin(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable)
verifier.SeenPlugins[KnTestPluginName] = pluginPath
defer DeleteTestPlugin(t, shadowPluginPath)
t.Run("fails with overshadowed error", func(t *testing.T) {
errs := verifier.Verify(shadowPluginPath)
assert.Assert(t, len(errs) == 1)
assert.Assert(t, errs[0] != nil)
errorMsg := fmt.Sprintf("warning: %s is overshadowed by a similarly named plugin: %s", shadowPluginPath, pluginPath)
assert.Assert(t, strings.Contains(errs[0].Error(), errorMsg))
})
})
})
t.Run("when kn plugin in path overwrites existing command", func(t *testing.T) {
setup(t)
defer cleanup(t)
var overwritingPluginPath = CreateTestPlugin(t, "kn-plugin", KnTestPluginScript, FileModeExecutable)
defer DeleteTestPlugin(t, overwritingPluginPath)
t.Run("fails with overwrites error", func(t *testing.T) {
errs := verifier.Verify(overwritingPluginPath)
assert.Assert(t, len(errs) == 1)
assert.Assert(t, errs[0] != nil)
errorMsg := fmt.Sprintf("warning: %s overwrites existing command: %q", "kn-plugin", "kn plugin")
assert.Assert(t, strings.Contains(errs[0].Error(), errorMsg))
})
})
})
}

View File

@ -0,0 +1,54 @@
// Copyright © 2018 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"github.com/knative/client/pkg/kn/commands"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func NewPluginCommand(p *commands.KnParams) *cobra.Command {
pluginCmd := &cobra.Command{
Use: "plugin",
Short: "Plugin command group",
Long: `Provides utilities for interacting and managing with kn plugins.
Plugins provide extended functionality that is not part of the core kn command-line distribution.
Please refer to the documentation and examples for more information about how write your own plugins.`,
}
AddPluginFlags(pluginCmd)
BindPluginsFlagToViper(pluginCmd)
pluginCmd.AddCommand(NewPluginListCommand(p))
return pluginCmd
}
// AddPluginFlags plugins-dir and lookup-plugins-in-path to cmd
func AddPluginFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&commands.Cfg.PluginsDir, "plugins-dir", "~/.kn/plugins", "kn plugins directory")
cmd.Flags().BoolVar(&commands.Cfg.LookupPluginsInPath, "lookup-plugins-in-path", false, "look for kn plugins in $PATH")
}
// BindPluginsFlagToViper bind and set default with viper for plugins flags
func BindPluginsFlagToViper(cmd *cobra.Command) {
viper.BindPFlag("pluginsDir", cmd.Flags().Lookup("plugins-dir"))
viper.BindPFlag("lookupPluginsInPath", cmd.Flags().Lookup("lookup-plugins-in-path"))
viper.SetDefault("pluginsDir", "~/.kn/plugins")
viper.SetDefault("lookupPluginsInPath", false)
}

View File

@ -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")
}

View File

@ -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)
})
}

View File

@ -0,0 +1,136 @@
// Copyright © 2019 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)
// PluginHandler is capable of parsing command line arguments
// and performing executable filename lookups to search
// for valid plugin files, and execute found plugins.
type PluginHandler interface {
// exists at the given filename, or a boolean false.
// Lookup will iterate over a list of given prefixes
// in order to recognize valid plugin filenames.
// The first filepath to match a prefix is returned.
Lookup(name string) (string, bool)
// Execute receives an executable's filepath, a slice
// of arguments, and a slice of environment variables
// to relay to the executable.
Execute(executablePath string, cmdArgs, environment []string) error
}
// DefaultPluginHandler implements PluginHandler
type DefaultPluginHandler struct {
ValidPrefixes []string
PluginsDir string
LookupPluginsInPath bool
}
// NewDefaultPluginHandler instantiates the DefaultPluginHandler with a list of
// given filename prefixes used to identify valid plugin filenames.
func NewDefaultPluginHandler(validPrefixes []string, pluginsDir string, lookupPluginsInPath bool) *DefaultPluginHandler {
return &DefaultPluginHandler{
ValidPrefixes: validPrefixes,
PluginsDir: pluginsDir,
LookupPluginsInPath: lookupPluginsInPath,
}
}
// Lookup implements PluginHandler
func (h *DefaultPluginHandler) Lookup(name string) (string, bool) {
for _, prefix := range h.ValidPrefixes {
pluginPath := fmt.Sprintf("%s-%s", prefix, name)
// Try to find plugin in pluginsDir
pluginDir, err := ExpandPath(h.PluginsDir)
if err != nil {
return "", false
}
pluginDirPluginPath := filepath.Join(pluginDir, pluginPath)
_, err = os.Stat(pluginDirPluginPath)
if !os.IsNotExist(err) {
return pluginDirPluginPath, true
}
// No plugins found in pluginsDir, try in PATH of that's an option
if h.LookupPluginsInPath {
pluginPath, err = exec.LookPath(pluginPath)
if err != nil {
continue
}
if pluginPath != "" {
return pluginPath, true
}
}
}
return "", false
}
// Execute implements PluginHandler
func (h *DefaultPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error {
return syscall.Exec(executablePath, cmdArgs, environment)
}
// HandlePluginCommand receives a pluginHandler and command-line arguments and attempts to find
// a plugin executable that satisfies the given arguments.
func HandlePluginCommand(pluginHandler PluginHandler, cmdArgs []string) error {
remainingArgs := []string{}
for idx := range cmdArgs {
if strings.HasPrefix(cmdArgs[idx], "-") {
continue
}
remainingArgs = append(remainingArgs, strings.Replace(cmdArgs[idx], "-", "_", -1))
}
foundBinaryPath := ""
// attempt to find binary, starting at longest possible name with given cmdArgs
for len(remainingArgs) > 0 {
path, found := pluginHandler.Lookup(strings.Join(remainingArgs, "-"))
if !found {
remainingArgs = remainingArgs[:len(remainingArgs)-1]
continue
}
foundBinaryPath = path
break
}
if len(foundBinaryPath) == 0 {
return errors.New("Could not find plugin to execute")
}
// invoke cmd binary relaying the current environment and args given
// remainingArgs will always have at least one element.
// execve will make remainingArgs[0] the "binary name".
err := pluginHandler.Execute(foundBinaryPath, append([]string{foundBinaryPath}, cmdArgs[len(remainingArgs):]...), os.Environ())
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,178 @@
// Copyright © 2018 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"gotest.tools/assert"
)
func TestPluginHandler(t *testing.T) {
var (
pluginHandler, tPluginHandler PluginHandler
pluginPath, pluginName, tmpPathDir, pluginsDir string
lookupPluginsInPath bool
err error
)
setup := func(t *testing.T) {
tmpPathDir, err = ioutil.TempDir("", "plugin_list")
assert.Assert(t, err == nil)
pluginsDir = tmpPathDir
}
cleanup := func(t *testing.T) {
err = os.RemoveAll(tmpPathDir)
assert.Assert(t, err == nil)
}
beforeEach := func(t *testing.T) {
pluginName = "fake"
pluginPath = CreateTestPluginInPath(t, "kn-"+pluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir)
assert.Assert(t, pluginPath != "")
pluginHandler = &DefaultPluginHandler{
ValidPrefixes: []string{"kn"},
PluginsDir: pluginsDir,
LookupPluginsInPath: lookupPluginsInPath,
}
assert.Assert(t, pluginHandler != nil)
tPluginHandler = NewTestPluginHandler(pluginHandler)
assert.Assert(t, tPluginHandler != nil)
}
t.Run("#NewDefaultPluginHandler", func(t *testing.T) {
setup(t)
defer cleanup(t)
pHandler := NewDefaultPluginHandler([]string{"kn"}, pluginPath, false)
assert.Assert(t, pHandler != nil)
})
t.Run("#Lookup", func(t *testing.T) {
t.Run("when plugin in pluginsDir", func(t *testing.T) {
t.Run("returns the first filepath matching prefix", func(t *testing.T) {
setup(t)
defer cleanup(t)
beforeEach(t)
path, exists := pluginHandler.Lookup(pluginName)
assert.Assert(t, path != "", fmt.Sprintf("no path when Lookup(%s)", pluginName))
assert.Assert(t, exists == true, fmt.Sprintf("could not Lookup(%s)", pluginName))
})
t.Run("returns empty filepath when no matching prefix found", func(t *testing.T) {
setup(t)
defer cleanup(t)
path, exists := pluginHandler.Lookup("bogus-plugin-name")
assert.Assert(t, path == "", fmt.Sprintf("unexpected plugin: kn-bogus-plugin-name"))
assert.Assert(t, exists == false, fmt.Sprintf("unexpected plugin: kn-bogus-plugin-name"))
})
})
t.Run("when plugin is in $PATH", func(t *testing.T) {
t.Run("--lookup-plugins-in-path=true", func(t *testing.T) {
setup(t)
defer cleanup(t)
pluginsDir = filepath.Join(tmpPathDir, "bogus")
err = os.Setenv("PATH", tmpPathDir)
assert.Assert(t, err == nil)
lookupPluginsInPath = true
beforeEach(t)
path, exists := pluginHandler.Lookup(pluginName)
assert.Assert(t, path != "", fmt.Sprintf("no path when Lookup(%s)", pluginName))
assert.Assert(t, exists == true, fmt.Sprintf("could not Lookup(%s)", pluginName))
})
t.Run("--lookup-plugins-in-path=false", func(t *testing.T) {
setup(t)
defer cleanup(t)
pluginsDir = filepath.Join(tmpPathDir, "bogus")
err = os.Setenv("PATH", tmpPathDir)
assert.Assert(t, err == nil)
lookupPluginsInPath = false
beforeEach(t)
path, exists := pluginHandler.Lookup(pluginName)
assert.Assert(t, path == "")
assert.Assert(t, exists == false)
})
})
})
t.Run("#Execute", func(t *testing.T) {
t.Run("fails executing bogus plugin name", func(t *testing.T) {
setup(t)
defer cleanup(t)
beforeEach(t)
bogusPath := filepath.Join(filepath.Dir(pluginPath), "kn-bogus-plugin-name")
err = pluginHandler.Execute(bogusPath, []string{bogusPath}, os.Environ())
assert.Assert(t, err != nil, fmt.Sprintf("bogus plugin in path %s unexpectedly executed OK", bogusPath))
})
})
t.Run("HandlePluginCommand", func(t *testing.T) {
t.Run("sucess handling", func(t *testing.T) {
setup(t)
defer cleanup(t)
beforeEach(t)
err = HandlePluginCommand(tPluginHandler, []string{pluginName})
assert.Assert(t, err == nil, fmt.Sprintf("test plugin %s failed executing", fmt.Sprintf("kn-%s", pluginName)))
})
t.Run("fails handling", func(t *testing.T) {
setup(t)
defer cleanup(t)
err = HandlePluginCommand(tPluginHandler, []string{"bogus"})
assert.Assert(t, err != nil, fmt.Sprintf("test plugin %s expected to fail executing", "bogus"))
})
})
}
// TestPluginHandler - needed to mock Execute() call
type testPluginHandler struct {
pluginHandler PluginHandler
}
func NewTestPluginHandler(pluginHandler PluginHandler) PluginHandler {
return &testPluginHandler{
pluginHandler: pluginHandler,
}
}
func (tHandler *testPluginHandler) Lookup(name string) (string, bool) {
return tHandler.pluginHandler.Lookup(name)
}
func (tHandler *testPluginHandler) Execute(executablePath string, cmdArgs, environment []string) error {
// Always success (avoids doing syscall.Exec which exits tests framework)
return nil
}

View File

@ -0,0 +1,224 @@
// Copyright © 2019 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/knative/client/pkg/kn/commands"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
homedir "github.com/mitchellh/go-homedir"
)
// ValidPluginFilenamePrefixes controls the prefix for all kn plugins
var ValidPluginFilenamePrefixes = []string{"kn"}
// NewPluginListCommand creates a new `kn plugin list` command
func NewPluginListCommand(p *commands.KnParams) *cobra.Command {
pluginFlags := PluginFlags{
IOStreams: genericclioptions.IOStreams{
In: os.Stdin,
Out: os.Stdout,
ErrOut: os.Stderr,
},
}
pluginListCommand := &cobra.Command{
Use: "list",
Short: "List all visible plugin executables",
Long: `List all visible plugin executables.
Available plugin files are those that are:
- executable
- begin with "kn-
- anywhere on the path specified in Kn's config pluginDir variable, which:
* can be overridden with the --plugin-dir flag`,
RunE: func(cmd *cobra.Command, args []string) error {
err := pluginFlags.complete(cmd)
if err != nil {
return err
}
err = pluginFlags.run()
if err != nil {
return err
}
return nil
},
}
AddPluginFlags(pluginListCommand)
BindPluginsFlagToViper(pluginListCommand)
pluginFlags.AddPluginFlags(pluginListCommand)
return pluginListCommand
}
// ExpandPath to a canonical path (need to see if Golang has a better option)
func ExpandPath(path string) (string, error) {
if strings.Contains(path, "~") {
var err error
path, err = expandHomeDir(path)
if err != nil {
return "", err
}
}
return path, nil
}
// Private
func (o *PluginFlags) complete(cmd *cobra.Command) error {
o.Verifier = &CommandOverrideVerifier{
Root: cmd.Root(),
SeenPlugins: make(map[string]string, 0),
}
pluginPath, err := ExpandPath(commands.Cfg.PluginsDir)
if err != nil {
return err
}
if commands.Cfg.LookupPluginsInPath {
pluginPath = pluginPath + string(os.PathListSeparator) + os.Getenv("PATH")
}
o.PluginPaths = filepath.SplitList(pluginPath)
return nil
}
func (o *PluginFlags) run() error {
pluginsFound := false
isFirstFile := true
pluginErrors := []error{}
pluginWarnings := 0
for _, dir := range uniquePathsList(o.PluginPaths) {
if dir == "" {
continue
}
files, err := ioutil.ReadDir(dir)
if err != nil {
if _, ok := err.(*os.PathError); ok {
fmt.Fprintf(o.ErrOut, "Unable read directory '%s' from your plugins path: %v. Skipping...", dir, err)
continue
}
pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to read directory '%s' from your plugin path: %v", dir, err))
continue
}
for _, f := range files {
if f.IsDir() {
continue
}
if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) {
continue
}
if isFirstFile {
fmt.Fprintf(o.ErrOut, "The following compatible plugins are available, using options:\n")
fmt.Fprintf(o.ErrOut, " - plugins dir: '%s'\n", commands.Cfg.PluginsDir)
fmt.Fprintf(o.ErrOut, " - lookup plugins in path: '%t'\n\n", commands.Cfg.LookupPluginsInPath)
pluginsFound = true
isFirstFile = false
}
pluginPath := f.Name()
if !o.NameOnly {
pluginPath = filepath.Join(dir, pluginPath)
}
fmt.Fprintf(o.Out, "%s\n", pluginPath)
if errs := o.Verifier.Verify(filepath.Join(dir, f.Name())); len(errs) != 0 {
for _, err := range errs {
fmt.Fprintf(o.ErrOut, " - %s\n", err)
pluginWarnings++
}
}
}
}
if !pluginsFound {
pluginErrors = append(pluginErrors, fmt.Errorf("warning: unable to find any kn plugins in your plugin path: '%s'", o.PluginPaths))
}
if pluginWarnings > 0 {
if pluginWarnings == 1 {
pluginErrors = append(pluginErrors, fmt.Errorf("error: one plugin warning was found"))
} else {
pluginErrors = append(pluginErrors, fmt.Errorf("error: %v plugin warnings were found", pluginWarnings))
}
}
if len(pluginErrors) > 0 {
fmt.Fprintln(o.ErrOut)
errs := bytes.NewBuffer(nil)
for _, e := range pluginErrors {
fmt.Fprintln(errs, e)
}
return fmt.Errorf("%s", errs.String())
}
return nil
}
// Private
// expandHomeDir replaces the ~ with the home directory value
func expandHomeDir(path string) (string, error) {
home, err := homedir.Dir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return "", err
}
return strings.Replace(path, "~", home, -1), nil
}
// uniquePathsList deduplicates a given slice of strings without
// sorting or otherwise altering its order in any way.
func uniquePathsList(paths []string) []string {
seen := map[string]bool{}
newPaths := []string{}
for _, p := range paths {
if seen[p] {
continue
}
seen[p] = true
newPaths = append(newPaths, p)
}
return newPaths
}
func hasValidPrefix(filepath string, validPrefixes []string) bool {
for _, prefix := range validPrefixes {
if !strings.HasPrefix(filepath, prefix+"-") {
continue
}
return true
}
return false
}

View File

@ -0,0 +1,240 @@
// Copyright © 2018 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/knative/client/pkg/kn/commands"
"github.com/spf13/cobra"
"gotest.tools/assert"
)
func TestPluginList(t *testing.T) {
var (
rootCmd, pluginCmd, pluginListCmd *cobra.Command
tmpPathDir, pluginsDir, pluginsDirFlag string
err error
)
setup := func(t *testing.T) {
knParams := &commands.KnParams{}
pluginCmd = NewPluginCommand(knParams)
assert.Assert(t, pluginCmd != nil)
rootCmd, _, _ = commands.CreateTestKnCommand(pluginCmd, knParams)
assert.Assert(t, rootCmd != nil)
pluginListCmd = FindSubCommand(t, pluginCmd, "list")
assert.Assert(t, pluginListCmd != nil)
tmpPathDir, err = ioutil.TempDir("", "plugin_list")
assert.Assert(t, err == nil)
pluginsDir = filepath.Join(tmpPathDir, "plugins")
pluginsDirFlag = fmt.Sprintf("--plugins-dir=%s", pluginsDir)
}
cleanup := func(t *testing.T) {
err = os.RemoveAll(tmpPathDir)
assert.Assert(t, err == nil)
}
t.Run("creates a new cobra.Command", func(t *testing.T) {
setup(t)
defer cleanup(t)
assert.Assert(t, pluginListCmd != nil)
assert.Assert(t, pluginListCmd.Use == "list")
assert.Assert(t, pluginListCmd.Short == "List all visible plugin executables")
assert.Assert(t, strings.Contains(pluginListCmd.Long, "List all visible plugin executables"))
assert.Assert(t, pluginListCmd.Flags().Lookup("plugins-dir") != nil)
assert.Assert(t, pluginListCmd.RunE != nil)
})
t.Run("when pluginsDir does not include any plugins", func(t *testing.T) {
t.Run("when --lookup-plugins-in-path is true", func(t *testing.T) {
var pluginPath string
beforeEach := func(t *testing.T) {
err = os.Setenv("PATH", tmpPathDir)
assert.Assert(t, err == nil)
}
t.Run("no plugins installed", func(t *testing.T) {
setup(t)
defer cleanup(t)
beforeEach(t)
t.Run("warns user that no plugins found", func(t *testing.T) {
rootCmd.SetArgs([]string{"plugin", "list", "--lookup-plugins-in-path=true", pluginsDirFlag})
err = rootCmd.Execute()
assert.Assert(t, err != nil)
assert.Assert(t, strings.Contains(err.Error(), "warning: unable to find any kn plugins in your plugin path:"))
})
})
t.Run("plugins installed", func(t *testing.T) {
t.Run("with valid plugin in $PATH", func(t *testing.T) {
beforeEach := func(t *testing.T) {
pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir)
assert.Assert(t, pluginPath != "")
err = os.Setenv("PATH", tmpPathDir)
assert.Assert(t, err == nil)
}
t.Run("list plugins in $PATH", func(t *testing.T) {
setup(t)
defer cleanup(t)
beforeEach(t)
commands.CaptureStdout(t)
rootCmd.SetArgs([]string{"plugin", "list", "--lookup-plugins-in-path=true", pluginsDirFlag})
err = rootCmd.Execute()
assert.Assert(t, err == nil)
})
})
t.Run("with non-executable plugin", func(t *testing.T) {
beforeEach := func(t *testing.T) {
pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeReadable, tmpPathDir)
assert.Assert(t, pluginPath != "")
}
t.Run("warns user plugin invalid", func(t *testing.T) {
setup(t)
defer cleanup(t)
beforeEach(t)
rootCmd.SetArgs([]string{"plugin", "list", "--lookup-plugins-in-path=true", pluginsDirFlag})
err = rootCmd.Execute()
assert.Assert(t, err != nil)
assert.Assert(t, strings.Contains(err.Error(), "warning: unable to find any kn plugins in your plugin path:"))
})
})
t.Run("with plugins with same name", func(t *testing.T) {
var tmpPathDir2 string
beforeEach := func(t *testing.T) {
pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir)
assert.Assert(t, pluginPath != "")
tmpPathDir2, err = ioutil.TempDir("", "plugins_list")
assert.Assert(t, err == nil)
err = os.Setenv("PATH", tmpPathDir+string(os.PathListSeparator)+tmpPathDir2)
assert.Assert(t, err == nil)
pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir2)
assert.Assert(t, pluginPath != "")
}
afterEach := func(t *testing.T) {
err = os.RemoveAll(tmpPathDir)
assert.Assert(t, err == nil)
err = os.RemoveAll(tmpPathDir2)
assert.Assert(t, err == nil)
}
t.Run("warns user about second (in $PATH) plugin shadowing first", func(t *testing.T) {
setup(t)
defer cleanup(t)
beforeEach(t)
defer afterEach(t)
rootCmd.SetArgs([]string{"plugin", "list", "--lookup-plugins-in-path=true", pluginsDirFlag})
err = rootCmd.Execute()
assert.Assert(t, err != nil)
assert.Assert(t, strings.Contains(err.Error(), "error: one plugin warning was found"))
})
})
t.Run("with plugins with name of existing command", func(t *testing.T) {
var fakeCmd *cobra.Command
beforeEach := func(t *testing.T) {
fakeCmd = &cobra.Command{
Use: "fake",
}
rootCmd.AddCommand(fakeCmd)
pluginPath = CreateTestPluginInPath(t, "kn-fake", KnTestPluginScript, FileModeExecutable, tmpPathDir)
assert.Assert(t, pluginPath != "")
err = os.Setenv("PATH", tmpPathDir)
assert.Assert(t, err == nil)
}
afterEach := func(t *testing.T) {
rootCmd.RemoveCommand(fakeCmd)
}
t.Run("warns user about overwritting exising command", func(t *testing.T) {
setup(t)
defer cleanup(t)
beforeEach(t)
defer afterEach(t)
rootCmd.SetArgs([]string{"plugin", "list", "--lookup-plugins-in-path=true", pluginsDirFlag})
err = rootCmd.Execute()
assert.Assert(t, err != nil)
assert.Assert(t, strings.Contains(err.Error(), "error: one plugin warning was found"))
})
})
})
})
})
t.Run("when pluginsDir has plugins", func(t *testing.T) {
var pluginPath string
beforeEach := func(t *testing.T) {
pluginPath = CreateTestPluginInPath(t, KnTestPluginName, KnTestPluginScript, FileModeExecutable, tmpPathDir)
assert.Assert(t, pluginPath != "")
err = os.Setenv("PATH", "")
assert.Assert(t, err == nil)
pluginsDirFlag = fmt.Sprintf("--plugins-dir=%s", tmpPathDir)
}
t.Run("list plugins in --plugins-dir", func(t *testing.T) {
setup(t)
defer cleanup(t)
beforeEach(t)
rootCmd.SetArgs([]string{"plugin", "list", pluginsDirFlag})
err = rootCmd.Execute()
assert.Assert(t, err == nil)
})
t.Run("no plugins installed", func(t *testing.T) {
setup(t)
defer cleanup(t)
rootCmd.SetArgs([]string{"plugin", "list", pluginsDirFlag})
err = rootCmd.Execute()
assert.Assert(t, err != nil)
})
})
}

View File

@ -0,0 +1,74 @@
// Copyright © 2018 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"strings"
"testing"
"github.com/knative/client/pkg/kn/commands"
"github.com/spf13/cobra"
"gotest.tools/assert"
)
const PluginCommandUsage = `Provides utilities for interacting and managing with kn plugins.
Plugins provide extended functionality that is not part of the core kn command-line distribution.
Please refer to the documentation and examples for more information about how write your own plugins.
Usage:
kn plugin [flags]
kn plugin [command]
Available Commands:
list List all visible plugin executables
Flags:
-h, --help help for plugin
--lookup-plugins-in-path look for kn plugins in $PATH
--plugins-dir string kn plugins directory (default "~/.kn/plugins")
Global Flags:
--config string kn config file (default is $HOME/.kn/config.yaml)
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
Use "kn plugin [command] --help" for more information about a command.`
func TestNewPluginCommand(t *testing.T) {
var (
rootCmd, pluginCmd *cobra.Command
)
setup := func(t *testing.T) {
knParams := &commands.KnParams{}
pluginCmd = NewPluginCommand(knParams)
assert.Assert(t, pluginCmd != nil)
rootCmd, _, _ = commands.CreateTestKnCommand(pluginCmd, knParams)
assert.Assert(t, rootCmd != nil)
}
t.Run("creates a new cobra.Command", func(t *testing.T) {
setup(t)
assert.Assert(t, pluginCmd != nil)
assert.Assert(t, pluginCmd.Use == "plugin")
assert.Assert(t, pluginCmd.Short == "Plugin command group")
assert.Assert(t, strings.Contains(pluginCmd.Long, "Provides utilities for interacting and managing with kn plugins."))
assert.Assert(t, pluginCmd.Flags().Lookup("plugins-dir") != nil)
assert.Assert(t, pluginCmd.Flags().Lookup("lookup-plugins-in-path") != nil)
assert.Assert(t, pluginCmd.Args == nil)
})
}

View File

@ -0,0 +1,69 @@
// Copyright © 2018 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/spf13/cobra"
"gotest.tools/assert"
)
const (
KnTestPluginName = "kn-test"
KnTestPluginScript = `#!/bin/bash
echo "I am a test Kn plugin"
exit 0
`
FileModeReadable = 0644
FileModeExecutable = 0777
)
// FindSubCommand return the sub-command by name
func FindSubCommand(t *testing.T, rootCmd *cobra.Command, name string) *cobra.Command {
for _, subCmd := range rootCmd.Commands() {
if subCmd.Name() == name {
return subCmd
}
}
return nil
}
// CreateTestPlugin with name, script, and fileMode and return the tmp random path
func CreateTestPlugin(t *testing.T, name, script string, fileMode os.FileMode) string {
path, err := ioutil.TempDir("", "plugin")
assert.Assert(t, err == nil)
return CreateTestPluginInPath(t, name, script, fileMode, path)
}
// CreateTestPluginInPath with name, path, script, and fileMode and return the tmp random path
func CreateTestPluginInPath(t *testing.T, name, script string, fileMode os.FileMode, path string) string {
err := ioutil.WriteFile(filepath.Join(path, name), []byte(script), fileMode)
assert.Assert(t, err == nil)
return filepath.Join(path, name)
}
// DeleteTestPlugin with path
func DeleteTestPlugin(t *testing.T, path string) {
err := os.RemoveAll(filepath.Dir(path))
assert.Assert(t, err == nil)
}

View File

@ -24,6 +24,7 @@ import (
"github.com/knative/client/pkg/serving/v1alpha1"
"github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1/fake"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gotest.tools/assert"
client_testing "k8s.io/client-go/testing"
)
@ -113,6 +114,15 @@ Eventing: Manage event subscriptions and channels. Connect up event sources.`,
rootCmd.PersistentFlags().StringVar(&CfgFile, "config", "", "config file (default is $HOME/.kn.yaml)")
rootCmd.PersistentFlags().StringVar(&params.KubeCfgPath, "kubeconfig", "", "kubectl config file (default is $HOME/.kube/config)")
rootCmd.Flags().StringVar(&Cfg.PluginsDir, "plugins-dir", "~/.kn/plugins", "kn plugins directory")
rootCmd.Flags().BoolVar(&Cfg.LookupPluginsInPath, "lookup-plugins-in-path", false, "look for kn plugins in $PATH")
viper.BindPFlag("pluginsDir", rootCmd.Flags().Lookup("plugins-dir"))
viper.BindPFlag("lookupPluginsInPath", rootCmd.Flags().Lookup("lookup-plugins-in-path"))
viper.SetDefault("pluginsDir", "~/.kn/plugins")
viper.SetDefault("lookupPluginsInPath", false)
rootCmd.AddCommand(subCommand)
// For glog parse error.

View File

@ -0,0 +1,56 @@
// Copyright © 2018 The Knative Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"strings"
"testing"
"github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1/fake"
"github.com/spf13/cobra"
"gotest.tools/assert"
)
func TestCreateTestKnCommand(t *testing.T) {
var (
knCmd *cobra.Command
serving *fake.FakeServingV1alpha1
buffer *bytes.Buffer
)
setup := func(t *testing.T) {
knParams := &KnParams{}
knCmd, serving, buffer = CreateTestKnCommand(&cobra.Command{Use: "fake"}, knParams)
assert.Assert(t, knCmd != nil)
assert.Assert(t, len(knCmd.Commands()) == 1)
assert.Assert(t, knCmd.Commands()[0].Use == "fake")
assert.Assert(t, serving != nil)
assert.Assert(t, buffer != nil)
}
t.Run("creates a new kn cobra.Command", func(t *testing.T) {
setup(t)
assert.Assert(t, knCmd != nil)
assert.Assert(t, knCmd.Use == "kn")
assert.Assert(t, knCmd.Short == "Knative client")
assert.Assert(t, strings.Contains(knCmd.Long, "Manage your Knative building blocks:"))
assert.Assert(t, knCmd.RunE == nil)
assert.Assert(t, knCmd.DisableAutoGenTag == true)
assert.Assert(t, knCmd.SilenceUsage == true)
assert.Assert(t, knCmd.SilenceErrors == true)
})
}

View File

@ -22,7 +22,6 @@ import (
"path/filepath"
serving_kn_v1alpha1 "github.com/knative/client/pkg/serving/v1alpha1"
serving_v1alpha1_client "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1"
"k8s.io/client-go/tools/clientcmd"
)
@ -30,6 +29,15 @@ import (
// CfgFile is Kn's config file is the path for the Kubernetes config
var CfgFile string
// Cfg is Kn's configuration values
var Cfg Config
// Config contains the variables for the Kn config
type Config struct {
PluginsDir string
LookupPluginsInPath bool
}
// Parameters for creating commands. Useful for inserting mocks for testing.
type KnParams struct {
Output io.Writer

View File

@ -18,24 +18,74 @@ import (
"errors"
"flag"
"fmt"
"io"
"os"
"path"
"strconv"
"strings"
"github.com/knative/client/pkg/kn/commands"
"github.com/knative/client/pkg/kn/commands/plugin"
"github.com/knative/client/pkg/kn/commands/revision"
"github.com/knative/client/pkg/kn/commands/route"
"github.com/knative/client/pkg/kn/commands/service"
"github.com/mitchellh/go-homedir"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
)
var cfgFile string
var kubeCfgFile string
// NewDefaultKnCommand creates the default `kn` command with a default plugin handler
func NewDefaultKnCommand() *cobra.Command {
rootCmd := NewKnCommand()
// NewKnCommand creates new rootCmd represents the base command when called without any subcommands
// Needed since otherwise --plugins-dir and --lookup-plugins-in-path
// will not be accounted for since the plugin is not a Cobra command
// and will not be parsed
pluginsDir, lookupPluginsInPath, err := extractKnPluginFlags(os.Args)
if err != nil {
panic("Invalid plugin flag value")
}
pluginHandler := plugin.NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes,
pluginsDir, lookupPluginsInPath)
return NewDefaultKnCommandWithArgs(rootCmd, pluginHandler,
os.Args, os.Stdin,
os.Stdout, os.Stderr)
}
// NewDefaultKnCommandWithArgs creates the `kn` command with arguments
func NewDefaultKnCommandWithArgs(rootCmd *cobra.Command,
pluginHandler plugin.PluginHandler,
args []string,
in io.Reader,
out,
errOut io.Writer) *cobra.Command {
if pluginHandler == nil {
return rootCmd
}
if len(args) > 1 {
cmdPathPieces := args[1:]
cmdPathPieces = removeKnPluginFlags(cmdPathPieces) // Plugin does not need these flags
// only look for suitable extension executables if
// the specified command does not already exist
if _, _, err := rootCmd.Find(cmdPathPieces); err != nil {
err := plugin.HandlePluginCommand(pluginHandler, cmdPathPieces)
if err != nil {
fmt.Fprintf(errOut, "%v\n", err)
os.Exit(1)
}
}
}
return rootCmd
}
// NewKnCommand creates the rootCmd which is the base command when called without any subcommands
func NewKnCommand(params ...commands.KnParams) *cobra.Command {
var p *commands.KnParams
if len(params) == 0 {
@ -67,11 +117,18 @@ func NewKnCommand(params ...commands.KnParams) *cobra.Command {
if p.Output != nil {
rootCmd.SetOutput(p.Output)
}
rootCmd.PersistentFlags().StringVar(&commands.CfgFile, "config", "", "config file (default is $HOME/.kn/config.yaml)")
// Persistent flags
rootCmd.PersistentFlags().StringVar(&commands.CfgFile, "config", "", "kn config file (default is $HOME/.kn/config.yaml)")
rootCmd.PersistentFlags().StringVar(&p.KubeCfgPath, "kubeconfig", "", "kubectl config file (default is $HOME/.kube/config)")
plugin.AddPluginFlags(rootCmd)
plugin.BindPluginsFlagToViper(rootCmd)
// root child commands
rootCmd.AddCommand(service.NewServiceCommand(p))
rootCmd.AddCommand(revision.NewRevisionCommand(p))
rootCmd.AddCommand(plugin.NewPluginCommand(p))
rootCmd.AddCommand(route.NewRouteCommand(p))
rootCmd.AddCommand(commands.NewCompletionCommand(p))
rootCmd.AddCommand(commands.NewVersionCommand(p))
@ -81,9 +138,15 @@ func NewKnCommand(params ...commands.KnParams) *cobra.Command {
// For glog parse error.
flag.CommandLine.Parse([]string{})
return rootCmd
}
// InitializeConfig initializes the kubeconfig used by all commands
func InitializeConfig() {
cobra.OnInitialize(initConfig)
}
// EmptyAndUnknownSubCommands adds a RunE to all commands that are groups to
// deal with errors when called with empty or unknown sub command
func EmptyAndUnknownSubCommands(cmd *cobra.Command) {
@ -106,6 +169,8 @@ func EmptyAndUnknownSubCommands(cmd *cobra.Command) {
}
}
// Private
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if commands.CfgFile != "" {
@ -119,7 +184,7 @@ func initConfig() {
os.Exit(1)
}
// Search config in home directory with name ".kn" (without extension).
// Search config in home directory with name ".kn" (without extension)
viper.AddConfigPath(path.Join(home, ".kn"))
viper.SetConfigName("config")
}
@ -127,7 +192,51 @@ func initConfig() {
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
err := viper.ReadInConfig()
if err == nil {
fmt.Fprintln(os.Stderr, "Using kn config file:", viper.ConfigFileUsed())
}
}
func extractKnPluginFlags(args []string) (string, bool, error) {
pluginsDir := "~/.kn/plugins"
lookupPluginsInPath := false
for _, arg := range args {
if strings.Contains(arg, "--plugins-dir") {
values := strings.Split(arg, "=")
if len(values) < 1 {
return "", false, errors.New("Invalid --plugins-dir flag value")
}
pluginsDir = values[1]
}
if strings.Contains(arg, "--lookup-plugins-in-path") {
values := strings.Split(arg, "=")
if len(values) < 1 {
return "", false, errors.New("Invalid --lookup-plugins-in-path flag value")
}
boolValue, err := strconv.ParseBool(values[1])
if err != nil {
return "", false, err
}
lookupPluginsInPath = boolValue
}
}
return pluginsDir, lookupPluginsInPath, nil
}
func removeKnPluginFlags(args []string) []string {
var remainingArgs []string
for _, arg := range args {
if strings.Contains(arg, "--plugins-dir") ||
strings.Contains(arg, "--lookup-plugins-in-path") {
continue
} else {
remainingArgs = append(remainingArgs, arg)
}
}
return remainingArgs
}

View File

@ -15,52 +15,123 @@
package core
import (
"io/ioutil"
"os"
"strings"
"testing"
"github.com/knative/client/pkg/kn/commands"
"github.com/knative/client/pkg/kn/commands/plugin"
"github.com/spf13/cobra"
"gotest.tools/assert"
)
func TestNewDefaultKnCommand(t *testing.T) {
var rootCmd *cobra.Command
setup := func(t *testing.T) {
rootCmd = NewDefaultKnCommand()
}
t.Run("returns a valid root command", func(t *testing.T) {
setup(t)
checkRootCmd(t, rootCmd)
})
}
func TestNewDefaultKnCommandWithArgs(t *testing.T) {
var (
rootCmd *cobra.Command
pluginHandler plugin.PluginHandler
args []string
)
setup := func(t *testing.T) {
rootCmd = NewDefaultKnCommandWithArgs(NewKnCommand(), pluginHandler, args, os.Stdin, os.Stdout, os.Stderr)
}
t.Run("when pluginHandler is nil", func(t *testing.T) {
args = []string{}
setup(t)
t.Run("returns a valid root command", func(t *testing.T) {
checkRootCmd(t, rootCmd)
})
})
t.Run("when pluginHandler is not nil", func(t *testing.T) {
t.Run("when args empty", func(t *testing.T) {
args = []string{}
setup(t)
t.Run("returns a valid root command", func(t *testing.T) {
checkRootCmd(t, rootCmd)
})
})
t.Run("when args not empty", func(t *testing.T) {
var (
pluginName, pluginPath, tmpPathDir string
err error
)
beforeEach := func(t *testing.T) {
tmpPathDir, err = ioutil.TempDir("", "plugin_list")
assert.Assert(t, err == nil)
pluginName = "fake-plugin-name"
pluginPath = plugin.CreateTestPluginInPath(t, "kn-"+pluginName, plugin.KnTestPluginScript, plugin.FileModeExecutable, tmpPathDir)
}
afterEach := func(t *testing.T) {
err = os.RemoveAll(tmpPathDir)
assert.Assert(t, err == nil)
}
beforeEach(t)
args = []string{pluginPath, pluginName}
setup(t)
defer afterEach(t)
t.Run("tries to handle args[1:] as plugin and return valid root command", func(t *testing.T) {
checkRootCmd(t, rootCmd)
})
})
})
}
func TestNewKnCommand(t *testing.T) {
var rootCmd *cobra.Command
setup := func() {
setup := func(t *testing.T) {
rootCmd = NewKnCommand(commands.KnParams{})
}
setup()
t.Run("returns a valid root command", func(t *testing.T) {
assert.Assert(t, rootCmd != nil)
assert.Equal(t, rootCmd.Name(), "kn")
assert.Equal(t, rootCmd.Short, "Knative client")
assert.Assert(t, strings.Contains(rootCmd.Long, "Manage your Knative building blocks:"))
assert.Assert(t, rootCmd.DisableAutoGenTag)
assert.Assert(t, rootCmd.SilenceUsage)
assert.Assert(t, rootCmd.SilenceErrors)
assert.Assert(t, rootCmd.RunE == nil)
setup(t)
checkRootCmd(t, rootCmd)
})
t.Run("sets the output params", func(t *testing.T) {
setup(t)
assert.Assert(t, rootCmd.OutOrStdout() != nil)
})
t.Run("sets the config and kubeconfig global flags", func(t *testing.T) {
setup(t)
assert.Assert(t, rootCmd.PersistentFlags().Lookup("config") != nil)
assert.Assert(t, rootCmd.PersistentFlags().Lookup("kubeconfig") != nil)
})
t.Run("adds the top level commands: version and completion", func(t *testing.T) {
setup(t)
checkCommand(t, "version", rootCmd)
checkCommand(t, "completion", rootCmd)
})
t.Run("adds the top level group commands", func(t *testing.T) {
setup(t)
checkCommandGroup(t, "service", rootCmd)
checkCommandGroup(t, "revision", rootCmd)
})
@ -69,7 +140,7 @@ func TestNewKnCommand(t *testing.T) {
func TestEmptyAndUnknownSubCommands(t *testing.T) {
var rootCmd, fakeCmd, fakeSubCmd *cobra.Command
setup := func() {
setup := func(t *testing.T) {
rootCmd = NewKnCommand(commands.KnParams{})
fakeCmd = &cobra.Command{
Use: "fake-cmd-name",
@ -84,9 +155,8 @@ func TestEmptyAndUnknownSubCommands(t *testing.T) {
assert.Assert(t, fakeSubCmd.RunE == nil)
}
setup()
t.Run("deals with empty and unknown sub-commands for all group commands", func(t *testing.T) {
setup(t)
EmptyAndUnknownSubCommands(rootCmd)
checkCommand(t, "fake-sub-cmd-name", fakeCmd)
checkCommandGroup(t, "fake-cmd-name", rootCmd)
@ -95,6 +165,23 @@ func TestEmptyAndUnknownSubCommands(t *testing.T) {
// Private
func checkRootCmd(t *testing.T, rootCmd *cobra.Command) {
assert.Assert(t, rootCmd != nil)
assert.Equal(t, rootCmd.Name(), "kn")
assert.Equal(t, rootCmd.Short, "Knative client")
assert.Assert(t, strings.Contains(rootCmd.Long, "Manage your Knative building blocks:"))
assert.Assert(t, rootCmd.DisableAutoGenTag)
assert.Assert(t, rootCmd.SilenceUsage)
assert.Assert(t, rootCmd.SilenceErrors)
assert.Assert(t, rootCmd.Flags().Lookup("plugins-dir") != nil)
assert.Assert(t, rootCmd.Flags().Lookup("lookup-plugins-in-path") != nil)
assert.Assert(t, rootCmd.RunE == nil)
}
func checkCommand(t *testing.T, name string, rootCmd *cobra.Command) {
cmd, _, err := rootCmd.Find([]string{"version"})
assert.Assert(t, err == nil)

2
vendor/modules.txt vendored
View File

@ -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