From 8e5115ce73368a60a7f6f694ff39ad1a9c64a870 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Mon, 28 Dec 2015 15:55:11 -0800 Subject: [PATCH] cmd: use spf13/cobra for dexctl cli logic --- cmd/dexctl/command_client.go | 16 +++-- cmd/dexctl/command_config.go | 31 ++++---- cmd/dexctl/command_help.go | 135 ----------------------------------- cmd/dexctl/main.go | 101 +++++++++++--------------- functional/dexctl_test.go | 72 +++++++++++++++++++ 5 files changed, 138 insertions(+), 217 deletions(-) delete mode 100644 cmd/dexctl/command_help.go create mode 100644 functional/dexctl_test.go diff --git a/cmd/dexctl/command_client.go b/cmd/dexctl/command_client.go index 25834a14..4460b9ad 100644 --- a/cmd/dexctl/command_client.go +++ b/cmd/dexctl/command_client.go @@ -4,22 +4,24 @@ import ( "net/url" "github.com/coreos/go-oidc/oidc" + "github.com/spf13/cobra" ) var ( - cmdNewClient = &command{ - Name: "new-client", - Summary: "Create a new client with the provided redirect URL(s)", - Usage: "...", - Run: runNewClient, + cmdNewClient = &cobra.Command{ + Use: "new-client", + Short: "Create a new client with one or more redirect URLs.", + Long: "Create a new client with one or more redirect URLs,", + Example: ` dexctl new-client --db-url=${DB_URL} 'https://example.com/callback'`, + Run: wrapRun(runNewClient), } ) func init() { - commands = append(commands, cmdNewClient) + rootCmd.AddCommand(cmdNewClient) } -func runNewClient(args []string) int { +func runNewClient(cmd *cobra.Command, args []string) int { if len(args) < 1 { stderr("Provide at least one redirect URL.") return 2 diff --git a/cmd/dexctl/command_config.go b/cmd/dexctl/command_config.go index 4a5030b8..5fda0927 100644 --- a/cmd/dexctl/command_config.go +++ b/cmd/dexctl/command_config.go @@ -4,30 +4,33 @@ import ( "fmt" "github.com/coreos/dex/connector" + "github.com/spf13/cobra" ) var ( - cmdGetConnectorConfigs = &command{ - Name: "get-connector-configs", - Summary: "Enumerate current IdP connector configs.", - Usage: "", - Run: runGetConnectorConfigs, + cmdGetConnectorConfigs = &cobra.Command{ + Use: "get-connector-configs", + Short: "Enumerate current IdP connector configs.", + Long: "Enumerate current IdP connector configs.", + Example: ` dexctl get-connector-configs --db-url=${DB_URL}`, + Run: wrapRun(runGetConnectorConfigs), } - cmdSetConnectorConfigs = &command{ - Name: "set-connector-configs", - Summary: "Overwrite the current IdP connector configs with those from a local file.", - Usage: "", - Run: runSetConnectorConfigs, + cmdSetConnectorConfigs = &cobra.Command{ + Use: "set-connector-configs", + Short: "Overwrite the current IdP connector configs with those from a local file.", + Long: "Overwrite the current IdP connector configs with those from a local file.", + Example: ` dexctl set-connector-configs --db-url=${DB_URL} ./static/conn_conf.json`, + Run: wrapRun(runSetConnectorConfigs), } ) func init() { - commands = append(commands, cmdSetConnectorConfigs) - commands = append(commands, cmdGetConnectorConfigs) + rootCmd.AddCommand(cmdGetConnectorConfigs) + rootCmd.AddCommand(cmdSetConnectorConfigs) } -func runSetConnectorConfigs(args []string) int { +func runSetConnectorConfigs(cmd *cobra.Command, args []string) int { if len(args) != 1 { stderr("Provide a single argument.") return 2 @@ -55,7 +58,7 @@ func runSetConnectorConfigs(args []string) int { return 0 } -func runGetConnectorConfigs(args []string) int { +func runGetConnectorConfigs(cmd *cobra.Command, args []string) int { if len(args) != 0 { stderr("Provide zero arguments.") return 2 diff --git a/cmd/dexctl/command_help.go b/cmd/dexctl/command_help.go deleted file mode 100644 index 9a77dffe..00000000 --- a/cmd/dexctl/command_help.go +++ /dev/null @@ -1,135 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - "strings" - "text/tabwriter" - "text/template" -) - -var ( - cmdHelp = &command{ - Name: "help", - Summary: "Show a list of commands or help for one command", - Usage: "[COMMAND]", - Run: runHelp, - } - - globalUsageTemplate *template.Template - commandUsageTemplate *template.Template - templFuncs = template.FuncMap{ - "descToLines": func(s string) []string { - // trim leading/trailing whitespace and split into slice of lines - return strings.Split(strings.Trim(s, "\n\t "), "\n") - }, - "printOption": func(name, defvalue, usage string) string { - prefix := "--" - if len(name) == 1 { - prefix = "-" - } - return fmt.Sprintf("\n\t%s%s=%s\t%s", prefix, name, defvalue, usage) - }, - } - - tabOut *tabwriter.Writer -) - -func init() { - tabOut = tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) - - commands = append(commands, cmdHelp) - - globalUsageTemplate = template.Must(template.New("global_usage").Funcs(templFuncs).Parse(` -NAME: -{{printf "\t%s - %s" .Executable .Description}} - -USAGE: -{{printf "\t%s" .Executable}} [global options] [command options] [arguments...] - -COMMANDS:{{range .Commands}} -{{printf "\t%s\t%s" .Name .Summary}}{{end}} - -GLOBAL OPTIONS:{{range .Flags}}{{printOption .Name .DefValue .Usage}}{{end}} - -Global options can also be configured via upper-case environment variables prefixed with "DEXCTL_" -For example, "some-flag" => "DEXCTL_SOME_FLAG" - -Run "{{.Executable}} help " for more details on a specific command. -`[1:])) - commandUsageTemplate = template.Must(template.New("command_usage").Funcs(templFuncs).Parse(` -NAME: -{{printf "\t%s - %s" .Cmd.Name .Cmd.Summary}} - -USAGE: -{{printf "\t%s %s %s" .Executable .Cmd.Name .Cmd.Usage}} - -DESCRIPTION: -{{range $line := descToLines .Cmd.Description}}{{printf "\t%s" $line}} -{{end}} -{{if .CmdFlags}}OPTIONS:{{range .CmdFlags}} -{{printOption .Name .DefValue .Usage}}{{end}} - -{{end}}For help on global options run "{{.Executable}} help" -`[1:])) -} - -func runHelp(args []string) (exit int) { - if len(args) < 1 { - printGlobalUsage() - return - } - - var cmd *command - for _, c := range commands { - if c.Name == args[0] { - cmd = c - break - } - } - - if cmd == nil { - stderr("Unrecognized command: %s", args[0]) - return 1 - } - - printCommandUsage(cmd) - return -} - -func printGlobalUsage() { - globalUsageTemplate.Execute(tabOut, struct { - Executable string - Commands []*command - Flags []*flag.Flag - Description string - }{ - cliName, - commands, - getFlags(globalFS), - cliDescription, - }) - tabOut.Flush() -} - -func printCommandUsage(cmd *command) { - commandUsageTemplate.Execute(tabOut, struct { - Executable string - Cmd *command - CmdFlags []*flag.Flag - }{ - cliName, - cmd, - getFlags(&cmd.Flags), - }) - tabOut.Flush() -} - -func getFlags(flagset *flag.FlagSet) (flags []*flag.Flag) { - flags = make([]*flag.Flag, 0) - flagset.VisitAll(func(f *flag.Flag) { - flags = append(flags, f) - }) - return -} diff --git a/cmd/dexctl/main.go b/cmd/dexctl/main.go index 9eb3abcc..038ce5e1 100644 --- a/cmd/dexctl/main.go +++ b/cmd/dexctl/main.go @@ -2,21 +2,46 @@ package main import ( "errors" - "flag" "net/http" "os" + "strings" - pflag "github.com/coreos/dex/pkg/flag" "github.com/coreos/dex/pkg/log" "github.com/coreos/go-oidc/oidc" + "github.com/spf13/cobra" + "github.com/spf13/pflag" ) var ( - cliName = "dexctl" - cliDescription = "???" + rootCmd = &cobra.Command{ + Use: "dexctl", + Short: "A command line tool for interacting with the dex system", + Long: "", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // initialize flags from environment + fs := cmd.Flags() - commands []*command - globalFS = flag.NewFlagSet(cliName, flag.ExitOnError) + // don't override flags set by command line flags + alreadySet := make(map[string]bool) + fs.Visit(func(f *pflag.Flag) { alreadySet[f.Name] = true }) + + var err error + fs.VisitAll(func(f *pflag.Flag) { + if err != nil || alreadySet[f.Name] { + return + } + key := "DEXCTL_" + strings.ToUpper(strings.Replace(f.Name, "-", "_", -1)) + if val := os.Getenv(key); val != "" { + err = fs.Set(f.Name, val) + } + }) + return err + }, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + os.Exit(2) + }, + } global struct { endpoint string @@ -30,69 +55,23 @@ var ( func init() { log.EnableTimestamps() - globalFS.StringVar(&global.endpoint, "endpoint", "", "URL of dex API") - globalFS.StringVar(&global.creds.ID, "client-id", "", "dex API user ID") - globalFS.StringVar(&global.creds.Secret, "client-secret", "", "dex API user password") - globalFS.StringVar(&global.dbURL, "db-url", "", "DSN-formatted database connection string") - globalFS.BoolVar(&global.help, "help", false, "Print usage information and exit") - globalFS.BoolVar(&global.help, "h", false, "Print usage information and exit") - globalFS.BoolVar(&global.logDebug, "log-debug", false, "Log debug-level information") + rootCmd.PersistentFlags().StringVar(&global.endpoint, "endpoint", "", "URL of dex API") + rootCmd.PersistentFlags().StringVar(&global.creds.ID, "client-id", "", "dex API user ID") + rootCmd.PersistentFlags().StringVar(&global.creds.Secret, "client-secret", "", "dex API user password") + rootCmd.PersistentFlags().StringVar(&global.dbURL, "db-url", "", "DSN-formatted database connection string") + rootCmd.PersistentFlags().BoolVar(&global.logDebug, "log-debug", false, "Log debug-level information") } func main() { - err := parseFlags() - if err != nil { - stderr(err.Error()) + if err := rootCmd.Execute(); err != nil { os.Exit(2) } - - if global.logDebug { - log.EnableDebug() - } - - args := globalFS.Args() - if len(args) < 1 || global.help { - args = []string{"help"} - } - - var cmd *command - for _, c := range commands { - if c.Name == args[0] { - cmd = c - if err := c.Flags.Parse(args[1:]); err != nil { - stderr("%v", err) - os.Exit(2) - } - break - } - } - - if cmd == nil { - stderr("%v: unknown subcommand: %q", cliName, args[0]) - stderr("Run '%v help' for usage.", cliName) - os.Exit(2) - } - - os.Exit(cmd.Run(cmd.Flags.Args())) } -type command struct { - Name string // Name of the command and the string to use to invoke it - Summary string // One-sentence summary of what the command does - Usage string // Usage options/arguments - Description string // Detailed description of command - Flags flag.FlagSet // Set of flags associated with this command - - Run func(args []string) int // Run a command with the given arguments, return exit status - -} - -func parseFlags() error { - if err := globalFS.Parse(os.Args[1:]); err != nil { - return err +func wrapRun(run func(cmd *cobra.Command, args []string) int) func(cmd *cobra.Command, args []string) { + return func(cmd *cobra.Command, args []string) { + os.Exit(run(cmd, args)) } - - return pflag.SetFlagsFromEnv(globalFS, "DEXCTL") } func getDriver() (drv driver) { diff --git a/functional/dexctl_test.go b/functional/dexctl_test.go new file mode 100644 index 00000000..10ff7179 --- /dev/null +++ b/functional/dexctl_test.go @@ -0,0 +1,72 @@ +package functional + +import ( + "io/ioutil" + "os" + "os/exec" + "strings" + "testing" +) + +var connConfigExample = []byte(`[ + { + "type": "local", + "id": "local", + "passwordInfos": [ + { + "userId":"elroy-id", + "passwordPlaintext": "bones" + }, + { + "userId":"penny", + "passwordPlaintext": "kibble" + } + ] + } +]`) + +func TestDexctlCommands(t *testing.T) { + tempFile, err := ioutil.TempFile("", "dexctl_functional_tests_") + if err != nil { + t.Fatal(err) + } + connConfig := tempFile.Name() + defer os.Remove(connConfig) + if _, err := tempFile.Write(connConfigExample); err != nil { + t.Fatal(err) + } + + tempFile.Close() + + tests := []struct { + args []string + env []string + expErr bool + }{ + { + args: []string{"new-client", "https://example.com/callback"}, + env: []string{"DEXCTL_DB_URL=" + dsn}, + }, + { + args: []string{"new-client", "--db-url", dsn, "https://example.com/callback"}, + }, + { + args: []string{"set-connector-configs", connConfig}, + env: []string{"DEXCTL_DB_URL=" + dsn}, + }, + { + args: []string{"set-connector-configs", "--db-url", dsn, connConfig}, + }, + } + + for _, tt := range tests { + cmd := exec.Command("../bin/dexctl", tt.args...) + cmd.Env = tt.env + out, err := cmd.CombinedOutput() + if !tt.expErr && err != nil { + t.Errorf("cmd 'dexctl %s' failed: %v %s", strings.Join(tt.args, " "), err, out) + } else if tt.expErr && err == nil { + t.Errorf("expected cmd 'dexctl %s' to fail", strings.Join(tt.args, " ")) + } + } +}