Skip to content

Commit

Permalink
feat: Add plugin listing to "kn --help" (#929)
Browse files Browse the repository at this point in the history
* feat: Add plugin listing to "kn --help"

This works on all levels. Test needs to be expanded still.

Fixes #266

* chore: Fix test

* Add test and ported #910 over.

* changelog update

* fix test

* Fix test

* fix integration tests

* fix test

* chore: Add some explanatory comments

* fix test
  • Loading branch information
rhuss authored Jul 14, 2020
1 parent 2e1a863 commit 490fe57
Show file tree
Hide file tree
Showing 15 changed files with 281 additions and 49 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
| New sink prefix "ksvc" and drop support for "svc", "service" prefixes for knative service
| https://github.com/knative/client/pull/896[#896]

| 🎁
| Add available plugins to help messages
| https://github.com/knative/client/pull/929[#929]

| 🎁
| Add "url" output format to return service url in service describe
| https://github.com/knative/client/pull/916[#916]
Expand Down
25 changes: 22 additions & 3 deletions cmd/kn/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ func main() {
}
}

// runError is used when during the execution of a command/plugin an error occurs and
// so no extra usage message should be shown.
type runError struct {
err error
}

// Error implements the error() interface
func (e *runError) Error() string {
return e.err.Error()
}

// Run the main program. Args are the args as given on the command line (excluding the program name itself)
func run(args []string) error {
// Parse config & plugin flags early to read in configuration file
Expand All @@ -67,7 +78,7 @@ func run(args []string) error {
}

// Create kn root command and all sub-commands
rootCmd, err := root.NewRootCommand()
rootCmd, err := root.NewRootCommand(pluginManager.HelpTemplateFuncs())
if err != nil {
return err
}
Expand All @@ -79,7 +90,11 @@ func run(args []string) error {
return err
}

return plugin.Execute(argsWithoutCommands(args, plugin.CommandParts()))
err := plugin.Execute(argsWithoutCommands(args, plugin.CommandParts()))
if err != nil {
return &runError{err: err}
}
return nil
} else {
// Validate args for root command
err = validateRootCommand(rootCmd)
Expand Down Expand Up @@ -190,7 +205,11 @@ func validateRootCommand(cmd *cobra.Command) error {
// printError prints out any given error
func printError(err error) {
fmt.Fprintf(os.Stderr, "Error: %s\n", cleanupErrorMessage(err.Error()))
fmt.Fprintf(os.Stderr, "Run '%s --help' for usage\n", extractCommandPathFromErrorMessage(err.Error(), os.Args[0]))
var runError *runError
if !errors.As(err, &runError) {
// Print help hint only if its not a runError occurred when executing a command
fmt.Fprintf(os.Stderr, "Run '%s --help' for usage\n", extractCommandPathFromErrorMessage(err.Error(), os.Args[0]))
}
}

// extractCommandPathFromErrorMessage tries to extract the command name from an error message
Expand Down
25 changes: 24 additions & 1 deletion cmd/kn/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func TestUnknownCommands(t *testing.T) {
}
for _, d := range data {
args := append([]string{"kn"}, d.givenCmdArgs...)
rootCmd, err := root.NewRootCommand()
rootCmd, err := root.NewRootCommand(nil)
os.Args = args
assert.NilError(t, err)
err = validateRootCommand(rootCmd)
Expand Down Expand Up @@ -253,6 +253,29 @@ func TestRunWithError(t *testing.T) {
}
}

func TestRunWithPluginError(t *testing.T) {
data := []struct {
given string
expected string
}{
{
"exit status 1",
"Error: exit status 1",
},
}
for _, d := range data {
capture := test.CaptureOutput(t)
// displayHelp argument is false for plugin error
printError(&runError{errors.New(d.given)})
stdOut, errOut := capture.Close()

assert.Equal(t, stdOut, "")
assert.Assert(t, strings.Contains(errOut, d.expected))
// check that --help message isn't displayed
assert.Assert(t, util.ContainsNone(errOut, "Run", "--help", "usage"))
}
}

// Smoke test
func TestRun(t *testing.T) {
oldArgs := os.Args
Expand Down
2 changes: 1 addition & 1 deletion hack/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ go_test() {

echo "🧪 ${X}Test"
set +e
go test -v ./pkg/... >$test_output 2>&1
go test -v ./cmd/... ./pkg/... >$test_output 2>&1
local err=$?
if [ $err -ne 0 ]; then
echo "🔥 ${red}Failure${reset}"
Expand Down
2 changes: 1 addition & 1 deletion hack/generate-docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
)

func main() {
rootCmd, err := root.NewRootCommand()
rootCmd, err := root.NewRootCommand(nil)
if err != nil {
log.Panicf("can not create root command: %v", err)
}
Expand Down
85 changes: 83 additions & 2 deletions pkg/kn/plugin/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import (
"runtime"
"sort"
"strings"
"text/template"

homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

// Interface describing a plugin
Expand Down Expand Up @@ -108,6 +110,12 @@ func (manager *Manager) FindPlugin(parts []string) (Plugin, error) {

// ListPlugins lists all plugins that can be found in the plugin directory or in the path (if configured)
func (manager *Manager) ListPlugins() (PluginList, error) {
return manager.ListPluginsForCommandGroup(nil)
}

// ListPluginsForCommandGroup lists all plugins that can be found in the plugin directory or in the path (if configured),
// and which fits to a command group
func (manager *Manager) ListPluginsForCommandGroup(commandGroupParts []string) (PluginList, error) {
var plugins []Plugin

dirs, err := manager.pluginLookupDirectories()
Expand Down Expand Up @@ -135,6 +143,11 @@ func (manager *Manager) ListPlugins() (PluginList, error) {
continue
}

// Check if plugin matches a command group
if !isPartOfCommandGroup(commandGroupParts, f.Name()) {
continue
}

// Ignore all plugins that are shadowed
if _, ok := hasSeen[name]; !ok {
plugins = append(plugins, &plugin{
Expand All @@ -146,11 +159,35 @@ func (manager *Manager) ListPlugins() (PluginList, error) {
}
}
}

// Sort according to name
sort.Sort(PluginList(plugins))
return plugins, nil
}

func isPartOfCommandGroup(commandGroupParts []string, name string) bool {
if commandGroupParts == nil {
return true
}

commandParts := extractPluginCommandFromFileName(name)

// commandParts must be one more element then the parts of the command group
// it belongs to. E.g. for the command "service", "log" (2 elements) the containing
// group only has one element ("service"). This condition is here for
// shortcut and ensure that we don't run in an out-of-bound array error
// in the loop below.
if len(commandParts) != len(commandGroupParts)+1 {
return false
}
for i := range commandGroupParts {
if commandParts[i] != commandGroupParts[i] {
return false
}
}
return true
}

// PluginsDir returns the configured directory holding plugins
func (manager *Manager) PluginsDir() string {
return manager.pluginsDir
Expand All @@ -176,8 +213,9 @@ func (plugin *plugin) Execute(args []string) error {
// Return a description of the plugin (if support by the plugin binary)
func (plugin *plugin) Description() (string, error) {
// TODO: Call out to the plugin to find a description.
// For now just use the plugin name
return strings.Join(plugin.commandParts, "-"), nil
// For now just use the path to the plugin
return plugin.path, nil
// return strings.Join(plugin.commandParts, "-"), nil
}

// The the command path leading to this plugin.
Expand Down Expand Up @@ -212,6 +250,49 @@ func (manager *Manager) pluginLookupDirectories() ([]string, error) {
return dirs, nil
}

// HelpTemplateFuncs returns a function map which can be used in templates for resolving
// plugin related help messages
func (manager *Manager) HelpTemplateFuncs() *template.FuncMap {
ret := template.FuncMap{
"listPlugins": manager.listPluginsHelpMessage(),
}

return &ret
}

// listPluginsHelpMessage returns a function which returns all plugins that are directly below the given
// command as a properly formatted string
func (manager *Manager) listPluginsHelpMessage() func(cmd *cobra.Command) string {
return func(cmd *cobra.Command) string {
if !cmd.HasSubCommands() {
return ""
}
list, err := manager.ListPluginsForCommandGroup(extractCommandGroup(cmd, []string{}))
if err != nil || len(list) == 0 {
// We don't show plugins if there is an error
return ""
}
var plugins []string
for _, pl := range list {
t := fmt.Sprintf(" %%-%ds %%s", cmd.NamePadding())
desc, _ := pl.Description()
command := (pl.CommandParts())[len(pl.CommandParts())-1]
help := fmt.Sprintf(t, command, desc)
plugins = append(plugins, help)
}
return strings.Join(plugins, "\n")
}
}

// extractCommandGroup constructs the command path as array of strings
func extractCommandGroup(cmd *cobra.Command, parts []string) []string {
if cmd.HasParent() {
parts = extractCommandGroup(cmd.Parent(), parts)
parts = append(parts, cmd.Name())
}
return parts
}

// uniquePathsList deduplicates a given slice of strings without
// sorting or otherwise altering its order in any way.
func uniquePathsList(paths []string) []string {
Expand Down
52 changes: 52 additions & 0 deletions pkg/kn/plugin/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import (
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"
"testing"

"github.com/spf13/cobra"
"gotest.tools/assert"
)

Expand Down Expand Up @@ -110,6 +112,56 @@ func TestPluginExecute(t *testing.T) {
assert.Equal(t, out, "OK arg1 arg2\n")
}

func TestPluginListForCommandGroup(t *testing.T) {
ctx := setup(t)
defer cleanup(t, ctx)
createTestPlugin(t, "kn-service-log_2", ctx)

pluginList, err := ctx.pluginManager.ListPluginsForCommandGroup([]string{"service"})
assert.NilError(t, err)
assert.Assert(t, pluginList.Len() == 1)
assert.Equal(t, pluginList[0].Name(), "kn-service-log_2")
pluginList, err = ctx.pluginManager.ListPluginsForCommandGroup([]string{})
assert.NilError(t, err)
assert.Assert(t, pluginList.Len() == 0)
}

func TestPluginHelpMessage(t *testing.T) {
ctx := setup(t)
defer cleanup(t, ctx)
createTestPlugin(t, "kn-service-log_2", ctx)
createTestPlugin(t, "kn-admin", ctx)

funcs := *ctx.pluginManager.HelpTemplateFuncs()
f := funcs["listPlugins"]
assert.Assert(t, f != nil)
listPluginsFunc := ctx.pluginManager.listPluginsHelpMessage()

root := &cobra.Command{
Use: "kn",
}
serviceCmd := &cobra.Command{
Use: "service",
}
serviceCreateCmd := &cobra.Command{
Use: "create",
}
serviceCmd.AddCommand(serviceCreateCmd)
root.AddCommand(serviceCmd)

helpRoot := listPluginsFunc(root)
re := regexp.MustCompile("^\\s*admin\\s.*admin")
assert.Assert(t, re.MatchString(helpRoot))

helpService := listPluginsFunc(serviceCmd)
println(helpService)
re = regexp.MustCompile("^\\s*log-2\\s.*kn-service-log_2")
assert.Assert(t, re.MatchString(helpService))

helpServiceCreate := listPluginsFunc(serviceCreateCmd)
assert.Assert(t, len(helpServiceCreate) == 0)
}

func TestPluginList(t *testing.T) {
ctx := setup(t)
defer cleanup(t, ctx)
Expand Down
5 changes: 3 additions & 2 deletions pkg/kn/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"flag"
"fmt"
"strings"
"text/template"

"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand All @@ -41,7 +42,7 @@ import (
)

// NewRootCommand creates the default `kn` command with a default plugin handler
func NewRootCommand() (*cobra.Command, error) {
func NewRootCommand(helpFuncs *template.FuncMap) (*cobra.Command, error) {
p := &commands.KnParams{}
p.Initialize()

Expand Down Expand Up @@ -107,7 +108,7 @@ func NewRootCommand() (*cobra.Command, error) {
groups.AddTo(rootCmd)

// Initialize default `help` cmd early to prevent unknown command errors
groups.SetRootUsage(rootCmd)
groups.SetRootUsage(rootCmd, helpFuncs)

// Add the "options" commands for showing all global options
rootCmd.AddCommand(options.NewOptionsCommand())
Expand Down
6 changes: 3 additions & 3 deletions pkg/kn/root/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
)

func TestNewRootCommand(t *testing.T) {
rootCmd, err := NewRootCommand()
rootCmd, err := NewRootCommand(nil)
assert.NilError(t, err)
assert.Assert(t, rootCmd != nil)

Expand All @@ -51,13 +51,13 @@ func TestNewRootCommand(t *testing.T) {
}

func TestSubCommands(t *testing.T) {
rootCmd, err := NewRootCommand()
rootCmd, err := NewRootCommand(nil)
assert.NilError(t, err)
checkLeafCommand(t, "version", rootCmd)
}

func TestCommandGroup(t *testing.T) {
rootCmd, err := NewRootCommand()
rootCmd, err := NewRootCommand(nil)
assert.NilError(t, err)
commandGroups := []string{
"service", "revision", "plugin", "source", "source apiserver",
Expand Down
9 changes: 4 additions & 5 deletions pkg/templates/command_groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package templates

import (
"text/template"

"github.com/spf13/cobra"
)

Expand All @@ -40,11 +42,8 @@ func (g CommandGroups) AddTo(cmd *cobra.Command) {
}

// SetRootUsage sets our own help and usage function messages to the root command
func (g CommandGroups) SetRootUsage(rootCmd *cobra.Command) {
engine := &templateEngine{
RootCmd: rootCmd,
CommandGroups: g,
}
func (g CommandGroups) SetRootUsage(rootCmd *cobra.Command, extraTemplateFunctions *template.FuncMap) {
engine := newTemplateEngine(rootCmd, g, extraTemplateFunctions)
setHelpFlagsToSubCommands(rootCmd)
rootCmd.SetUsageFunc(engine.usageFunc())
rootCmd.SetHelpFunc(engine.helpFunc())
Expand Down
Loading

0 comments on commit 490fe57

Please sign in to comment.