Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor main flow and introduce explicit plugin and config handling #877

Merged
merged 8 commits into from
Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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