diff --git a/examples/simple/main.go b/examples/simple/main.go new file mode 100644 index 0000000..630324f --- /dev/null +++ b/examples/simple/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "git.flexabyte.io/flexabyte/go-kit/pkg/application" + "git.flexabyte.io/flexabyte/go-kit/pkg/slogd" +) + +func main() { + var err error + slogd.Init(slogd.LevelTrace, true) + + config := application.Config{ + Name: "main", + Title: "Main Test", + Banner: "", + Version: "0.1.0-alpha.0+metadata.20101112", + EnableGracefulShutdown: false, + OverrideRunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("overrideRunE") + return nil + }, + PersistentPreRunE: nil, + PersistentPostRunE: nil, + ShutdownSignals: nil, + ShutdownTimeout: 0, + SubCommands: nil, + SubCommandInitializeFunc: nil, + ValidArgs: nil, + } + + var app application.Application + if app, err = application.New(config); err != nil { + panic(err) + } + + if err = app.Start(context.Background()); err != nil { + panic(err) + } +} diff --git a/pkg/app/app.go b/pkg/app/app.go deleted file mode 100644 index 5a743d1..0000000 --- a/pkg/app/app.go +++ /dev/null @@ -1,13 +0,0 @@ -package app - -import "fmt" - -func New() *App { - return &App{} -} - -type App struct{} - -func (a *App) Execute() { - fmt.Println("Hello World") -} diff --git a/pkg/application/application.go b/pkg/application/application.go new file mode 100644 index 0000000..b43f9b3 --- /dev/null +++ b/pkg/application/application.go @@ -0,0 +1,38 @@ +package application + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" +) + +type Application interface { + Start(ctx context.Context) error + Shutdown() error +} + +func New(c Config) (Application, error) { + var cmd *cobra.Command + var err error + if cmd, err = c.getRootCommand(); err != nil { + return nil, err + } + + return &application{ + cmd: cmd, + }, nil +} + +type application struct { + cmd *cobra.Command +} + +func (a *application) Start(ctx context.Context) error { + return a.cmd.ExecuteContext(ctx) +} + +func (a *application) Shutdown() error { + fmt.Println("Shutdown") + return nil +} diff --git a/pkg/application/application_test.go b/pkg/application/application_test.go new file mode 100644 index 0000000..b584a8a --- /dev/null +++ b/pkg/application/application_test.go @@ -0,0 +1 @@ +package application diff --git a/pkg/application/commander.go b/pkg/application/commander.go new file mode 100644 index 0000000..217e1e8 --- /dev/null +++ b/pkg/application/commander.go @@ -0,0 +1,28 @@ +package application + +import "github.com/spf13/cobra" + +type Commander interface { + Initialize(f func(c *cobra.Command)) *cobra.Command +} + +type Command struct { + Command *cobra.Command + SubCommands []Commander + Configure func(c *cobra.Command) +} + +func (c Command) Initialize(f func(c *cobra.Command)) *cobra.Command { + if f != nil { + f(c.Command) + } + + if c.Configure != nil { + c.Configure(c.Command) + } + + for _, sub := range c.SubCommands { + c.Command.AddCommand(sub.Initialize(f)) + } + return c.Command +} diff --git a/pkg/application/config.go b/pkg/application/config.go new file mode 100644 index 0000000..5808c4b --- /dev/null +++ b/pkg/application/config.go @@ -0,0 +1,110 @@ +package application + +import ( + "errors" + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + + "git.flexabyte.io/flexabyte/go-kit/pkg/semver" +) + +type Config struct { + Name string + Title string + Banner string + Version string + EnableGracefulShutdown bool + OverrideRunE func(cmd *cobra.Command, args []string) error + PersistentPreRunE []func(cmd *cobra.Command, args []string) error // collection of PreRunE functions + PersistentPostRunE []func(cmd *cobra.Command, args []string) error // collection of PostRunE functions + ShutdownSignals []os.Signal + ShutdownTimeout time.Duration + SubCommands []Command + SubCommandInitializeFunc func(cmd *cobra.Command) + ValidArgs []string +} + +func (c Config) getRootCommand() (*cobra.Command, error) { + var err error + if err = c.Validate(); err != nil { + return nil, err + } + + var long string + if c.Banner != "" { + long = c.Banner + "\n" + c.Title + } else { + long = c.Title + } + + cmd := &cobra.Command{ + Use: c.Name, + Short: c.Title, + Long: long, + PersistentPreRunE: persistentPreRunFuncE, + PersistentPostRunE: persistentPostRunFuncE, + RunE: runFuncE, + SilenceErrors: true, + SilenceUsage: true, + } + + if c.OverrideRunE != nil { + cmd.RunE = c.OverrideRunE + } + + for _, subcommand := range c.SubCommands { + cmd.AddCommand(subcommand.Initialize(c.SubCommandInitializeFunc)) + } + + var v semver.Version + if v, err = c.ParseVersion(); err != nil { + return nil, err + } + + configureVersionFlag(cmd, v) // Configure app for version information + configureOutputFlags(cmd) // Configure verbosity + configureLoggingFlags(cmd) // Configure logging + cmd.PersistentFlags().SetNormalizeFunc(normalizeFunc) // normalize persistent flags + + return cmd, nil +} + +func (c Config) ParseVersion() (semver.Version, error) { + return semver.Parse(c.Version) +} + +func (c Config) RegisterCommand(cmd Commander, f func(*cobra.Command)) { + appCmd.AddCommand(cmd.Initialize(f)) +} + +func (c Config) RegisterCommands(cmds []Commander, f func(*cobra.Command)) { + for _, cmd := range cmds { + appCmd.AddCommand(cmd.Initialize(f)) + } +} + +func (c Config) RegisterPersistentPreRunE(f func(cmd *cobra.Command, args []string) error) { + persistentPreRunE = append(persistentPreRunE, f) +} + +func (c Config) RegisterPersistentPostRunE(f func(cmd *cobra.Command, args []string) error) { + persistentPostRunE = append(persistentPostRunE, f) +} + +func (c Config) Validate() error { + if c.Name == "" { + return errors.New("name is required") + } + if c.Title == "" { + return errors.New("title is required") + } + + var err error + if _, err = semver.Parse(c.Version); err != nil { + return fmt.Errorf("invalid version: %s", c.Version) + } + return nil +} diff --git a/pkg/application/config_test.go b/pkg/application/config_test.go new file mode 100644 index 0000000..9528332 --- /dev/null +++ b/pkg/application/config_test.go @@ -0,0 +1,22 @@ +package application + +import ( + "testing" +) + +func TestConfig_Validate(t *testing.T) { + tests := []struct { + name string + config Config + wantErr bool + }{ + {}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.config.Validate(); (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/application/globals.go b/pkg/application/globals.go new file mode 100644 index 0000000..4342992 --- /dev/null +++ b/pkg/application/globals.go @@ -0,0 +1,105 @@ +package application + +import ( + "io" + "log/slog" + "os" + "reflect" + "runtime" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "git.flexabyte.io/flexabyte/go-kit/pkg/slogd" + "git.flexabyte.io/flexabyte/go-kit/pkg/slogd_colored" +) + +var ( + appName string + appCmd *cobra.Command + persistentPreRunE []func(cmd *cobra.Command, args []string) error // collection of PreRunE functions + persistentPostRunE []func(cmd *cobra.Command, args []string) error // collection of PostRunE functions + outWriter io.Writer = os.Stdout + // version semver.Version +) + +func helpFuncE(cmd *cobra.Command, args []string) error { + return cmd.Help() +} + +func normalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName { + // switch name { + // case "no-color": + // name = "log-type" + // break + // } + return pflag.NormalizedName(name) +} + +func persistentPreRunFuncE(cmd *cobra.Command, args []string) error { + slogd.SetLevel(slogd.Level(logLevelFlag)) + if slogd.ActiveHandler() == slogd_colored.HandlerColor && noColorFlag { + slogd.UseHandler(slogd.HandlerText) + cmd.SetContext(slogd.WithContext(cmd.Context())) + } + + slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "starting application", slog.String("command", cmd.CommandPath())) + slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "executing PersistentPreRun") + + // Make sure we can always get the version + if versionFlag || cmd.Use == versionName { + slogd.FromContext(cmd.Context()).LogAttrs(cmd.Context(), slogd.LevelTrace, "overriding command", slog.String("old_function", runtime.FuncForPC(reflect.ValueOf(cmd.RunE).Pointer()).Name()), slog.String("new_function", runtime.FuncForPC(reflect.ValueOf(versionRunFuncE).Pointer()).Name())) + cmd.RunE = versionRunFuncE + return nil + } + + // Make sure that we show the app help if no commands or flags are passed + if cmd.CalledAs() == appName && runtime.FuncForPC(reflect.ValueOf(cmd.RunE).Pointer()).Name() == runtime.FuncForPC(reflect.ValueOf(runFuncE).Pointer()).Name() { + slogd.FromContext(cmd.Context()).LogAttrs(cmd.Context(), slogd.LevelTrace, "overriding command", slog.String("old_function", runtime.FuncForPC(reflect.ValueOf(cmd.RunE).Pointer()).Name()), slog.String("new_function", runtime.FuncForPC(reflect.ValueOf(helpFuncE).Pointer()).Name())) + + cmd.RunE = helpFuncE + return nil + } + + // TODO move to front?? + if quietFlag { + slogd.FromContext(cmd.Context()).LogAttrs(cmd.Context(), slogd.LevelDebug, "activating quiet mode") + outWriter = io.Discard + } + + if persistentPreRunE == nil { + return nil + } + + var err error + for _, preRun := range persistentPreRunE { + slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "executing PersistentPreRun function", slog.String("function", runtime.FuncForPC(reflect.ValueOf(preRun).Pointer()).Name())) + if err = preRun(cmd, args); err != nil { + return err + } + } + return nil +} + +func persistentPostRunFuncE(cmd *cobra.Command, args []string) error { + defer slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "stopping application", slog.String("command", cmd.CommandPath())) + slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "executing PersistentPostRunE") + + if persistentPostRunE == nil { + return nil + } + + var err error + for _, postRun := range persistentPostRunE { + slogd.FromContext(cmd.Context()).Log(cmd.Context(), slogd.LevelTrace, "executing PersistentPostRun function", slog.String("function", runtime.FuncForPC(reflect.ValueOf(postRun).Pointer()).Name())) + if err = postRun(cmd, args); err != nil { + return err + } + } + return nil +} + +// appRunE is an empty catch function to allow overrides through persistentPreRunE +func runFuncE(cmd *cobra.Command, args []string) error { + return nil +} diff --git a/pkg/application/logging.go b/pkg/application/logging.go new file mode 100644 index 0000000..af7680b --- /dev/null +++ b/pkg/application/logging.go @@ -0,0 +1,35 @@ +package application + +import ( + "github.com/spf13/cobra" +) + +const ( + LogOutputStdOut = "stdout" + LogOutputStdErr = "stderr" + LogOutputFile = "file" +) + +var logLevelFlag string +var logOutputFlag string +var logTypeFlag string + +func addLogLevelFlag(cmd *cobra.Command) { + cmd.PersistentFlags().StringVarP(&logLevelFlag, "log-level", "", "info", "Set log level (trace, debug, info, warn, error, fatal)") +} + +func addLogOutputFlag(cmd *cobra.Command) { + cmd.PersistentFlags().StringVarP(&logOutputFlag, "log-outWriter", "", "stderr", "Set log outWriter (stdout, stderr, file)") +} + +func addLogTypeFlag(cmd *cobra.Command) { + cmd.PersistentFlags().StringVarP(&logTypeFlag, "log-type", "", "text", "Set log type (text, json, color)") +} + +func configureLoggingFlags(cmd *cobra.Command) { + addLogLevelFlag(cmd) + addLogOutputFlag(cmd) + addLogTypeFlag(cmd) + + cmd.MarkFlagsMutuallyExclusive("no-color", "log-type") +} diff --git a/pkg/application/output.go b/pkg/application/output.go new file mode 100644 index 0000000..d9960c3 --- /dev/null +++ b/pkg/application/output.go @@ -0,0 +1,36 @@ +package application + +import "github.com/spf13/cobra" + +var jsonOutputFlag bool +var noColorFlag bool +var quietFlag bool +var verboseFlag bool + +func addJsonOutputFlag(cmd *cobra.Command) { + cmd.PersistentFlags().BoolVarP(&jsonOutputFlag, "json", "", false, "Enable JSON outWriter") +} + +func addNoColorFlag(cmd *cobra.Command) { + cmd.PersistentFlags().BoolVarP(&noColorFlag, "no-color", "", false, "Disable color outWriter") +} + +func addQuietFlag(cmd *cobra.Command) { + cmd.PersistentFlags().BoolVarP(&quietFlag, "quiet", "q", false, "Enable quiet mode") +} + +func addVerboseFlag(cmd *cobra.Command) { + cmd.PersistentFlags().BoolVarP(&verboseFlag, "verbose", "v", false, "Enable verbose outWriter") +} + +func configureOutputFlags(cmd *cobra.Command) { + addJsonOutputFlag(cmd) + addNoColorFlag(cmd) + addVerboseFlag(cmd) + addQuietFlag(cmd) + + cmd.MarkFlagsMutuallyExclusive("verbose", "quiet", "json") + cmd.MarkFlagsMutuallyExclusive("json", "no-color") + cmd.MarkFlagsMutuallyExclusive("quiet", "no-color") + +} diff --git a/pkg/application/version.go b/pkg/application/version.go new file mode 100644 index 0000000..248fab9 --- /dev/null +++ b/pkg/application/version.go @@ -0,0 +1,69 @@ +package application + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + + "git.flexabyte.io/flexabyte/go-kit/pkg/semver" +) + +const ( + versionName = "version" + versionShortHand = "V" + versionUsage = "Show version information" +) + +var ( + version semver.Version + versionFlag bool + versionCmd = &cobra.Command{ + Use: versionName, + Short: versionUsage, + RunE: versionRunFuncE, + } +) + +func addVersionFlag(cmd *cobra.Command) { + cmd.PersistentFlags().BoolVarP(&versionFlag, versionName, versionShortHand, false, versionUsage) +} + +func configureVersionFlag(cmd *cobra.Command, v semver.Version) { + version = v + cmd.AddCommand(versionCmd) + addVersionFlag(cmd) +} + +func printVersion(v semver.Version) string { + var output string + if !verboseFlag { + output = v.String() + } + + if jsonOutputFlag { + var b []byte + b, _ = json.Marshal(v) + output = string(b) + } + + if output != "" { + return output + } + + return fmt.Sprintf( + "Full: %s\nVersion: %s\nChannel: %s\nCommit: %s\nDate: %s", + v.String(), + v.Number(), + v.Release(), + v.Commit(), + v.Date(), + ) +} + +func versionRunFuncE(cmd *cobra.Command, args []string) error { + if _, err := fmt.Fprintln(outWriter, printVersion(version)); err != nil { + return err + } + return nil +}