mirror of https://github.com/knative/client.git
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:
parent
df816e63fe
commit
59b2855d04
|
|
@ -19,12 +19,26 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/knative/client/pkg/kn/core"
|
"github.com/knative/client/pkg/kn/core"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
core.InitializeConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := core.NewKnCommand().Execute()
|
defer cleanup()
|
||||||
|
err = core.NewDefaultKnCommand().Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanup() {
|
||||||
|
if err == nil {
|
||||||
|
viper.WriteConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,16 @@ Manage your Knative building blocks:
|
||||||
### Options
|
### 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
|
-h, --help help for kn
|
||||||
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
--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
|
### SEE ALSO
|
||||||
|
|
||||||
|
* [kn plugin](kn_plugin.md) - Plugin command group
|
||||||
* [kn revision](kn_revision.md) - Revision command group
|
* [kn revision](kn_revision.md) - Revision command group
|
||||||
* [kn route](kn_route.md) - Route command group
|
* [kn route](kn_route.md) - Route command group
|
||||||
* [kn service](kn_service.md) - Service command group
|
* [kn service](kn_service.md) - Service command group
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ kn revision [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ kn revision delete NAME [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ kn revision describe NAME [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ kn revision list [name] [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ kn route [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ kn route describe NAME [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ kn route list NAME [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ kn service [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ kn service create NAME --image IMAGE [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ kn service delete NAME [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ kn service describe NAME [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ kn service list [name] [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ kn service update NAME [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ kn version [flags]
|
||||||
### Options inherited from parent commands
|
### 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)
|
--kubeconfig string kubectl config file (default is $HOME/.kube/config)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"github.com/knative/client/pkg/serving/v1alpha1"
|
"github.com/knative/client/pkg/serving/v1alpha1"
|
||||||
"github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1/fake"
|
"github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1/fake"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
client_testing "k8s.io/client-go/testing"
|
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(&CfgFile, "config", "", "config file (default is $HOME/.kn.yaml)")
|
||||||
rootCmd.PersistentFlags().StringVar(¶ms.KubeCfgPath, "kubeconfig", "", "kubectl config file (default is $HOME/.kube/config)")
|
rootCmd.PersistentFlags().StringVar(¶ms.KubeCfgPath, "kubeconfig", "", "kubectl config file (default is $HOME/.kube/config)")
|
||||||
|
|
||||||
|
rootCmd.Flags().StringVar(&Cfg.PluginsDir, "plugins-dir", "~/.kn/plugins", "kn plugins directory")
|
||||||
|
rootCmd.Flags().BoolVar(&Cfg.LookupPluginsInPath, "lookup-plugins-in-path", false, "look for kn plugins in $PATH")
|
||||||
|
|
||||||
|
viper.BindPFlag("pluginsDir", rootCmd.Flags().Lookup("plugins-dir"))
|
||||||
|
viper.BindPFlag("lookupPluginsInPath", rootCmd.Flags().Lookup("lookup-plugins-in-path"))
|
||||||
|
|
||||||
|
viper.SetDefault("pluginsDir", "~/.kn/plugins")
|
||||||
|
viper.SetDefault("lookupPluginsInPath", false)
|
||||||
|
|
||||||
rootCmd.AddCommand(subCommand)
|
rootCmd.AddCommand(subCommand)
|
||||||
|
|
||||||
// For glog parse error.
|
// For glog parse error.
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
serving_kn_v1alpha1 "github.com/knative/client/pkg/serving/v1alpha1"
|
serving_kn_v1alpha1 "github.com/knative/client/pkg/serving/v1alpha1"
|
||||||
|
|
||||||
serving_v1alpha1_client "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1"
|
serving_v1alpha1_client "github.com/knative/serving/pkg/client/clientset/versioned/typed/serving/v1alpha1"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
)
|
)
|
||||||
|
|
@ -30,6 +29,15 @@ import (
|
||||||
// CfgFile is Kn's config file is the path for the Kubernetes config
|
// CfgFile is Kn's config file is the path for the Kubernetes config
|
||||||
var CfgFile string
|
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.
|
// Parameters for creating commands. Useful for inserting mocks for testing.
|
||||||
type KnParams struct {
|
type KnParams struct {
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
|
|
|
||||||
|
|
@ -18,24 +18,74 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/knative/client/pkg/kn/commands"
|
"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/revision"
|
||||||
"github.com/knative/client/pkg/kn/commands/route"
|
"github.com/knative/client/pkg/kn/commands/route"
|
||||||
"github.com/knative/client/pkg/kn/commands/service"
|
"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/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cfgFile string
|
// NewDefaultKnCommand creates the default `kn` command with a default plugin handler
|
||||||
var kubeCfgFile string
|
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 {
|
func NewKnCommand(params ...commands.KnParams) *cobra.Command {
|
||||||
var p *commands.KnParams
|
var p *commands.KnParams
|
||||||
if len(params) == 0 {
|
if len(params) == 0 {
|
||||||
|
|
@ -67,11 +117,18 @@ func NewKnCommand(params ...commands.KnParams) *cobra.Command {
|
||||||
if p.Output != nil {
|
if p.Output != nil {
|
||||||
rootCmd.SetOutput(p.Output)
|
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)")
|
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(service.NewServiceCommand(p))
|
||||||
rootCmd.AddCommand(revision.NewRevisionCommand(p))
|
rootCmd.AddCommand(revision.NewRevisionCommand(p))
|
||||||
|
rootCmd.AddCommand(plugin.NewPluginCommand(p))
|
||||||
rootCmd.AddCommand(route.NewRouteCommand(p))
|
rootCmd.AddCommand(route.NewRouteCommand(p))
|
||||||
rootCmd.AddCommand(commands.NewCompletionCommand(p))
|
rootCmd.AddCommand(commands.NewCompletionCommand(p))
|
||||||
rootCmd.AddCommand(commands.NewVersionCommand(p))
|
rootCmd.AddCommand(commands.NewVersionCommand(p))
|
||||||
|
|
@ -81,9 +138,15 @@ func NewKnCommand(params ...commands.KnParams) *cobra.Command {
|
||||||
|
|
||||||
// For glog parse error.
|
// For glog parse error.
|
||||||
flag.CommandLine.Parse([]string{})
|
flag.CommandLine.Parse([]string{})
|
||||||
|
|
||||||
return rootCmd
|
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
|
// EmptyAndUnknownSubCommands adds a RunE to all commands that are groups to
|
||||||
// deal with errors when called with empty or unknown sub command
|
// deal with errors when called with empty or unknown sub command
|
||||||
func EmptyAndUnknownSubCommands(cmd *cobra.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.
|
// initConfig reads in config file and ENV variables if set.
|
||||||
func initConfig() {
|
func initConfig() {
|
||||||
if commands.CfgFile != "" {
|
if commands.CfgFile != "" {
|
||||||
|
|
@ -119,7 +184,7 @@ func initConfig() {
|
||||||
os.Exit(1)
|
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.AddConfigPath(path.Join(home, ".kn"))
|
||||||
viper.SetConfigName("config")
|
viper.SetConfigName("config")
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +192,51 @@ func initConfig() {
|
||||||
viper.AutomaticEnv() // read in environment variables that match
|
viper.AutomaticEnv() // read in environment variables that match
|
||||||
|
|
||||||
// If a config file is found, read it in.
|
// If a config file is found, read it in.
|
||||||
if err := viper.ReadInConfig(); err == nil {
|
err := viper.ReadInConfig()
|
||||||
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,52 +15,123 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/knative/client/pkg/kn/commands"
|
"github.com/knative/client/pkg/kn/commands"
|
||||||
|
"github.com/knative/client/pkg/kn/commands/plugin"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gotest.tools/assert"
|
"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) {
|
func TestNewKnCommand(t *testing.T) {
|
||||||
var rootCmd *cobra.Command
|
var rootCmd *cobra.Command
|
||||||
|
|
||||||
setup := func() {
|
setup := func(t *testing.T) {
|
||||||
rootCmd = NewKnCommand(commands.KnParams{})
|
rootCmd = NewKnCommand(commands.KnParams{})
|
||||||
}
|
}
|
||||||
|
|
||||||
setup()
|
|
||||||
|
|
||||||
t.Run("returns a valid root command", func(t *testing.T) {
|
t.Run("returns a valid root command", func(t *testing.T) {
|
||||||
assert.Assert(t, rootCmd != nil)
|
setup(t)
|
||||||
|
checkRootCmd(t, rootCmd)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("sets the output params", func(t *testing.T) {
|
t.Run("sets the output params", func(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
assert.Assert(t, rootCmd.OutOrStdout() != nil)
|
assert.Assert(t, rootCmd.OutOrStdout() != nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("sets the config and kubeconfig global flags", func(t *testing.T) {
|
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("config") != nil)
|
||||||
assert.Assert(t, rootCmd.PersistentFlags().Lookup("kubeconfig") != nil)
|
assert.Assert(t, rootCmd.PersistentFlags().Lookup("kubeconfig") != nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("adds the top level commands: version and completion", func(t *testing.T) {
|
t.Run("adds the top level commands: version and completion", func(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
checkCommand(t, "version", rootCmd)
|
checkCommand(t, "version", rootCmd)
|
||||||
checkCommand(t, "completion", rootCmd)
|
checkCommand(t, "completion", rootCmd)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("adds the top level group commands", func(t *testing.T) {
|
t.Run("adds the top level group commands", func(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
checkCommandGroup(t, "service", rootCmd)
|
checkCommandGroup(t, "service", rootCmd)
|
||||||
checkCommandGroup(t, "revision", rootCmd)
|
checkCommandGroup(t, "revision", rootCmd)
|
||||||
})
|
})
|
||||||
|
|
@ -69,7 +140,7 @@ func TestNewKnCommand(t *testing.T) {
|
||||||
func TestEmptyAndUnknownSubCommands(t *testing.T) {
|
func TestEmptyAndUnknownSubCommands(t *testing.T) {
|
||||||
var rootCmd, fakeCmd, fakeSubCmd *cobra.Command
|
var rootCmd, fakeCmd, fakeSubCmd *cobra.Command
|
||||||
|
|
||||||
setup := func() {
|
setup := func(t *testing.T) {
|
||||||
rootCmd = NewKnCommand(commands.KnParams{})
|
rootCmd = NewKnCommand(commands.KnParams{})
|
||||||
fakeCmd = &cobra.Command{
|
fakeCmd = &cobra.Command{
|
||||||
Use: "fake-cmd-name",
|
Use: "fake-cmd-name",
|
||||||
|
|
@ -84,9 +155,8 @@ func TestEmptyAndUnknownSubCommands(t *testing.T) {
|
||||||
assert.Assert(t, fakeSubCmd.RunE == nil)
|
assert.Assert(t, fakeSubCmd.RunE == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
setup()
|
|
||||||
|
|
||||||
t.Run("deals with empty and unknown sub-commands for all group commands", func(t *testing.T) {
|
t.Run("deals with empty and unknown sub-commands for all group commands", func(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
EmptyAndUnknownSubCommands(rootCmd)
|
EmptyAndUnknownSubCommands(rootCmd)
|
||||||
checkCommand(t, "fake-sub-cmd-name", fakeCmd)
|
checkCommand(t, "fake-sub-cmd-name", fakeCmd)
|
||||||
checkCommandGroup(t, "fake-cmd-name", rootCmd)
|
checkCommandGroup(t, "fake-cmd-name", rootCmd)
|
||||||
|
|
@ -95,6 +165,23 @@ func TestEmptyAndUnknownSubCommands(t *testing.T) {
|
||||||
|
|
||||||
// Private
|
// 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) {
|
func checkCommand(t *testing.T, name string, rootCmd *cobra.Command) {
|
||||||
cmd, _, err := rootCmd.Find([]string{"version"})
|
cmd, _, err := rootCmd.Find([]string{"version"})
|
||||||
assert.Assert(t, err == nil)
|
assert.Assert(t, err == nil)
|
||||||
|
|
|
||||||
|
|
@ -139,9 +139,9 @@ golang.org/x/oauth2/jwt
|
||||||
golang.org/x/sys/unix
|
golang.org/x/sys/unix
|
||||||
golang.org/x/sys/windows
|
golang.org/x/sys/windows
|
||||||
# golang.org/x/text v0.3.0
|
# golang.org/x/text v0.3.0
|
||||||
golang.org/x/text/encoding/unicode
|
|
||||||
golang.org/x/text/transform
|
golang.org/x/text/transform
|
||||||
golang.org/x/text/unicode/norm
|
golang.org/x/text/unicode/norm
|
||||||
|
golang.org/x/text/encoding/unicode
|
||||||
golang.org/x/text/encoding
|
golang.org/x/text/encoding
|
||||||
golang.org/x/text/encoding/internal
|
golang.org/x/text/encoding/internal
|
||||||
golang.org/x/text/encoding/internal/identifier
|
golang.org/x/text/encoding/internal/identifier
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue