From 71a5a44923029180912be0932889dfb5ff98a605 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 24 Nov 2024 00:40:07 -0500 Subject: [PATCH 1/6] Reorganize config --- pkg/config/config.go | 33 --------------------------------- pkg/config/parse.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 33 deletions(-) create mode 100644 pkg/config/parse.go diff --git a/pkg/config/config.go b/pkg/config/config.go index c9f3074..1c33128 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,15 +1,12 @@ package config import ( - "os" "sync" "time" "dynatron.me/x/stillbox/internal/jsontypes" "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "gopkg.in/yaml.v3" ) type Config struct { @@ -92,33 +89,3 @@ func (rl *RateLimit) Verify() bool { return false } - -func (c *Config) PreRunE() func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - return c.ReadConfig() - } -} - -func New(rootCommand *cobra.Command) *Config { - c := &Config{} - - rootCommand.PersistentFlags().StringVarP(&c.configPath, "config", "c", "config.yaml", "configuration file") - - return c -} - -func (c *Config) ReadConfig() error { - cfgBytes, err := os.ReadFile(c.configPath) - if err != nil { - return err - } - - err = yaml.Unmarshal(cfgBytes, c) - if err != nil { - return err - } - - log.Info().Str("configPath", c.configPath).Msg("read config") - - return nil -} diff --git a/pkg/config/parse.go b/pkg/config/parse.go new file mode 100644 index 0000000..e4375ac --- /dev/null +++ b/pkg/config/parse.go @@ -0,0 +1,39 @@ +package config + +import ( + "os" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +func (c *Config) PreRunE() func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + return c.ReadConfig() + } +} + +func New(rootCommand *cobra.Command) *Config { + c := &Config{} + + rootCommand.PersistentFlags().StringVarP(&c.configPath, "config", "c", "config.yaml", "configuration file") + + return c +} + +func (c *Config) ReadConfig() error { + cfgBytes, err := os.ReadFile(c.configPath) + if err != nil { + return err + } + + err = yaml.Unmarshal(cfgBytes, c) + if err != nil { + return err + } + + log.Info().Str("configPath", c.configPath).Msg("read config") + + return nil +} From a845259d81a31e80ec871fe978b979a928b26c4d Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 24 Nov 2024 08:40:14 -0500 Subject: [PATCH 2/6] Reorg config --- pkg/cmd/admin/admin.go | 4 +- pkg/cmd/serve/serve.go | 6 +-- pkg/config/config.go | 8 ++- pkg/config/parse.go | 18 ++++--- pkg/config/parse_test.go | 81 +++++++++++++++++++++++++++++ pkg/config/testdata/testconfig.yaml | 38 ++++++++++++++ pkg/server/server.go | 4 +- pkg/server/signals.go | 2 +- 8 files changed, 144 insertions(+), 17 deletions(-) create mode 100644 pkg/config/parse_test.go create mode 100644 pkg/config/testdata/testconfig.yaml diff --git a/pkg/cmd/admin/admin.go b/pkg/cmd/admin/admin.go index 725c654..d515bf4 100644 --- a/pkg/cmd/admin/admin.go +++ b/pkg/cmd/admin/admin.go @@ -115,14 +115,14 @@ func readPassword(prompt string) (string, error) { } // Command is the users command. -func Command(cfg *config.Config) []*cobra.Command { +func Command(cfg *config.Configuration) []*cobra.Command { userCmd := &cobra.Command{ Use: "users", Aliases: []string{"u"}, Short: "administers the server", PersistentPreRunE: cfg.PreRunE(), } - userCmd.AddCommand(addUserCommand(cfg), passwdCommand(cfg)) + userCmd.AddCommand(addUserCommand(&cfg.Config), passwdCommand(&cfg.Config)) return []*cobra.Command{userCmd} } diff --git a/pkg/cmd/serve/serve.go b/pkg/cmd/serve/serve.go index 314504f..f5ea062 100644 --- a/pkg/cmd/serve/serve.go +++ b/pkg/cmd/serve/serve.go @@ -13,10 +13,10 @@ import ( ) type ServeOptions struct { - cfg *config.Config + cfg *config.Configuration } -func Command(cfg *config.Config) *cobra.Command { +func Command(cfg *config.Configuration) *cobra.Command { opts := makeOptions(cfg) serveCmd := &cobra.Command{ Use: "serve", @@ -28,7 +28,7 @@ func Command(cfg *config.Config) *cobra.Command { return serveCmd } -func makeOptions(cfg *config.Config) *ServeOptions { +func makeOptions(cfg *config.Configuration) *ServeOptions { return &ServeOptions{ cfg: cfg, } diff --git a/pkg/config/config.go b/pkg/config/config.go index 1c33128..e934d06 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -9,6 +9,12 @@ import ( "github.com/rs/zerolog/log" ) +type Configuration struct { + Config + + configPath string `yaml:"-"` +} + type Config struct { DB DB `yaml:"db"` CORS CORS `yaml:"cors"` @@ -20,8 +26,6 @@ type Config struct { RateLimit RateLimit `yaml:"rateLimit"` Notify Notify `yaml:"notify"` Relay []Relay `yaml:"relay"` - - configPath string } type Auth struct { diff --git a/pkg/config/parse.go b/pkg/config/parse.go index e4375ac..01f90fa 100644 --- a/pkg/config/parse.go +++ b/pkg/config/parse.go @@ -8,32 +8,36 @@ import ( "gopkg.in/yaml.v3" ) -func (c *Config) PreRunE() func(*cobra.Command, []string) error { +func (c *Configuration) PreRunE() func(*cobra.Command, []string) error { return func(cmd *cobra.Command, args []string) error { return c.ReadConfig() } } -func New(rootCommand *cobra.Command) *Config { - c := &Config{} +func New(rootCommand *cobra.Command) *Configuration { + c := &Configuration{} rootCommand.PersistentFlags().StringVarP(&c.configPath, "config", "c", "config.yaml", "configuration file") return c } -func (c *Config) ReadConfig() error { +func (c *Configuration) ReadConfig() error { + log.Info().Str("configPath", c.configPath).Msg("read config") + + return c.read() +} + +func (c *Configuration) read() error { cfgBytes, err := os.ReadFile(c.configPath) if err != nil { return err } - err = yaml.Unmarshal(cfgBytes, c) + err = yaml.Unmarshal(cfgBytes, &c.Config) if err != nil { return err } - log.Info().Str("configPath", c.configPath).Msg("read config") - return nil } diff --git a/pkg/config/parse_test.go b/pkg/config/parse_test.go new file mode 100644 index 0000000..e286650 --- /dev/null +++ b/pkg/config/parse_test.go @@ -0,0 +1,81 @@ +package config + +import ( + "testing" + "time" + + "dynatron.me/x/stillbox/internal/common" + "dynatron.me/x/stillbox/internal/jsontypes" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" +) + +var expCfg = &Config{ + DB: DB{ + Connect: "postgres://stillbox:somepassword@stillbox:5432/stillbox?sslmode=disable", + LogQueries: true, + }, + CORS: CORS{ + AllowedOrigins: []string{ + "http://localhost:*", +}, + }, + Auth: Auth{ + JWTSecret: "somesecret", + Domain: "xenon", + AllowInsecure: map[string]bool{ + "localhost": true, + "stillbox": true, + }, + }, + Alerting: Alerting{ + Enable: true, + LookbackDays: 7, + HalfLife: jsontypes.Duration(30*time.Minute), + Recent: jsontypes.Duration(2*time.Hour), + AlertThreshold: 0.3, + Renotify: common.PtrTo(jsontypes.Duration(30*time.Minute)), + }, + Log: []Logger{ + Logger{ + Level: common.PtrTo("debug"), + }, + Logger{ + Level: common.PtrTo("error"), + File: common.PtrTo("error.log"), + }, + }, + Listen: ":3051", + Public: true, + RateLimit: RateLimit{ + Enable: true, + Requests: 200, + Over: 2*time.Minute, + }, + Notify: Notify{ + NotifyService{ + Provider: "slackwebhook", + Config: map[string]interface{}{ + "webhookURL": "https://hook", + }, + }, + }, + Relay: []Relay{ + Relay{ + URL: "http://relay", + APIKey: "secret", + Required: true, + }, + }, + +} + +func TestConfigParse(t *testing.T) { + c := &Configuration{configPath: "testdata/testconfig.yaml"} + + err := c.read() + require.NoError(t, err) + + assert.Equal(t, expCfg, &c.Config) +} diff --git a/pkg/config/testdata/testconfig.yaml b/pkg/config/testdata/testconfig.yaml new file mode 100644 index 0000000..e64ee1f --- /dev/null +++ b/pkg/config/testdata/testconfig.yaml @@ -0,0 +1,38 @@ +db: + connect: 'postgres://stillbox:somepassword@stillbox:5432/stillbox?sslmode=disable' + logQueries: true +cors: + allowedOrigins: + - 'http://localhost:*' +auth: + jwtsecret: 'somesecret' + domain: xenon + allowInsecureFor: + "localhost": true + "stillbox": true +listen: ':3051' +public: true +log: + - level: debug + - level: error + file: error.log +rateLimit: + enable: true + requests: 200 + over: 2m +alerting: + enable: true + lookbackDays: 7 + halfLife: 30m + recent: 2h + alertThreshold: 0.3 + renotify: 30m +notify: + - provider: slackwebhook + config: + webhookURL: "https://hook" +relay: + - url: "http://relay" + apiKey: "secret" + required: true + diff --git a/pkg/server/server.go b/pkg/server/server.go index b14778f..57a9048 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -27,7 +27,7 @@ const shutdownTimeout = 5 * time.Second type Server struct { auth *auth.Auth - conf *config.Config + conf *config.Configuration db database.Store r *chi.Mux sources sources.Sources @@ -42,7 +42,7 @@ type Server struct { rest rest.API } -func New(ctx context.Context, cfg *config.Config) (*Server, error) { +func New(ctx context.Context, cfg *config.Configuration) (*Server, error) { logger, err := NewLogger(cfg.Log) if err != nil { return nil, err diff --git a/pkg/server/signals.go b/pkg/server/signals.go index 267be3d..b3a60a8 100644 --- a/pkg/server/signals.go +++ b/pkg/server/signals.go @@ -34,7 +34,7 @@ func (s *Server) installHupHandler() { hs := s.huppers() for _, h := range hs { - h.HUP(s.conf) + h.HUP(&s.conf.Config) } } }() From f1e65aaa6826c65417c629d1ec10f7da20773f6f Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 24 Nov 2024 09:10:31 -0500 Subject: [PATCH 3/6] Switch to koanf --- go.mod | 8 ++++++++ go.sum | 16 +++++++++++++++ internal/common/common.go | 6 +++++- internal/jsontypes/jsontime.go | 11 +++++++++++ pkg/config/parse.go | 36 +++++++++++++++++++++++++++++----- pkg/config/parse_test.go | 31 ++++++++++++++--------------- 6 files changed, 86 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index cb2b871..0f36159 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,10 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/hajimehoshi/oto v1.0.1 github.com/jackc/pgx/v5 v5.7.1 + github.com/knadh/koanf/parsers/yaml v0.1.0 + github.com/knadh/koanf/providers/env v1.0.0 + github.com/knadh/koanf/providers/file v1.1.2 + github.com/knadh/koanf/v2 v2.1.2 github.com/nikoksr/notify v1.1.0 github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 @@ -34,6 +38,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-audio/audio v1.0.0 // indirect github.com/go-audio/riff v1.0.0 // indirect github.com/goccy/go-json v0.10.3 // indirect @@ -44,6 +49,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect @@ -52,6 +58,8 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum index a99b121..126273f 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= @@ -85,6 +87,16 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0= +github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= +github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= +github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= +github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -110,6 +122,10 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= diff --git a/internal/common/common.go b/internal/common/common.go index 7e5f8cb..7eb0e70 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -4,7 +4,11 @@ import ( "github.com/spf13/cobra" ) -const AppName = "stillbox" +const ( + AppName = "stillbox" + EnvPrefix = "STILLBOX_" +) + const ( TimeFormat = "Jan 2 15:04:05" diff --git a/internal/jsontypes/jsontime.go b/internal/jsontypes/jsontime.go index 03880e0..8c2f164 100644 --- a/internal/jsontypes/jsontime.go +++ b/internal/jsontypes/jsontime.go @@ -68,6 +68,17 @@ func (d *Duration) UnmarshalYAML(n *yaml.Node) error { return nil } +func (d *Duration) UnmarshalText(text []byte) error { + dur, err := time.ParseDuration(string(text)) + if err != nil { + return err + } + + *d = Duration(dur) + + return nil +} + func (d *Duration) UnmarshalJSON(b []byte) error { s := strings.Trim(string(b), `"`) dur, err := time.ParseDuration(s) diff --git a/pkg/config/parse.go b/pkg/config/parse.go index 01f90fa..e8bff5a 100644 --- a/pkg/config/parse.go +++ b/pkg/config/parse.go @@ -1,11 +1,19 @@ package config import ( - "os" + "fmt" + "strings" + "dynatron.me/x/stillbox/internal/common" + + "github.com/go-viper/mapstructure/v2" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" + // "github.com/knadh/koanf/providers/posflag" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" ) func (c *Configuration) PreRunE() func(*cobra.Command, []string) error { @@ -29,14 +37,32 @@ func (c *Configuration) ReadConfig() error { } func (c *Configuration) read() error { - cfgBytes, err := os.ReadFile(c.configPath) + k := koanf.New(".") + err := k.Load(file.Provider(c.configPath), yaml.Parser()) if err != nil { return err } - err = yaml.Unmarshal(cfgBytes, &c.Config) + k.Load(env.Provider(common.EnvPrefix, ".", func(s string) string { + return strings.Replace(strings.ToLower( + strings.TrimPrefix(s, common.EnvPrefix)), "_", ".", -1) + }), nil) + + err = k.UnmarshalWithConf("", &c.Config, + koanf.UnmarshalConf{ + Tag: "yaml", + DecoderConfig: &mapstructure.DecoderConfig{ + Result: &c.Config, + WeaklyTypedInput: true, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.TextUnmarshallerHookFunc(), + ), + }, + }) + if err != nil { - return err + return fmt.Errorf("unmarshal err: %w", err) } return nil diff --git a/pkg/config/parse_test.go b/pkg/config/parse_test.go index e286650..6107d96 100644 --- a/pkg/config/parse_test.go +++ b/pkg/config/parse_test.go @@ -7,35 +7,35 @@ import ( "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/jsontypes" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var expCfg = &Config{ DB: DB{ - Connect: "postgres://stillbox:somepassword@stillbox:5432/stillbox?sslmode=disable", + Connect: "postgres://stillbox:somepassword@stillbox:5432/stillbox?sslmode=disable", LogQueries: true, }, CORS: CORS{ AllowedOrigins: []string{ "http://localhost:*", -}, + }, }, Auth: Auth{ JWTSecret: "somesecret", - Domain: "xenon", + Domain: "xenon", AllowInsecure: map[string]bool{ "localhost": true, - "stillbox": true, + "stillbox": true, }, }, Alerting: Alerting{ - Enable: true, - LookbackDays: 7, - HalfLife: jsontypes.Duration(30*time.Minute), - Recent: jsontypes.Duration(2*time.Hour), + Enable: true, + LookbackDays: 7, + HalfLife: jsontypes.Duration(30 * time.Minute), + Recent: jsontypes.Duration(2 * time.Hour), AlertThreshold: 0.3, - Renotify: common.PtrTo(jsontypes.Duration(30*time.Minute)), + Renotify: common.PtrTo(jsontypes.Duration(30 * time.Minute)), }, Log: []Logger{ Logger{ @@ -43,15 +43,15 @@ var expCfg = &Config{ }, Logger{ Level: common.PtrTo("error"), - File: common.PtrTo("error.log"), + File: common.PtrTo("error.log"), }, }, Listen: ":3051", Public: true, RateLimit: RateLimit{ - Enable: true, + Enable: true, Requests: 200, - Over: 2*time.Minute, + Over: 2 * time.Minute, }, Notify: Notify{ NotifyService{ @@ -63,12 +63,11 @@ var expCfg = &Config{ }, Relay: []Relay{ Relay{ - URL: "http://relay", - APIKey: "secret", + URL: "http://relay", + APIKey: "secret", Required: true, }, }, - } func TestConfigParse(t *testing.T) { From 4bee6840dc2e60b25bfcb79c987f923224966021 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 24 Nov 2024 10:38:19 -0500 Subject: [PATCH 4/6] Move to urfave/cli --- cmd/stillbox/main.go | 58 ++++++++++++++++++-------- go.mod | 7 ++-- go.sum | 14 +++---- internal/common/common.go | 21 ++++------ pkg/cmd/admin/admin.go | 85 ++++++++++++++++++++++++--------------- pkg/cmd/serve/serve.go | 15 ++++--- pkg/config/config.go | 2 +- pkg/config/parse.go | 25 +++++------- 8 files changed, 130 insertions(+), 97 deletions(-) diff --git a/cmd/stillbox/main.go b/cmd/stillbox/main.go index b32a0cd..32eec62 100644 --- a/cmd/stillbox/main.go +++ b/cmd/stillbox/main.go @@ -13,28 +13,52 @@ import ( "dynatron.me/x/stillbox/pkg/cmd/serve" "dynatron.me/x/stillbox/pkg/config" - "github.com/spf13/cobra" + "github.com/urfave/cli/v2" ) +const DefaultConfig = "config.yaml" + func main() { + configFile := DefaultConfig log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: common.TimeFormat}) - rootCmd := &cobra.Command{ - Use: common.AppName, - } - rootCmd.PersistentFlags().BoolP("version", "V", false, "show version") - cfg := config.New(rootCmd) - rootCmd.PreRun = func(cmd *cobra.Command, args []string) { - v, _ := rootCmd.PersistentFlags().GetBool("version") - if v { - fmt.Print(version.String()) - os.Exit(0) - } + cfg := config.New(&configFile) + app := &cli.App{ + Name: common.AppName, + Usage: "a scanner call server", + UseShortOptionHandling: true, + Before: cfg.Before, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Value: DefaultConfig, + Usage: "configuration file", + Destination: &configFile, + Aliases: []string{"c"}, + }, + &cli.BoolFlag{ + Name: "version", + Aliases: []string{"V"}, + Action: func(_ *cli.Context, v bool) error { + if v { + fmt.Print(version.String()) + os.Exit(0) + } + + return nil + }, + DisableDefaultText: true, + }, + }, + Commands: []*cli.Command{ + serve.Command(cfg), + admin.Command(cfg), + }, } - cmds := append([]*cobra.Command{serve.Command(cfg)}, admin.Command(cfg)...) - rootCmd.AddCommand(cmds...) - - // cobra is already checking for errors and will print them - _ = rootCmd.Execute() + err := app.Run(os.Args) + if err != nil { + os.Stderr.Write([]byte("Error: " + err.Error() + "\n")) + os.Exit(1) + } } diff --git a/go.mod b/go.mod index 0f36159..47af3ec 100644 --- a/go.mod +++ b/go.mod @@ -23,9 +23,9 @@ require ( github.com/knadh/koanf/v2 v2.1.2 github.com/nikoksr/notify v1.1.0 github.com/rs/zerolog v1.33.0 - github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 + github.com/urfave/cli/v2 v2.27.5 golang.org/x/crypto v0.29.0 golang.org/x/sync v0.9.0 golang.org/x/term v0.26.0 @@ -36,6 +36,7 @@ require ( require ( github.com/ajg/form v1.5.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect @@ -44,7 +45,6 @@ require ( github.com/goccy/go-json v0.10.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -61,9 +61,10 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/segmentio/asm v1.2.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/exp/shiny v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/image v0.22.0 // indirect diff --git a/go.sum b/go.sum index 126273f..c0df874 100644 --- a/go.sum +++ b/go.sum @@ -11,7 +11,8 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoU github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -73,8 +74,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -148,14 +147,11 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= @@ -167,6 +163,10 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI= github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= diff --git a/internal/common/common.go b/internal/common/common.go index 7eb0e70..ea19e5f 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -1,38 +1,31 @@ package common import ( - "github.com/spf13/cobra" + "github.com/urfave/cli/v2" ) const ( - AppName = "stillbox" + AppName = "stillbox" EnvPrefix = "STILLBOX_" ) - const ( TimeFormat = "Jan 2 15:04:05" ) type cmdOptions interface { - Options(*cobra.Command, []string) error + Options(*cli.Context) error Execute() error } -func RunE(c cmdOptions) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - err := c.Options(cmd, args) +func Action(c cmdOptions) cli.ActionFunc { + return func(ctx *cli.Context) error { + err := c.Options(ctx) if err != nil { - cmd.SilenceUsage = true return err } - err = c.Execute() - if err != nil { - cmd.SilenceUsage = true - } - - return err + return c.Execute() } } diff --git a/pkg/cmd/admin/admin.go b/pkg/cmd/admin/admin.go index d515bf4..8a0aae8 100644 --- a/pkg/cmd/admin/admin.go +++ b/pkg/cmd/admin/admin.go @@ -8,7 +8,7 @@ import ( "dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/database" - "github.com/spf13/cobra" + "github.com/urfave/cli/v2" "golang.org/x/crypto/bcrypt" "golang.org/x/term" ) @@ -115,59 +115,78 @@ func readPassword(prompt string) (string, error) { } // Command is the users command. -func Command(cfg *config.Configuration) []*cobra.Command { - userCmd := &cobra.Command{ - Use: "users", - Aliases: []string{"u"}, - Short: "administers the server", - PersistentPreRunE: cfg.PreRunE(), +func Command(cfg *config.Configuration) *cli.Command { + c := &cfg.Config + userCmd := &cli.Command{ + Name: "users", + Aliases: []string{"u"}, + Usage: "administers users", + Subcommands: []*cli.Command{ + addUserCommand(c), + passwdCommand(c), + }, } - userCmd.AddCommand(addUserCommand(&cfg.Config), passwdCommand(&cfg.Config)) - return []*cobra.Command{userCmd} + return userCmd } -func addUserCommand(cfg *config.Config) *cobra.Command { - c := &cobra.Command{ - Use: "add", - Short: "adds a user", - RunE: func(cmd *cobra.Command, args []string) error { +func addUserCommand(cfg *config.Config) *cli.Command { + c := &cli.Command{ + Name: "add", + Description: "adds a user", + UsageText: "stillbox users add [-a] [-m email] [username]", + Args: true, + Action: func(ctx *cli.Context) error { + if ctx.Args().Len() != 1 { + return errors.New(ctx.Command.Usage) + } + db, err := database.NewClient(context.Background(), cfg.DB) if err != nil { return err } - username := args[0] - isAdmin, err := cmd.Flags().GetBool("admin") - if err != nil { - return err - } - email, err := cmd.Flags().GetString("email") - if err != nil { - return err - } + username := ctx.Args().Get(0) + isAdmin := ctx.Bool("admin") + email := ctx.String("email") return AddUser(database.CtxWithDB(context.Background(), db), username, email, isAdmin) }, - Args: cobra.ExactArgs(1), + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "admin", + Aliases: []string{"a"}, + Value: false, + Usage: "user is an admin", + }, + &cli.StringFlag{ + Name: "email", + Usage: "email address", + Aliases: []string{"m"}, + }, + }, } - c.Flags().BoolP("admin", "a", false, "is admin") - c.Flags().StringP("email", "m", "", "email address") return c } -func passwdCommand(cfg *config.Config) *cobra.Command { - c := &cobra.Command{ - Use: "passwd userid", - Short: "changes password for a user", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { +func passwdCommand(cfg *config.Config) *cli.Command { + c := &cli.Command{ + Name: "passwd", + Usage: "changes password for a user", + UsageText: "stillbox users passwd [username]", + Args: true, + Action: func(ctx *cli.Context) error { + if ctx.Args().Len() != 1 { + return errors.New(ctx.Command.Usage) + } + db, err := database.NewClient(context.Background(), cfg.DB) if err != nil { return err } - username := args[0] + username := ctx.Args().Get(0) + return Passwd(database.CtxWithDB(context.Background(), db), username) }, } diff --git a/pkg/cmd/serve/serve.go b/pkg/cmd/serve/serve.go index f5ea062..c1d2304 100644 --- a/pkg/cmd/serve/serve.go +++ b/pkg/cmd/serve/serve.go @@ -9,20 +9,19 @@ import ( "dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/server" - "github.com/spf13/cobra" + "github.com/urfave/cli/v2" ) type ServeOptions struct { cfg *config.Configuration } -func Command(cfg *config.Configuration) *cobra.Command { +func Command(cfg *config.Configuration) *cli.Command { opts := makeOptions(cfg) - serveCmd := &cobra.Command{ - Use: "serve", - Short: "starts the" + common.AppName + " server", - PersistentPreRunE: cfg.PreRunE(), - RunE: common.RunE(opts), + serveCmd := &cli.Command{ + Name: "serve", + Usage: "starts the " + common.AppName + " server", + Action: common.Action(opts), } return serveCmd @@ -34,7 +33,7 @@ func makeOptions(cfg *config.Configuration) *ServeOptions { } } -func (o *ServeOptions) Options(_ *cobra.Command, args []string) error { +func (o *ServeOptions) Options(_ *cli.Context) error { return nil } diff --git a/pkg/config/config.go b/pkg/config/config.go index e934d06..7d67a9b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -12,7 +12,7 @@ import ( type Configuration struct { Config - configPath string `yaml:"-"` + configPath *string `yaml:"-"` } type Config struct { diff --git a/pkg/config/parse.go b/pkg/config/parse.go index e8bff5a..f8e9ac0 100644 --- a/pkg/config/parse.go +++ b/pkg/config/parse.go @@ -11,34 +11,31 @@ import ( "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/v2" - // "github.com/knadh/koanf/providers/posflag" "github.com/rs/zerolog/log" - "github.com/spf13/cobra" + "github.com/urfave/cli/v2" ) -func (c *Configuration) PreRunE() func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - return c.ReadConfig() +func New(configFile *string) *Configuration { + if configFile == nil { + panic("configFile must not be nil") } + + return &Configuration{configPath: configFile} } -func New(rootCommand *cobra.Command) *Configuration { - c := &Configuration{} - - rootCommand.PersistentFlags().StringVarP(&c.configPath, "config", "c", "config.yaml", "configuration file") - - return c +func (c *Configuration) Before(ctx *cli.Context) error { + return c.ReadConfig() } func (c *Configuration) ReadConfig() error { - log.Info().Str("configPath", c.configPath).Msg("read config") + log.Info().Str("configPath", *c.configPath).Msg("read config") return c.read() } func (c *Configuration) read() error { k := koanf.New(".") - err := k.Load(file.Provider(c.configPath), yaml.Parser()) + err := k.Load(file.Provider(*c.configPath), yaml.Parser()) if err != nil { return err } @@ -52,7 +49,7 @@ func (c *Configuration) read() error { koanf.UnmarshalConf{ Tag: "yaml", DecoderConfig: &mapstructure.DecoderConfig{ - Result: &c.Config, + Result: &c.Config, WeaklyTypedInput: true, DecodeHook: mapstructure.ComposeDecodeHookFunc( mapstructure.StringToTimeDurationHookFunc(), From b9a00e1bdcd3cd2e8923cc90b3fb9a69156152e9 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 24 Nov 2024 15:45:29 -0500 Subject: [PATCH 5/6] Fix config test --- pkg/config/parse_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/config/parse_test.go b/pkg/config/parse_test.go index 6107d96..59b2396 100644 --- a/pkg/config/parse_test.go +++ b/pkg/config/parse_test.go @@ -62,7 +62,7 @@ var expCfg = &Config{ }, }, Relay: []Relay{ - Relay{ + { URL: "http://relay", APIKey: "secret", Required: true, @@ -71,7 +71,7 @@ var expCfg = &Config{ } func TestConfigParse(t *testing.T) { - c := &Configuration{configPath: "testdata/testconfig.yaml"} + c := &Configuration{configPath: common.PtrTo("testdata/testconfig.yaml")} err := c.read() require.NoError(t, err) From 4453114053d7b4222cddac32c1eb129fb5098686 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 24 Nov 2024 16:03:17 -0500 Subject: [PATCH 6/6] Adjust content-length --- internal/forms/testdata/urlenc3.http | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/forms/testdata/urlenc3.http b/internal/forms/testdata/urlenc3.http index 854231b..79b96ab 100644 --- a/internal/forms/testdata/urlenc3.http +++ b/internal/forms/testdata/urlenc3.http @@ -2,7 +2,7 @@ POST /api/talkgroup/ HTTP/1.1 Host: xenon:3051 User-Agent: curl/8.10.1 Accept: */* -Content-Length: 16 +Content-Length: 27 Content-Type: application/x-www-form-urlencoded page=1&perPage=2&orderBy=id