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) {