From 72f7278f79528db8bf82e6f5fb0c918ae2e4d30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ma=C5=A1ek?= Date: Sun, 29 Dec 2024 23:10:09 +0100 Subject: [PATCH 1/3] wip --- {monitor => config}/config.default.yaml | 0 {monitor => config}/config.go | 15 ++++++++++++++- {monitor => config}/config_test.go | 2 +- {monitor => config}/service_config.go | 2 +- {monitor => config}/service_config_test.go | 11 ++++++++++- 5 files changed, 26 insertions(+), 4 deletions(-) rename {monitor => config}/config.default.yaml (100%) rename {monitor => config}/config.go (88%) rename {monitor => config}/config_test.go (98%) rename {monitor => config}/service_config.go (99%) rename {monitor => config}/service_config_test.go (74%) diff --git a/monitor/config.default.yaml b/config/config.default.yaml similarity index 100% rename from monitor/config.default.yaml rename to config/config.default.yaml diff --git a/monitor/config.go b/config/config.go similarity index 88% rename from monitor/config.go rename to config/config.go index 47a0076..da93b92 100644 --- a/monitor/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -package monitor +package config import ( _ "embed" @@ -11,8 +11,13 @@ import ( "strings" "github.com/spf13/viper" + "gopkg.in/yaml.v3" ) +type Config struct { + settings map[string]interface{} +} + //go:embed config.default.yaml var DEFAULT_CONFIG []byte @@ -101,6 +106,14 @@ func setupConfig(config *viper.Viper) (*viper.Viper, error) { if err != nil { return nil, err } + data, err := os.ReadFile(config.ConfigFileUsed()) + if err != nil { + return nil, err + } + config2 := &Config{settings: make(map[string]interface{})} + err = yaml.Unmarshal(data, config2.settings) + log.Println(">>>>", config2, "<<<<") + fmt.Printf("Config: %+v\n", config2.settings) // TODO: refactor // why does not Go have sets? :/ diff --git a/monitor/config_test.go b/config/config_test.go similarity index 98% rename from monitor/config_test.go rename to config/config_test.go index 1b5d427..0877f11 100644 --- a/monitor/config_test.go +++ b/config/config_test.go @@ -1,4 +1,4 @@ -package monitor +package config import ( "os" diff --git a/monitor/service_config.go b/config/service_config.go similarity index 99% rename from monitor/service_config.go rename to config/service_config.go index 607c827..aaa1570 100644 --- a/monitor/service_config.go +++ b/config/service_config.go @@ -1,4 +1,4 @@ -package monitor +package config import ( "fmt" diff --git a/monitor/service_config_test.go b/config/service_config_test.go similarity index 74% rename from monitor/service_config_test.go rename to config/service_config_test.go index 3e29464..cacf9b1 100644 --- a/monitor/service_config_test.go +++ b/config/service_config_test.go @@ -1,4 +1,4 @@ -package monitor +package config import ( "path/filepath" @@ -31,4 +31,13 @@ func TestLoadExampleConfig(t *testing.T) { assert.Nil(t, services["example-basic-web"].BodyContent) assert.Equal(t, services["example-temp-disable"].Enabled, false) + + // Service without config should still be included. + // Viper does not handle this well: + // - https://github.com/spf13/viper/issues/406 + // Possible alternatives: + // - https://github.com/knadh/koanf + // - roll own config + require.Contains(t, services, "beacon-periodic-checker") + assert.Equal(t, services["beacon-periodic-checker"].Enabled, true) } From 36b88a0edea9cf4f8044a4defabf536e0123e80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ma=C5=A1ek?= Date: Wed, 1 Jan 2025 20:06:37 +0100 Subject: [PATCH 2/3] wip, go-tests passing but the config reading/setting is not correect --- README-dev.md | 11 +- cmd/config.go | 9 +- {config => conf}/config.default.yaml | 0 conf/config.go | 270 ++++++++++++++++++++++++ {config => conf}/config_test.go | 10 +- {config => conf}/service_config.go | 19 +- {config => conf}/service_config_test.go | 2 +- config/config.go | 136 ------------ go.mod | 25 +-- go.sum | 63 +----- handlers/mail.go | 6 +- handlers/report.go | 6 +- handlers/server.go | 9 +- monitor/web_pinger.go | 13 -- scheduler/scheduler.go | 18 +- scheduler/scheduler_test.go | 6 +- storage/storage.go | 2 +- tests/e2e_test.go | 4 +- 18 files changed, 325 insertions(+), 284 deletions(-) rename {config => conf}/config.default.yaml (100%) create mode 100644 conf/config.go rename {config => conf}/config_test.go (79%) rename {config => conf}/service_config.go (89%) rename {config => conf}/service_config_test.go (98%) delete mode 100644 config/config.go diff --git a/README-dev.md b/README-dev.md index a210d81..e918f19 100644 --- a/README-dev.md +++ b/README-dev.md @@ -94,9 +94,8 @@ Some design choices: - for creating new data there is HealthCheckInput - currently same as HealthCheck without ID, in future possibly different - naming conventions: - ID will be lowercased when used in variable name - FooId - to follow CamelCaseNaming -- dependency chain / architecture: - - storage < monitor < handlers < cmd - - storage (DB) is the base, handles persistence, should depend on nothing (nothing internal, can depend e.g. on SQLite) +- modules: + - storage (DB) is the base, handles persistence - monitors interact with the outside world and store health checks to DB - handlers - take data from DB and do something with it @@ -104,5 +103,7 @@ Some design choices: - send notifications - cmd - entrypoints - - can depend on anything (apart from each other) - - should be simple and high-level + - should be simple, only wrap existing functionality + - conf + - store/load configuration + - name chosen to prevent naming variables `config` (not super happy about naming here) \ No newline at end of file diff --git a/cmd/config.go b/cmd/config.go index 2b5776e..dba298c 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -4,22 +4,21 @@ import ( "log" "strings" - "github.com/davidmasek/beacon/monitor" + "github.com/davidmasek/beacon/conf" "github.com/spf13/cobra" - "github.com/spf13/viper" ) -func loadConfig(cmd *cobra.Command) (*viper.Viper, error) { +func loadConfig(cmd *cobra.Command) (*conf.Config, error) { configFile, err := cmd.Flags().GetString("config") if err != nil { return nil, err } if configFile != "" { - return monitor.DefaultConfigFrom(configFile) + return conf.DefaultConfigFrom(configFile) } - config, err := monitor.DefaultConfig() + config, err := conf.DefaultConfig() // TODO: quick fix to enable start when no config file found // default one should be created instead if err != nil && strings.Contains(err.Error(), `Config File "beacon.yaml" Not Found in`) { diff --git a/config/config.default.yaml b/conf/config.default.yaml similarity index 100% rename from config/config.default.yaml rename to conf/config.default.yaml diff --git a/conf/config.go b/conf/config.go new file mode 100644 index 0000000..32c4b0e --- /dev/null +++ b/conf/config.go @@ -0,0 +1,270 @@ +package conf + +import ( + _ "embed" + "errors" + "fmt" + "io/fs" + "log" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +// prefix for environment variables +const ENV_VAR_PREFIX = "BEACON_" + +type Config struct { + envPrefix string + settings map[string]interface{} + // manually set, should take precedence + overrides map[string]interface{} +} + +func (config *Config) AllSettings() map[string]interface{} { + return config.settings +} + +func (config *Config) keyToEnvVar(key string) string { + key = strings.ReplaceAll(key, ".", "_") + key = strings.ToUpper(key) + return config.envPrefix + key +} + +func (config *Config) get(key string) interface{} { + // TODO: dot notation for nested access not implemented + val, ok := config.overrides[key] + if ok { + return val + } + // overwrite with ENV var if available + envVal, isSet := os.LookupEnv(config.keyToEnvVar(key)) + if isSet { + return envVal + } + val, ok = config.settings[key] + // TODO: do we want default or nil? + if !ok { + return nil + } + return val +} + +func (config *Config) GetString(key string) string { + val := config.get(key) + strVal, ok := val.(string) + if ok { + return strVal + } + return fmt.Sprint(val) +} + +func (config *Config) GetInt(key string) int { + val := config.get(key) + intVal, ok := val.(int) + if ok { + return intVal + } + strVal, ok := val.(string) + if !ok { + panic(fmt.Sprintf("For key %q - cannot convert %q to int", key, val)) + } + intVal, err := strconv.Atoi(strVal) + if err != nil { + panic(fmt.Sprintf("For key %q - cannot convert %q to int", key, val)) + } + return intVal +} + +var boolyStrings = map[string]bool{ + "true": true, + "1": true, + "TRUE": true, + "false": false, + "0": false, + "FALSE": false, +} + +func (config *Config) GetBool(key string) bool { + val := config.settings[key] + boolVal, isBool := val.(bool) + if isBool { + return boolVal + } + strVal, isString := val.(string) + if !isString { + panic(fmt.Sprintf("Cannot parse key %q with value %q as bool", key, config.settings[key])) + } + boolVal, isExpectedFormat := boolyStrings[strVal] + if !isExpectedFormat { + panic(fmt.Sprintf("Cannot parse key %q with value %q as bool", key, config.settings[key])) + } + return boolVal +} + +func (config *Config) GetDuration(key string) time.Duration { + value := config.get(key) + durationValue, isDuration := value.(time.Duration) + if isDuration { + return durationValue + } + parsedValue, err := time.ParseDuration(value.(string)) + if err != nil { + panic(fmt.Sprintf("Cannot parse %q as time.Duration", value)) + } + return parsedValue +} + +func (config *Config) Set(key string, value interface{}) { + // TODO: dot notation for nested access not implemented + config.overrides[key] = value +} + +func (config *Config) SetDefault(key string, value interface{}) { + if !config.IsSet(key) { + config.settings[key] = value + } +} + +func (config *Config) IsSet(key string) bool { + _, ok := config.overrides[key] + if ok { + return true + } + _, isSet := os.LookupEnv(config.keyToEnvVar(key)) + if isSet { + return true + } + _, ok = config.settings[key] + return ok +} + +func subSettings(parent map[string]interface{}, key string) map[string]interface{} { + val, ok := parent[key] + if !ok { + return map[string]interface{}{} + } + child, ok := val.(map[string]interface{}) + // if config[key] is not a dict, it is a scalar + // to keep the same structure we convert it to map + // with the scalar as key and no value + // todo: maybe better design could be pondered here + if !ok { + child = make(map[string]interface{}) + child[val.(string)] = struct{}{} + } + return child +} + +func (config *Config) Sub(key string) *Config { + // TODO: doing a quick implementation here + // either needs more testing or remove this method + sub := &Config{} + sub.envPrefix = config.envPrefix + strings.ToUpper(key) + "_" + sub.overrides = subSettings(config.overrides, key) + sub.settings = subSettings(config.settings, key) + + return sub +} + +//go:embed config.default.yaml +var DEFAULT_CONFIG []byte + +func ensureConfigFile(path string) error { + _, err := os.Stat(path) + if errors.Is(err, fs.ErrNotExist) { + err = os.WriteFile(path, DEFAULT_CONFIG, 0644) + return err + } + return err +} + +// Load config file from home dir (such as `~/beacon.yaml`). +// +// Create config file if not found. +// Setup config to use env variables. +func DefaultConfig() (*Config, error) { + homedir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + configFile := filepath.Join(homedir, "beacon.yaml") + err = ensureConfigFile(configFile) + if err != nil { + return nil, err + } + return DefaultConfigFrom(configFile) +} + +// Load config file from `config.sample.yaml`. Useful for testing. +// +// Fail if example config file not found. +// Setup config to use env variables. +func ExampleConfig() (*Config, error) { + // test can be run with different working dir + locations := []string{ + filepath.Join("config.sample.yaml"), + filepath.Join("..", "config.sample.yaml"), + } + for _, loc := range locations { + _, err := os.Stat(loc) + if errors.Is(err, fs.ErrNotExist) { + continue + } + if err != nil { + return nil, err + } + return DefaultConfigFrom(loc) + } + return nil, fmt.Errorf("config.sample.yaml file not found") +} + +// Load config file from the specified path. +// +// Create config file if not found. +// Setup config to use env variables. +func DefaultConfigFrom(configFile string) (*Config, error) { + err := ensureConfigFile(configFile) + if err != nil { + return nil, err + } + return setupConfig(configFile) +} + +// Empty config +func NewConfig() *Config { + config := &Config{ + envPrefix: ENV_VAR_PREFIX, + settings: make(map[string]interface{}), + overrides: make(map[string]interface{}), + } + return config +} + +// Minimal working config +func BaseConfig() *Config { + config := NewConfig() + config.settings["services"] = []string{} + config.settings["email"] = map[string]string{} + return config +} + +// Setup config to use ENV variables and read specified config file. +func setupConfig(configFile string) (*Config, error) { + log.Printf("reading config from %q\n", configFile) + data, err := os.ReadFile(configFile) + if err != nil { + return nil, err + } + config := NewConfig() + err = yaml.Unmarshal(data, config.settings) + if err != nil { + return nil, err + } + log.Println(">>>>", config, "<<<<") + return config, err +} diff --git a/config/config_test.go b/conf/config_test.go similarity index 79% rename from config/config_test.go rename to conf/config_test.go index 0877f11..83687fd 100644 --- a/config/config_test.go +++ b/conf/config_test.go @@ -1,4 +1,4 @@ -package config +package conf import ( "os" @@ -15,6 +15,9 @@ func TestLoadConfigFrom(t *testing.T) { config, err := DefaultConfigFrom(exampleConfigFile) require.NoError(t, err) require.NotNil(t, config) + + require.True(t, config.IsSet("services"), config) + require.True(t, config.IsSet("email"), config) } func TestEnvVariablesOverwrite(t *testing.T) { @@ -29,8 +32,9 @@ func TestEnvVariablesOverwrite(t *testing.T) { require.NoError(t, err) require.NotNil(t, config) + require.True(t, config.IsSet("email"), config) emailConfig := config.Sub("email") t.Log(emailConfig.AllSettings()) - assert.Equal(t, "my-new-prefix", emailConfig.GetString("prefix")) - assert.Equal(t, 123, emailConfig.GetInt("smtp_port")) + assert.Equal(t, "my-new-prefix", emailConfig.GetString("prefix"), emailConfig) + assert.Equal(t, 123, emailConfig.GetInt("smtp_port"), emailConfig) } diff --git a/config/service_config.go b/conf/service_config.go similarity index 89% rename from config/service_config.go rename to conf/service_config.go index aaa1570..3621f6e 100644 --- a/config/service_config.go +++ b/conf/service_config.go @@ -1,10 +1,8 @@ -package config +package conf import ( "fmt" "time" - - "github.com/spf13/viper" ) type ServiceConfig struct { @@ -110,15 +108,16 @@ func (sc *ServiceConfig) IsWebService() bool { return sc.Url != "" } -func ParseServicesConfig(servicesConfig *viper.Viper) (map[string]*ServiceConfig, error) { - inputs := make(map[string]map[string]interface{}) - err := servicesConfig.Unmarshal(&inputs) - if err != nil { - return nil, err - } +func ParseServicesConfig(servicesConfig *Config) (map[string]*ServiceConfig, error) { + inputs := servicesConfig.AllSettings() services := make(map[string]*ServiceConfig) - for id, input := range inputs { + for id, rawInput := range inputs { + input, ok := rawInput.(map[string]interface{}) + // TODO: is this the correct check for empty values? + if !ok { + input = make(map[string]interface{}) + } serviceConfig, err := NewServiceConfig(id, input) if err != nil { return nil, err diff --git a/config/service_config_test.go b/conf/service_config_test.go similarity index 98% rename from config/service_config_test.go rename to conf/service_config_test.go index cacf9b1..d9918e5 100644 --- a/config/service_config_test.go +++ b/conf/service_config_test.go @@ -1,4 +1,4 @@ -package config +package conf import ( "path/filepath" diff --git a/config/config.go b/config/config.go deleted file mode 100644 index da93b92..0000000 --- a/config/config.go +++ /dev/null @@ -1,136 +0,0 @@ -package config - -import ( - _ "embed" - "errors" - "fmt" - "io/fs" - "log" - "os" - "path/filepath" - "strings" - - "github.com/spf13/viper" - "gopkg.in/yaml.v3" -) - -type Config struct { - settings map[string]interface{} -} - -//go:embed config.default.yaml -var DEFAULT_CONFIG []byte - -func ensureConfigFile(path string) error { - _, err := os.Stat(path) - if errors.Is(err, fs.ErrNotExist) { - err = os.WriteFile(path, DEFAULT_CONFIG, 0644) - return err - } - return err -} - -// Load config file from home dir (such as `~/beacon.yaml`). -// -// Create config file if not found. -// Setup config to use env variables. -func DefaultConfig() (*viper.Viper, error) { - homedir, err := os.UserHomeDir() - if err != nil { - return nil, err - } - configFile := filepath.Join(homedir, "beacon.yaml") - err = ensureConfigFile(configFile) - if err != nil { - return nil, err - } - return DefaultConfigFrom(configFile) -} - -// Load config file from `config.sample.yaml`. Useful for testing. -// -// Fail if example config file not found. -// Setup config to use env variables. -func ExampleConfig() (*viper.Viper, error) { - // test can be run with different working dir - locations := []string{ - filepath.Join("config.sample.yaml"), - filepath.Join("..", "config.sample.yaml"), - } - for _, loc := range locations { - _, err := os.Stat(loc) - if errors.Is(err, fs.ErrNotExist) { - continue - } - if err != nil { - return nil, err - } - return DefaultConfigFrom(loc) - } - return nil, fmt.Errorf("config.sample.yaml file not found") -} - -// Load config file from the specified path. -// -// Create config file if not found. -// Setup config to use env variables. -func DefaultConfigFrom(configFile string) (*viper.Viper, error) { - config := viper.New() - config.SetConfigFile(configFile) - err := ensureConfigFile(configFile) - if err != nil { - return nil, err - } - return setupConfig(config) -} - -// Setup config to use ENV variables and read specified config file. -func setupConfig(config *viper.Viper) (*viper.Viper, error) { - config.SetEnvPrefix("BEACON") - // Bash doesn't allow dot in the environment variable name. - // Viper requires dot for nested variables. - // Use underscore and replace. - config.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - // The combination of the prefix + string replacer - // means that to overwrite config `email.smtp_port`, i.e. - // ```yaml - // email: - // smtp_port: 587 - // ``` - // you should use BEACON_EMAIL_SMTP_PORT key, e.g. - // BEACON_EMAIL_SMTP_PORT=123 - config.AutomaticEnv() - - err := config.ReadInConfig() - log.Printf("read config from %q\n", config.ConfigFileUsed()) - if err != nil { - return nil, err - } - data, err := os.ReadFile(config.ConfigFileUsed()) - if err != nil { - return nil, err - } - config2 := &Config{settings: make(map[string]interface{})} - err = yaml.Unmarshal(data, config2.settings) - log.Println(">>>>", config2, "<<<<") - fmt.Printf("Config: %+v\n", config2.settings) - - // TODO: refactor - // why does not Go have sets? :/ - keys := config.AllSettings() - expectedKeys := []string{ - "services", - "email", - } - for _, expected := range expectedKeys { - if _, exists := keys[expected]; !exists { - log.Printf("%q key not present in config", expected) - } - delete(keys, expected) - } - for key := range keys { - log.Printf("unexpected %q key present in config", key) - } - - return config, err -} diff --git a/go.mod b/go.mod index afebb17..5feadba 100644 --- a/go.mod +++ b/go.mod @@ -3,36 +3,19 @@ module github.com/davidmasek/beacon go 1.23.1 require ( - github.com/go-resty/resty/v2 v2.16.2 github.com/mattn/go-sqlite3 v1.14.23 github.com/spf13/cobra v1.8.1 - github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 + golang.org/x/crypto v0.31.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/net v0.27.0 // indirect golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 677e46b..faf27fc 100644 --- a/go.sum +++ b/go.sum @@ -1,92 +1,33 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= -github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 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/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/mail.go b/handlers/mail.go index 56fb62d..6ebd387 100644 --- a/handlers/mail.go +++ b/handlers/mail.go @@ -7,14 +7,14 @@ import ( "net/smtp" "strings" - "github.com/spf13/viper" + "github.com/davidmasek/beacon/conf" ) type SMTPMailer struct { Server SMTPServer } -func (sm SMTPMailer) Send(reports []ServiceReport, emailConfig *viper.Viper) error { +func (sm SMTPMailer) Send(reports []ServiceReport, emailConfig *conf.Config) error { var buffer bytes.Buffer log.Printf("[SMTPMailer] Generating report") @@ -56,7 +56,7 @@ type SMTPServer struct { } // Load the SMTP server details from config -func LoadServer(emailConfig *viper.Viper) (SMTPServer, error) { +func LoadServer(emailConfig *conf.Config) (SMTPServer, error) { return SMTPServer{ server: emailConfig.GetString("SMTP_SERVER"), port: emailConfig.GetString("SMTP_PORT"), diff --git a/handlers/report.go b/handlers/report.go index 64ea8bf..5c63d8f 100644 --- a/handlers/report.go +++ b/handlers/report.go @@ -7,8 +7,8 @@ import ( "strings" "time" + "github.com/davidmasek/beacon/conf" "github.com/davidmasek/beacon/storage" - "github.com/spf13/viper" ) func GenerateReport(db storage.Storage) ([]ServiceReport, error) { @@ -47,7 +47,7 @@ func GenerateReport(db storage.Storage) ([]ServiceReport, error) { return reports, nil } -func sendEmail(config *viper.Viper, reports []ServiceReport) error { +func sendEmail(config *conf.Config, reports []ServiceReport) error { if !config.IsSet("email") { err := fmt.Errorf("no email configuration provided") return err @@ -66,7 +66,7 @@ func sendEmail(config *viper.Viper, reports []ServiceReport) error { // Generate, save and send report. // // See ShouldReport to check if this task should be run. -func DoReportTask(db storage.Storage, config *viper.Viper, now time.Time) error { +func DoReportTask(db storage.Storage, config *conf.Config, now time.Time) error { reports, err := GenerateReport(db) if err != nil { return err diff --git a/handlers/server.go b/handlers/server.go index 1405542..62f99a8 100644 --- a/handlers/server.go +++ b/handlers/server.go @@ -5,21 +5,18 @@ import ( "log" "net/http" + "github.com/davidmasek/beacon/conf" "github.com/davidmasek/beacon/monitor" "github.com/davidmasek/beacon/storage" - "github.com/spf13/viper" ) -func StartServer(db storage.Storage, config *viper.Viper) (*http.Server, error) { +func StartServer(db storage.Storage, config *conf.Config) (*http.Server, error) { mux := http.NewServeMux() mux.HandleFunc("/{$}", handleIndex(db)) monitor.RegisterHeartbeatHandlers(db, mux) - - if config == nil { - config = viper.New() - } + // TODO: centralize defaults config.SetDefault("port", "8088") port := config.GetString("port") diff --git a/monitor/web_pinger.go b/monitor/web_pinger.go index 83649cb..742645c 100644 --- a/monitor/web_pinger.go +++ b/monitor/web_pinger.go @@ -1,7 +1,6 @@ package monitor import ( - "fmt" "log" "time" @@ -11,26 +10,14 @@ import ( "strings" "github.com/davidmasek/beacon/storage" - "github.com/spf13/viper" ) -type WebPinger struct{} - type WebConfig struct { Url string `mapstructure:"url"` HttpStatus []int `mapstructure:"status"` BodyContent []string `mapstructure:"content"` } -func (*WebPinger) Start(db storage.Storage, config *viper.Viper) error { - websites := make(map[string]WebConfig) - err := config.UnmarshalKey("websites", &websites) - if err != nil { - return fmt.Errorf("fatal error unmarshaling config file: %w", err) - } - return CheckWebsites(db, websites) -} - func CheckWebsites(db storage.Storage, websites map[string]WebConfig) error { for service, config := range websites { log.Println("Checking website", service) diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index ece38f6..4ca08cf 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -5,22 +5,18 @@ import ( "log" "time" + "github.com/davidmasek/beacon/conf" "github.com/davidmasek/beacon/handlers" "github.com/davidmasek/beacon/monitor" "github.com/davidmasek/beacon/storage" - "github.com/spf13/viper" ) -func ShouldCheckWebServices(db storage.Storage, config *viper.Viper, now time.Time) bool { - if !config.IsSet("services") { - log.Println("No services specified in config file - not checking any websites") - return false - } +func ShouldCheckWebServices(db storage.Storage, config *conf.Config, now time.Time) bool { // TODO - should follow some config or smth return true } -func CheckWebServices(db storage.Storage, services map[string]*monitor.ServiceConfig) error { +func CheckWebServices(db storage.Storage, services map[string]*conf.ServiceConfig) error { // TODO: using "legacy" approach to get this done quickly // should look into monitor.CheckWebsites refactor // and getting rid of WebConfig struct @@ -60,7 +56,7 @@ func abs(x int) int { // - 2024-12-29T14:31:26+01:00 // - 2024-12-29T05:31:26-08:00 // The date part will be ignored. -func ShouldReport(db storage.Storage, config *viper.Viper, query time.Time) (bool, error) { +func ShouldReport(db storage.Storage, config *conf.Config, query time.Time) (bool, error) { // TODO: this function could use more work, building minimal functionality now // Provide default REPORT_TIME as 17h of local time. @@ -102,12 +98,12 @@ func ShouldReport(db storage.Storage, config *viper.Viper, query time.Time) (boo return true, nil } -func RunSingle(db storage.Storage, config *viper.Viper, now time.Time) error { +func RunSingle(db storage.Storage, config *conf.Config, now time.Time) error { log.Println("Do scheduling work") var err error = nil if ShouldCheckWebServices(db, config, now) { - services, err := monitor.ParseServicesConfig(config.Sub("services")) + services, err := conf.ParseServicesConfig(config.Sub("services")) if err != nil { return err } @@ -134,7 +130,7 @@ func RunSingle(db storage.Storage, config *viper.Viper, now time.Time) error { // // Will not call run next job again until previous one returns, even // if specified interval passes. -func Start(ctx context.Context, db storage.Storage, config *viper.Viper) { +func Start(ctx context.Context, db storage.Storage, config *conf.Config) { config.SetDefault("SCHEDULER_PERIOD", "15m") checkInterval := config.GetDuration("SCHEDULER_PERIOD") log.Printf("Starting scheduler: run each %s\n", checkInterval) diff --git a/scheduler/scheduler_test.go b/scheduler/scheduler_test.go index 8bdc2b0..67958bd 100644 --- a/scheduler/scheduler_test.go +++ b/scheduler/scheduler_test.go @@ -6,14 +6,14 @@ import ( "testing" "time" - "github.com/davidmasek/beacon/monitor" + "github.com/davidmasek/beacon/conf" "github.com/davidmasek/beacon/storage" "github.com/stretchr/testify/require" ) func TestRunSingle(t *testing.T) { db := storage.NewTestDb(t) - config, err := monitor.ExampleConfig() + config, err := conf.ExampleConfig() require.NoError(t, err) tmp, err := os.CreateTemp("", "beacon-test-report-*.html") @@ -40,7 +40,7 @@ func TestRunSingle(t *testing.T) { func TestShouldReport(t *testing.T) { db := storage.NewTestDb(t) defer db.Close() - config, err := monitor.ExampleConfig() + config, err := conf.ExampleConfig() require.NoError(t, err) targetStr := "2024-12-29T05:31:26-08:00" diff --git a/storage/storage.go b/storage/storage.go index 7443d42..77b7409 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -313,7 +313,7 @@ func InitDB(dbPath string) (Storage, error) { // Always falling back to env var makes the path // rewritable independent on how config is loaded. // --- - // Alternatively we could pass config as usual with *viper.Viper + // Alternatively we could pass config as usual with *conf.Config // but if that would not be set to read from env variables // (due to an error) than we could modify the wrong database. // Due to high impact of a potential mistake we use diff --git a/tests/e2e_test.go b/tests/e2e_test.go index 388e986..400f075 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -8,9 +8,9 @@ import ( "testing" "time" + "github.com/davidmasek/beacon/conf" "github.com/davidmasek/beacon/handlers" "github.com/davidmasek/beacon/storage" - "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -30,7 +30,7 @@ func TestEndToEndHeartbeat(t *testing.T) { service_name := "heartbeat-monitor" - config := viper.New() + config := conf.NewConfig() // shouldn't be fixed, but at least it's different than the default serverPort := "9000" config.Set("port", serverPort) From 60f7b0b577c35c56aaecf1fdd0a7e3356fca58f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Ma=C5=A1ek?= Date: Thu, 2 Jan 2025 22:24:17 +0100 Subject: [PATCH 3/3] sq --- conf/config.go | 142 ++++++++++++++++++++++++++++---------------- conf/config_test.go | 51 ++++++++++++++++ handlers/mail.go | 6 +- handlers/report.go | 2 +- 4 files changed, 146 insertions(+), 55 deletions(-) diff --git a/conf/config.go b/conf/config.go index 32c4b0e..93fc10c 100644 --- a/conf/config.go +++ b/conf/config.go @@ -20,24 +20,58 @@ const ENV_VAR_PREFIX = "BEACON_" type Config struct { envPrefix string + parents []string settings map[string]interface{} // manually set, should take precedence overrides map[string]interface{} } func (config *Config) AllSettings() map[string]interface{} { - return config.settings + settings := config.settings + for _, parent := range config.parents { + if settings == nil { + return nil + } + settingsSub, ok := settings[parent].(map[string]interface{}) + if ok { + settings = settingsSub + } else { + return nil + } + } + return settings } func (config *Config) keyToEnvVar(key string) string { - key = strings.ReplaceAll(key, ".", "_") + // todo: nested access + if strings.Contains(key, ".") { + panic("nested access with `.` not implemented") + } + // key = strings.ReplaceAll(key, ".", "_") + key = config.envPrefix + strings.Join(config.parents, "_") + "_" + key key = strings.ToUpper(key) - return config.envPrefix + key + return key } func (config *Config) get(key string) interface{} { - // TODO: dot notation for nested access not implemented - val, ok := config.overrides[key] + if config == nil { + return nil + } + // todo: nested access + if strings.Contains(key, ".") { + panic("nested access with `.` not implemented") + } + overrides := config.overrides + for _, parent := range config.parents { + if overrides == nil { + break + } + overridesSub, ok := overrides[parent].(map[string]interface{}) + if ok { + overrides = overridesSub + } + } + val, ok := overrides[key] if ok { return val } @@ -46,8 +80,20 @@ func (config *Config) get(key string) interface{} { if isSet { return envVal } - val, ok = config.settings[key] - // TODO: do we want default or nil? + settings := config.settings + for _, parent := range config.parents { + if settings == nil { + return nil + } + settingsSub, ok := settings[parent].(map[string]interface{}) + if ok { + settings = settingsSub + } else { + return nil + } + } + val, ok = settings[key] + if !ok { return nil } @@ -90,7 +136,7 @@ var boolyStrings = map[string]bool{ } func (config *Config) GetBool(key string) bool { - val := config.settings[key] + val := config.get(key) boolVal, isBool := val.(bool) if isBool { return boolVal @@ -120,55 +166,54 @@ func (config *Config) GetDuration(key string) time.Duration { } func (config *Config) Set(key string, value interface{}) { - // TODO: dot notation for nested access not implemented + // todo: nested access + if strings.Contains(key, ".") { + panic("nested access with `.` not implemented") + } + if len(config.parents) > 0 { + // todo: sub configs are read only for now + // not sure what to do with them atm + panic("Cannot set values for .Sub configs") + } config.overrides[key] = value } func (config *Config) SetDefault(key string, value interface{}) { - if !config.IsSet(key) { + // todo: nested access + if strings.Contains(key, ".") { + panic("nested access with `.` not implemented") + } + if len(config.parents) > 0 { + // todo: sub configs are read only for now + // not sure what to do with them atm + panic("Cannot set values for .Sub configs") + } + val := config.get(key) + if val == nil { config.settings[key] = value } } func (config *Config) IsSet(key string) bool { - _, ok := config.overrides[key] - if ok { - return true - } - _, isSet := os.LookupEnv(config.keyToEnvVar(key)) - if isSet { - return true - } - _, ok = config.settings[key] - return ok + // here if the key exists but has nil value we return false + // now this is kinda stupid but it kinda makes sense for our use-cases + // I don't have a solution that would be simple to do and work well atm + // todo: probably want to rethink the whole Config anyway + val := config.get(key) + return val != nil } -func subSettings(parent map[string]interface{}, key string) map[string]interface{} { - val, ok := parent[key] - if !ok { - return map[string]interface{}{} +func (config *Config) Sub(key string) *Config { + // todo: kinda weird implementation, not sure how I want to use this yet + if !config.IsSet(key) { + return nil } - child, ok := val.(map[string]interface{}) - // if config[key] is not a dict, it is a scalar - // to keep the same structure we convert it to map - // with the scalar as key and no value - // todo: maybe better design could be pondered here - if !ok { - child = make(map[string]interface{}) - child[val.(string)] = struct{}{} + return &Config{ + envPrefix: config.envPrefix, + parents: append(config.parents, key), + settings: config.settings, + overrides: config.overrides, } - return child -} - -func (config *Config) Sub(key string) *Config { - // TODO: doing a quick implementation here - // either needs more testing or remove this method - sub := &Config{} - sub.envPrefix = config.envPrefix + strings.ToUpper(key) + "_" - sub.overrides = subSettings(config.overrides, key) - sub.settings = subSettings(config.settings, key) - - return sub } //go:embed config.default.yaml @@ -239,20 +284,13 @@ func DefaultConfigFrom(configFile string) (*Config, error) { func NewConfig() *Config { config := &Config{ envPrefix: ENV_VAR_PREFIX, + parents: []string{}, settings: make(map[string]interface{}), overrides: make(map[string]interface{}), } return config } -// Minimal working config -func BaseConfig() *Config { - config := NewConfig() - config.settings["services"] = []string{} - config.settings["email"] = map[string]string{} - return config -} - // Setup config to use ENV variables and read specified config file. func setupConfig(configFile string) (*Config, error) { log.Printf("reading config from %q\n", configFile) diff --git a/conf/config_test.go b/conf/config_test.go index 83687fd..878defc 100644 --- a/conf/config_test.go +++ b/conf/config_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestLoadConfigFrom(t *testing.T) { @@ -38,3 +39,53 @@ func TestEnvVariablesOverwrite(t *testing.T) { assert.Equal(t, "my-new-prefix", emailConfig.GetString("prefix"), emailConfig) assert.Equal(t, 123, emailConfig.GetInt("smtp_port"), emailConfig) } + +func TestConfigGet(t *testing.T) { + config := NewConfig() + data := ` +bedroom: bed +kitchen: + fruit: apple + vegetable: cucumber + table: +` + err := yaml.Unmarshal([]byte(data), config.settings) + require.NoError(t, err) + require.NotNil(t, config) + t.Log(config) + + require.True(t, config.IsSet("bedroom")) + require.True(t, config.IsSet("kitchen")) + require.Equal(t, "bed", config.GetString("bedroom")) + + settings := config.AllSettings() + require.Contains(t, settings, "bedroom") + require.Contains(t, settings, "kitchen") + + kitchen := config.get("kitchen") + require.NotNil(t, kitchen) + t.Log(kitchen) + + kitchenConfig := config.Sub("kitchen") + require.NotNil(t, kitchenConfig) + t.Log(kitchenConfig) + + require.True(t, kitchenConfig.IsSet("fruit")) + require.True(t, kitchenConfig.IsSet("vegetable")) + require.Equal(t, "apple", kitchenConfig.GetString("fruit")) + // todo: Config rethink... the following will return false by design + // but the key exists! + // require.True(t, kitchenConfig.IsSet("table")) + // but for our use case it is more important that the following works: + settings = kitchenConfig.AllSettings() + require.Contains(t, settings, "fruit") + require.Contains(t, settings, "vegetable") + require.Contains(t, settings, "table") +} + +func TestConfigSet(t *testing.T) { + config := NewConfig() + config.Set("foo", true) + foo := config.GetBool("foo") + require.True(t, foo) +} diff --git a/handlers/mail.go b/handlers/mail.go index 6ebd387..4435a74 100644 --- a/handlers/mail.go +++ b/handlers/mail.go @@ -23,8 +23,10 @@ func (sm SMTPMailer) Send(reports []ServiceReport, emailConfig *conf.Config) err return err } - emailConfig.SetDefault("prefix", "") - prefix := emailConfig.GetString("prefix") + prefix := "" + if emailConfig.IsSet("prefix") { + prefix = emailConfig.GetString("prefix") + } // add whitespace after prefix if it exists and is not included already if prefix != "" && !strings.HasSuffix(prefix, " ") { prefix = prefix + " " diff --git a/handlers/report.go b/handlers/report.go index 5c63d8f..b068e93 100644 --- a/handlers/report.go +++ b/handlers/report.go @@ -86,7 +86,7 @@ func DoReportTask(db storage.Storage, config *conf.Config, now time.Time) error // TODO: need better way so check if should send email // currently the config.IsSet handles the "config-file path" // and allows overwrite via config "send-mail" variable for CLI usage - shouldSendEmail := config.IsSet("email.smtp_password") + shouldSendEmail := config.Sub("email").IsSet("smtp_password") if config.IsSet("send-mail") { shouldSendEmail = config.GetBool("send-mail") }