Skip to content

Commit

Permalink
Refactor main flow and introduce explicit plugin and config handling (#…
Browse files Browse the repository at this point in the history
…877)

* Refactor main flow, plugin and configuration handling

* The plugin handling has been moved out of the `KnDefaultCommand` constructor where it was executed as a side-effect. The original code from `kubectl` suffers from the same issue that plugin handling is not a top-level concern but was very likely introduced as an after-thought. Instead, the plugin handling is done now by a `PluginManager` which is explicitly called in `main()`.
* Configuration and bootstrap option handling is centralized in the package `option`. After the bootstrap happened, the content of the configuration file, as well as any other global configuration, can be obtained from methods on  `config.GlobalConfig`. Also, all flag handling is delegated to cobra so that no own parsing is needed.
* Many of the logic in `pkg/kn/commands/plugin` for plugin management has been moved up to `pkg/kn/plugin` as this code is not only relevant for `plugin list` but also for the bootstrap process.

* fix: invalid subcommands will lead to a proper error message

* Update pkg/kn/config/types.go

Co-authored-by: Navid Shaikh <[email protected]>

* Update pkg/kn/plugin/manager.go

Co-authored-by: Navid Shaikh <[email protected]>

* Update hack/generate-docs.go

Co-authored-by: Navid Shaikh <[email protected]>

* Update hack/generate-docs.go

Co-authored-by: Navid Shaikh <[email protected]>

* chore: Add missing links

* chore: recert to shas in links in developer guide for now.

Co-authored-by: Navid Shaikh <[email protected]>
  • Loading branch information
rhuss and navidshaikh authored Jun 15, 2020
1 parent 3f146b1 commit c742645
Show file tree
Hide file tree
Showing 84 changed files with 2,821 additions and 2,652 deletions.
157 changes: 142 additions & 15 deletions cmd/kn/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,164 @@ import (
"fmt"
"math/rand"
"os"
"strings"
"time"

"github.com/spf13/viper"
"knative.dev/client/pkg/kn/core"
"github.com/pkg/errors"
"github.com/spf13/cobra"

"knative.dev/client/pkg/kn/config"
"knative.dev/client/pkg/kn/plugin"
"knative.dev/client/pkg/kn/root"
)

func init() {
core.InitializeConfig()
rand.Seed(time.Now().UnixNano())
}

var err error

func main() {
defer cleanup()
rand.Seed(time.Now().UnixNano())
kn, err := core.NewDefaultKnCommand()
err := run(os.Args[1:])
if err != nil {
fmt.Fprintln(os.Stderr, err)
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
// This is the only point from where to exit when an error occurs
os.Exit(1)
}
}

// 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
// and bind to viper. After that you can access all configuration and
// global options via methods on config.GlobalConfig
err := config.BootstrapConfig()
if err != nil {
return err
}

// Strip of all flags to get the non-flag commands only
commands, err := stripFlags(args)
if err != nil {
return err
}

if err := kn.Execute(); err != nil {
if err.Error() != "subcommand is required" {
fmt.Fprintln(os.Stderr, err)
// Find plugin with the commands arguments
pluginManager := plugin.NewManager(config.GlobalConfig.PluginsDir(), config.GlobalConfig.LookupPluginsInPath())
plugin, err := pluginManager.FindPlugin(commands)
if err != nil {
return err
}

// Create kn root command and all sub-commands
rootCmd, err := root.NewRootCommand()
if err != nil {
return err
}

if plugin != nil {
// Validate & Execute plugin
err = validatePlugin(rootCmd, plugin)
if err != nil {
return err
}

return plugin.Execute(argsWithoutCommands(args, plugin.CommandParts()))
} else {
// Validate args for root command
err = validateRootCommand(rootCmd)
if err != nil {
return err
}
// Execute kn root command, args are taken from os.Args directly
return rootCmd.Execute()
}
}

// Get only the args provided but no options. The extraction
// process is a bit tricky as Cobra doesn't provide such
// functionality out of the box
func stripFlags(args []string) ([]string, error) {
// Store all command
commandsFound := &[]string{}

// Use a canary command that allows all options and only extracts
// commands. Doesn't work with arbitrary boolean flags but is good enough
// for us here
extractCommand := cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
for _, arg := range args {
*commandsFound = append(*commandsFound, arg)
}
},
}

// Filter out --help and -h options to avoid special treatment which we don't
// need here
extractCommand.SetArgs(filterHelpOptions(args))

// Adding all global flags here
config.AddBootstrapFlags(extractCommand.Flags())

// Allow all options
extractCommand.FParseErrWhitelist = cobra.FParseErrWhitelist{UnknownFlags: true}

// Execute to get to the command args
err := extractCommand.Execute()
if err != nil {
return nil, err
}
return *commandsFound, nil
}

// Strip all plugin commands before calling out to the plugin
func argsWithoutCommands(cmdArgs []string, pluginCommandsParts []string) []string {
var ret []string
for _, arg := range cmdArgs {
if len(pluginCommandsParts) > 0 && pluginCommandsParts[0] == arg {
pluginCommandsParts = pluginCommandsParts[1:]
continue
}
ret = append(ret, arg)
}
return ret
}

// Remove all help options
func filterHelpOptions(args []string) []string {
var ret []string
for _, arg := range args {
if arg != "-h" && arg != "--help" {
ret = append(ret, arg)
}
os.Exit(1)
}
return ret
}

func cleanup() {
// Check if the plugin collides with any command specified in the root command
func validatePlugin(root *cobra.Command, plugin plugin.Plugin) error {
// Check if a given plugin can be identified as a command
cmd, args, err := root.Find(plugin.CommandParts())

if err == nil {
viper.WriteConfig()
if !cmd.HasSubCommands() || // a leaf command can't be overridden
cmd.HasSubCommands() && len(args) == 0 { // a group can't be overridden either
return errors.Errorf("plugin %s is overriding built-in command '%s' which is not allowed", plugin.Path(), strings.Join(plugin.CommandParts(), " "))
}
}
return nil
}

// Check whether an unknown sub-command is addressed and return an error if this is the case
// Needs to be called after the plugin has been extracted (as a plugin name can also lead to
// an unknown sub command error otherwise)
func validateRootCommand(cmd *cobra.Command) error {
foundCmd, innerArgs, err := cmd.Find(os.Args[1:])
if err == nil && foundCmd.HasSubCommands() && len(innerArgs) > 0 {
argsWithoutFlags, err := stripFlags(innerArgs)
if len(argsWithoutFlags) > 0 || err != nil {
return errors.Errorf("unknown sub-command '%s' for '%s'. Available sub-commands: %s", innerArgs[0], foundCmd.Name(), strings.Join(root.ExtractSubCommandNames(foundCmd.Commands()), ", "))
}
// If no args where given (only flags), then fall through to execute the command itself, which leads to
// a more appropriate error message
}
return nil
}
Loading

0 comments on commit c742645

Please sign in to comment.