From 5c3c83b2cb2c203d461ff28b39390cce82f161df Mon Sep 17 00:00:00 2001 From: Christian Schwarz Date: Sat, 13 Oct 2018 15:07:50 +0200 Subject: [PATCH] cli: refactor to allow definition of subcommands next to their implementation --- cli/cli.go | 105 ++++++++++++++++++++++++++ client/configcheck.go | 36 ++++++++- client/pprof.go | 37 ++++++++- client/signal.go | 11 ++- client/status.go | 26 +++++-- client/stdinserver.go | 14 +++- client/version.go | 39 +++++++--- daemon/main.go | 9 +++ main.go | 169 +++--------------------------------------- 9 files changed, 255 insertions(+), 191 deletions(-) create mode 100644 cli/cli.go diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000..5e646d1 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,105 @@ +package cli + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/zrepl/zrepl/config" + "os" +) + +var rootArgs struct { + configPath string +} + +var rootCmd = &cobra.Command{ + Use: "zrepl", + Short: "One-stop ZFS replication solution", +} + +var bashcompCmd = &cobra.Command{ + Use: "bashcomp path/to/out/file", + Short: "generate bash completions", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n") + cmd.Usage() + os.Exit(1) + } + if err := rootCmd.GenBashCompletionFile(args[0]); err != nil { + fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err) + os.Exit(1) + } + }, + Hidden: true, +} + +func init() { + rootCmd.PersistentFlags().StringVar(&rootArgs.configPath, "config", "", "config file path") + rootCmd.AddCommand(bashcompCmd) +} + +type Subcommand struct { + Use string + Short string + NoRequireConfig bool + Run func(subcommand *Subcommand, args []string) error + SetupFlags func(f *pflag.FlagSet) + + config *config.Config + configErr error +} + +func (s *Subcommand) ConfigParsingError() error { + return s.configErr +} + +func (s *Subcommand) Config() *config.Config { + if !s.NoRequireConfig && s.config == nil { + panic("command that requires config is running and has no config set") + } + return s.config +} + +func (s *Subcommand) run(cmd *cobra.Command, args []string) { + s.tryParseConfig() + err := s.Run(s, args) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } +} + +func (s *Subcommand) tryParseConfig() { + config, err := config.ParseConfig(rootArgs.configPath) + s.configErr = err + if err != nil { + if s.NoRequireConfig { + // doesn't matter + return + } else { + fmt.Fprintf(os.Stderr, "could not parse config: %s\n", err) + os.Exit(1) + } + } + s.config = config +} + +func AddSubcommand(s *Subcommand) { + cmd := cobra.Command{ + Use: s.Use, + Short: s.Short, + Run: s.run, + } + if s.SetupFlags != nil { + s.SetupFlags(cmd.Flags()) + } + rootCmd.AddCommand(&cmd) +} + + +func Run() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} \ No newline at end of file diff --git a/client/configcheck.go b/client/configcheck.go index b50ac80..34c1b13 100644 --- a/client/configcheck.go +++ b/client/configcheck.go @@ -1,8 +1,36 @@ package client -import "github.com/zrepl/zrepl/config" +import ( + "encoding/json" + "github.com/kr/pretty" + "github.com/spf13/pflag" + "github.com/zrepl/yaml-config" + "github.com/zrepl/zrepl/cli" + "os" +) -func RunConfigcheck(conf *config.Config, args []string) error { - // TODO: do the 'build' steps, e.g. build the jobs and see if that fails - return nil +var configcheckArgs struct { + format string } + +var ConfigcheckCmd = &cli.Subcommand{ + Use: "configcheck", + Short: "check if config can be parsed without errors", + SetupFlags: func(f *pflag.FlagSet) { + f.StringVar(&configcheckArgs.format, "format", "", "dump parsed config object [pretty|yaml|json]") + }, + Run: func(subcommand *cli.Subcommand, args []string) error { + switch configcheckArgs.format { + case "pretty": + _, err := pretty.Println(subcommand.Config()) + return err + case "json": + return json.NewEncoder(os.Stdout).Encode(subcommand.Config()) + case "yaml": + return yaml.NewEncoder(os.Stdout).Encode(subcommand.Config()) + default: // no output + } + return nil + }, +} + diff --git a/client/pprof.go b/client/pprof.go index 7afb4cb..29cd4e5 100644 --- a/client/pprof.go +++ b/client/pprof.go @@ -1,17 +1,48 @@ package client import ( + "errors" + "github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/daemon" "log" "os" ) -type PProfArgs struct { +var pprofArgs struct { daemon.PprofServerControlMsg } -func RunPProf(conf *config.Config, args PProfArgs) { +var PprofCmd = &cli.Subcommand{ + Use: "pprof off | [on TCP_LISTEN_ADDRESS]", + Short: "start a http server exposing go-tool-compatible profiling endpoints at TCP_LISTEN_ADDRESS", + Run: func(subcommand *cli.Subcommand, args []string) error { + if len(args) < 1 { + goto enargs + } + switch args[0] { + case "on": + pprofArgs.Run = true + if len(args) != 2 { + return errors.New("must specify TCP_LISTEN_ADDRESS as second positional argument") + } + pprofArgs.HttpListenAddress = args[1] + case "off": + if len(args) != 1 { + goto enargs + } + pprofArgs.Run = false + } + + RunPProf(subcommand.Config()) + return nil + enargs: + return errors.New("invalid number of positional arguments") + + }, +} + +func RunPProf(conf *config.Config) { log := log.New(os.Stderr, "", 0) die := func() { @@ -26,7 +57,7 @@ func RunPProf(conf *config.Config, args PProfArgs) { log.Printf("error creating http client: %s", err) die() } - err = jsonRequestResponse(httpc, daemon.ControlJobEndpointPProf, args.PprofServerControlMsg, struct{}{}) + err = jsonRequestResponse(httpc, daemon.ControlJobEndpointPProf, pprofArgs.PprofServerControlMsg, struct{}{}) if err != nil { log.Printf("error sending control message: %s", err) die() diff --git a/client/signal.go b/client/signal.go index 273ea18..701849e 100644 --- a/client/signal.go +++ b/client/signal.go @@ -2,11 +2,20 @@ package client import ( "github.com/pkg/errors" + "github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/daemon" ) -func RunSignal(config *config.Config, args []string) error { +var SignalCmd = &cli.Subcommand{ + Use: "signal [wakeup|reset] JOB", + Short: "wake up a job from wait state or abort its current invocation", + Run: func(subcommand *cli.Subcommand, args []string) error { + return runSignalCmd(subcommand.Config(), args) + }, +} + +func runSignalCmd(config *config.Config, args []string) error { if len(args) != 2 { return errors.Errorf("Expected 2 arguments: [wakeup|reset] JOB") } diff --git a/client/status.go b/client/status.go index 7ecb986..7e715e9 100644 --- a/client/status.go +++ b/client/status.go @@ -4,21 +4,22 @@ import ( "fmt" "github.com/nsf/termbox-go" "github.com/pkg/errors" + "github.com/spf13/pflag" "github.com/zrepl/yaml-config" - "github.com/zrepl/zrepl/config" + "github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/daemon" "github.com/zrepl/zrepl/daemon/job" "github.com/zrepl/zrepl/daemon/pruner" "github.com/zrepl/zrepl/replication" "github.com/zrepl/zrepl/replication/fsrep" + "io" "math" + "net/http" + "os" "sort" "strings" "sync" "time" - "io" - "os" - "net/http" ) type tui struct { @@ -73,17 +74,26 @@ func (t *tui) addIndent(indent int) { t.moveLine(0, 0) } -type StatusFlags struct { +var statusFlags struct { Raw bool } -func RunStatus(flags StatusFlags, config *config.Config, args []string) error { - httpc, err := controlHttpClient(config.Global.Control.SockPath) +var StatusCmd = &cli.Subcommand{ + Use: "status", + Short: "show job activity or dump as JSON for monitoring", + SetupFlags: func(f *pflag.FlagSet) { + f.BoolVar(&statusFlags.Raw, "raw", false, "dump raw status description from zrepl daemon") + }, + Run: runStatus, +} + +func runStatus(s *cli.Subcommand, args []string) error { + httpc, err := controlHttpClient(s.Config().Global.Control.SockPath) if err != nil { return err } - if flags.Raw { + if statusFlags.Raw { resp, err := httpc.Get("http://unix"+daemon.ControlJobEndpointStatus) if err != nil { return err diff --git a/client/stdinserver.go b/client/stdinserver.go index 9d47a25..5db5520 100644 --- a/client/stdinserver.go +++ b/client/stdinserver.go @@ -1,18 +1,26 @@ package client import ( + "github.com/zrepl/zrepl/cli" "os" "context" + "errors" "github.com/problame/go-netssh" + "github.com/zrepl/zrepl/config" "log" "path" - "github.com/zrepl/zrepl/config" - "errors" ) +var StdinserverCmd = &cli.Subcommand{ + Use: "stdinserver CLIENT_IDENTITY", + Short: "stdinserver transport mode (started from authorized_keys file as forced command)", + Run: func(subcommand *cli.Subcommand, args []string) error { + return runStdinserver(subcommand.Config(), args) + }, +} -func RunStdinserver(config *config.Config, args []string) error { +func runStdinserver(config *config.Config, args []string) error { // NOTE: the netssh proxying protocol requires exiting with non-zero status if anything goes wrong defer os.Exit(1) diff --git a/client/version.go b/client/version.go index e584a80..9dcec05 100644 --- a/client/version.go +++ b/client/version.go @@ -2,27 +2,39 @@ package client import ( "fmt" + "github.com/spf13/pflag" + "github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/daemon" "github.com/zrepl/zrepl/version" "os" ) -type VersionArgs struct { +var versionArgs struct { Show string Config *config.Config + ConfigErr error } -func RunVersion(args VersionArgs) { +var VersionCmd = &cli.Subcommand{ + Use: "version", + Short: "print version of zrepl binary and running daemon", + NoRequireConfig: true, + SetupFlags: func(f *pflag.FlagSet) { + f.StringVar(&versionArgs.Show, "show", "", "version info to show (client|daemon)") + }, + Run: func(subcommand *cli.Subcommand, args []string) error { + versionArgs.Config = subcommand.Config() + versionArgs.ConfigErr = subcommand.ConfigParsingError() + return runVersionCmd() + }, +} - die := func() { - fmt.Fprintf(os.Stderr, "exiting after error\n") - os.Exit(1) - } +func runVersionCmd() error { + args := versionArgs if args.Show != "daemon" && args.Show != "client" && args.Show != "" { - fmt.Fprintf(os.Stderr, "show flag must be 'client' or 'server' or be left empty") - die() + return fmt.Errorf("show flag must be 'client' or 'server' or be left empty") } var clientVersion, daemonVersion *version.ZreplVersionInformation @@ -32,17 +44,19 @@ func RunVersion(args VersionArgs) { } if args.Show == "daemon" || args.Show == "" { + if args.ConfigErr != nil { + return fmt.Errorf("config parsing error: %s", args.ConfigErr) + } + httpc, err := controlHttpClient(args.Config.Global.Control.SockPath) if err != nil { - fmt.Fprintf(os.Stderr, "server: error: %s\n", err) - die() + return fmt.Errorf("server: error: %s\n", err) } var info version.ZreplVersionInformation err = jsonRequestResponse(httpc, daemon.ControlJobEndpointVersion, "", &info) if err != nil { - fmt.Fprintf(os.Stderr, "server: error: %s\n", err) - die() + return fmt.Errorf("server: error: %s\n", err) } daemonVersion = &info fmt.Printf("server: %s\n", daemonVersion.String()) @@ -54,4 +68,5 @@ func RunVersion(args VersionArgs) { } } + return nil } diff --git a/daemon/main.go b/daemon/main.go index 2dd2590..fae8c12 100644 --- a/daemon/main.go +++ b/daemon/main.go @@ -1,7 +1,16 @@ package daemon import ( + "github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/logger" ) type Logger = logger.Logger + +var DaemonCmd = &cli.Subcommand { + Use: "daemon", + Short: "run the zrepl daemon", + Run: func(subcommand *cli.Subcommand, args []string) error { + return Run(subcommand.Config()) + }, +} diff --git a/main.go b/main.go index 7c112a6..3b5ae45 100644 --- a/main.go +++ b/main.go @@ -2,172 +2,21 @@ package main import ( - "errors" - "github.com/spf13/cobra" + "github.com/zrepl/zrepl/cli" "github.com/zrepl/zrepl/client" - "github.com/zrepl/zrepl/config" "github.com/zrepl/zrepl/daemon" - "log" - "os" - "fmt" ) -var rootCmd = &cobra.Command{ - Use: "zrepl", - Short: "One-stop ZFS replication solution", -} - -var daemonCmd = &cobra.Command{ - Use: "daemon", - Short: "run the zrepl daemon", - RunE: func(cmd *cobra.Command, args []string) error { - conf, err := config.ParseConfig(rootArgs.configFile) - if err != nil { - return err - } - return daemon.Run(conf) - }, -} - -var signalCmd = &cobra.Command{ - Use: "signal [wakeup|reset] JOB", - Short: "wake up a job from wait state or abort its current invocation", - RunE: func(cmd *cobra.Command, args []string) error { - conf, err := config.ParseConfig(rootArgs.configFile) - if err != nil { - return err - } - return client.RunSignal(conf, args) - }, -} - -var statusCmdFlags client.StatusFlags - -var statusCmd = &cobra.Command{ - Use: "status", - Short: "show job activity or dump as JSON for monitoring", - RunE: func(cmd *cobra.Command, args []string) error { - conf, err := config.ParseConfig(rootArgs.configFile) - if err != nil { - return err - } - return client.RunStatus(statusCmdFlags, conf, args) - }, -} - -var stdinserverCmd = &cobra.Command{ - Use: "stdinserver CLIENT_IDENTITY", - Short: "stdinserver transport mode (started from authorized_keys file as forced command)", - RunE: func(cmd *cobra.Command, args []string) error { - conf, err := config.ParseConfig(rootArgs.configFile) - if err != nil { - return err - } - return client.RunStdinserver(conf, args) - }, -} - - -var bashcompCmd = &cobra.Command{ - Use: "bashcomp path/to/out/file", - Short: "generate bash completions", - Run: func(cmd *cobra.Command, args []string) { - if len(args) != 1 { - fmt.Fprintf(os.Stderr, "specify exactly one positional agument\n") - cmd.Usage() - os.Exit(1) - } - if err := rootCmd.GenBashCompletionFile(args[0]); err != nil { - fmt.Fprintf(os.Stderr, "error generating bash completion: %s", err) - os.Exit(1) - } - }, - Hidden: true, -} - -var configcheckCmd = &cobra.Command{ - Use: "configcheck", - Short: "check if config can be parsed without errors", - RunE: func(cmd *cobra.Command, args []string) error { - conf, err := config.ParseConfig(rootArgs.configFile) - if err != nil { - return err - } - return client.RunConfigcheck(conf, args) - }, -} - -var versionCmdArgs client.VersionArgs -var versionCmd = &cobra.Command{ - Use: "version", - Short: "print version of zrepl binary and running daemon", - Run: func(cmd *cobra.Command, args []string) { - conf, err := config.ParseConfig(rootArgs.configFile) - if err == nil { - versionCmdArgs.Config = conf - } - client.RunVersion(versionCmdArgs) - }, -} - -var pprofCmd = &cobra.Command{ - Use: "pprof off | [on TCP_LISTEN_ADDRESS]", - Short: "start a http server exposing go-tool-compatible profiling endpoints at TCP_LISTEN_ADDRESS", - RunE: func(cmd *cobra.Command, args []string) error { - conf, err := config.ParseConfig(rootArgs.configFile) - if err != nil { - return err - } - - var pprofCmdArgs client.PProfArgs - if cmd.Flags().NArg() < 1 { - goto enargs - } - switch cmd.Flags().Arg(0) { - case "on": - pprofCmdArgs.Run = true - if cmd.Flags().NArg() != 2 { - return errors.New("must specify TCP_LISTEN_ADDRESS as second positional argument") - } - pprofCmdArgs.HttpListenAddress = cmd.Flags().Arg(1) - case "off": - if cmd.Flags().NArg() != 1 { - goto enargs - } - pprofCmdArgs.Run = false - } - - client.RunPProf(conf, pprofCmdArgs) - return nil - enargs: - return errors.New("invalid number of positional arguments") - - }, -} - -var rootArgs struct { - configFile string -} - func init() { - //cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVar(&rootArgs.configFile, "config", "", "config file path") - rootCmd.AddCommand(daemonCmd) - rootCmd.AddCommand(signalCmd) - statusCmd.Flags().BoolVar(&statusCmdFlags.Raw, "raw", false, "dump raw status description from zrepl daemon") - rootCmd.AddCommand(statusCmd) - rootCmd.AddCommand(stdinserverCmd) - rootCmd.AddCommand(bashcompCmd) - rootCmd.AddCommand(configcheckCmd) - versionCmd.Flags().StringVar(&versionCmdArgs.Show, "show", "", "version info to show (client|daemon)") - rootCmd.AddCommand(versionCmd) - rootCmd.AddCommand(pprofCmd) + cli.AddSubcommand(daemon.DaemonCmd) + cli.AddSubcommand(client.StatusCmd) + cli.AddSubcommand(client.SignalCmd) + cli.AddSubcommand(client.StdinserverCmd) + cli.AddSubcommand(client.ConfigcheckCmd) + cli.AddSubcommand(client.VersionCmd) + cli.AddSubcommand(client.PprofCmd) } func main() { - - if err := rootCmd.Execute(); err != nil { - log.Printf("error executing root command: %s", err) - os.Exit(1) - } + cli.Run() }