diff --git a/database/config_test.go b/database/config_test.go new file mode 100644 index 00000000..c6fe3b8a --- /dev/null +++ b/database/config_test.go @@ -0,0 +1,104 @@ +package database + +import ( + "github.com/creasty/defaults" + "github.com/icinga/icinga-go-library/config" + "github.com/stretchr/testify/require" + "testing" +) + +func TestConfig(t *testing.T) { + var defaultOptions Options + require.NoError(t, defaults.Set(&defaultOptions), "setting default options") + + subtests := []struct { + name string + opts config.EnvOptions + expected Config + error bool + }{ + { + name: "empty-missing-fields", + opts: config.EnvOptions{}, + error: true, + }, + { + name: "unknown-db-type", + opts: config.EnvOptions{Environment: map[string]string{"TYPE": "☃"}}, + error: true, + }, + { + name: "minimal-config", + opts: config.EnvOptions{Environment: map[string]string{ + "HOST": "db.example.com", + "USER": "user", + "DATABASE": "db", + }}, + expected: Config{ + Type: "mysql", + Host: "db.example.com", + Database: "db", + User: "user", + Options: defaultOptions, + }, + }, + { + name: "tls", + opts: config.EnvOptions{Environment: map[string]string{ + "HOST": "db.example.com", + "USER": "user", + "DATABASE": "db", + "TLS": "true", + "CERT": "/var/empty/db.crt", + "CA": "/var/empty/ca.crt", + }}, + expected: Config{ + Type: "mysql", + Host: "db.example.com", + Database: "db", + User: "user", + TlsOptions: config.TLS{ + Enable: true, + Cert: "/var/empty/db.crt", + Ca: "/var/empty/ca.crt", + }, + Options: defaultOptions, + }, + }, + { + name: "options", + opts: config.EnvOptions{Environment: map[string]string{ + "HOST": "db.example.com", + "USER": "user", + "DATABASE": "db", + "OPTIONS_MAX_CONNECTIONS": "1", + "OPTIONS_MAX_ROWS_PER_TRANSACTION": "65535", + }}, + expected: Config{ + Type: "mysql", + Host: "db.example.com", + Database: "db", + User: "user", + Options: Options{ + MaxConnections: 1, + MaxConnectionsPerTable: 8, + MaxPlaceholdersPerStatement: 8192, + MaxRowsPerTransaction: 65535, + WsrepSyncWait: 7, + }, + }, + }, + } + + for _, test := range subtests { + t.Run(test.name, func(t *testing.T) { + var out Config + if err := config.FromEnv(&out, test.opts); test.error { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, test.expected, out) + } + }) + } +} diff --git a/logging/config.go b/logging/config.go index 0bf64ab1..d739cb2b 100644 --- a/logging/config.go +++ b/logging/config.go @@ -5,12 +5,39 @@ import ( "github.com/pkg/errors" "go.uber.org/zap/zapcore" "os" + "strings" "time" ) // Options define child loggers with their desired log level. type Options map[string]zapcore.Level +// UnmarshalText implements encoding.TextUnmarshaler to allow Options to be parsed by env. +// +// This custom TextUnmarshaler is necessary as - for the moment - env does not support map[T]encoding.TextUnmarshaler. +// After got merged and a new env release was drafted, this method can be +// removed. +func (o *Options) UnmarshalText(text []byte) error { + optionsMap := make(map[string]zapcore.Level) + + for _, entry := range strings.Split(string(text), ",") { + key, valueStr, found := strings.Cut(entry, ":") + if !found { + return fmt.Errorf("entry %q cannot be unmarshalled as an Option entry", entry) + } + + valueLvl, err := zapcore.ParseLevel(valueStr) + if err != nil { + return fmt.Errorf("entry %q cannot be unmarshalled as level, %w", entry, err) + } + + optionsMap[key] = valueLvl + } + + *o = optionsMap + return nil +} + // Config defines Logger configuration. type Config struct { // zapcore.Level at 0 is for info level. diff --git a/logging/config_test.go b/logging/config_test.go new file mode 100644 index 00000000..1ac28fa8 --- /dev/null +++ b/logging/config_test.go @@ -0,0 +1,75 @@ +package logging + +import ( + "github.com/icinga/icinga-go-library/config" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" + "testing" + "time" +) + +func TestConfig(t *testing.T) { + subtests := []struct { + name string + opts config.EnvOptions + expected Config + error bool + }{ + { + name: "empty", + opts: config.EnvOptions{}, + expected: Config{ + Output: "console", + Interval: 20 * time.Second, + }, + }, + { + name: "invalid-output", + opts: config.EnvOptions{Environment: map[string]string{"OUTPUT": "☃"}}, + error: true, + }, + { + name: "customized", + opts: config.EnvOptions{Environment: map[string]string{ + "LEVEL": zapcore.DebugLevel.String(), + "OUTPUT": JOURNAL, + "INTERVAL": "3m14s", + }}, + expected: Config{ + Level: zapcore.DebugLevel, + Output: JOURNAL, + Interval: 3*time.Minute + 14*time.Second, + }, + }, + { + name: "options", + opts: config.EnvOptions{Environment: map[string]string{"OPTIONS": "foo:debug,bar:info,buz:panic"}}, + expected: Config{ + Output: "console", + Interval: 20 * time.Second, + Options: map[string]zapcore.Level{ + "foo": zapcore.DebugLevel, + "bar": zapcore.InfoLevel, + "buz": zapcore.PanicLevel, + }, + }, + }, + { + name: "options-invalid-levels", + opts: config.EnvOptions{Environment: map[string]string{"OPTIONS": "foo:foo,bar:0"}}, + error: true, + }, + } + + for _, test := range subtests { + t.Run(test.name, func(t *testing.T) { + var out Config + if err := config.FromEnv(&out, test.opts); test.error { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, test.expected, out) + } + }) + } +} diff --git a/redis/config_test.go b/redis/config_test.go new file mode 100644 index 00000000..9c43304b --- /dev/null +++ b/redis/config_test.go @@ -0,0 +1,100 @@ +package redis + +import ( + "github.com/creasty/defaults" + "github.com/icinga/icinga-go-library/config" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestConfig(t *testing.T) { + var defaultOptions Options + require.NoError(t, defaults.Set(&defaultOptions), "setting default options") + + subtests := []struct { + name string + opts config.EnvOptions + expected Config + error bool + }{ + { + name: "empty-missing-host", + opts: config.EnvOptions{}, + error: true, + }, + { + name: "minimal-config", + opts: config.EnvOptions{Environment: map[string]string{"HOST": "kv.example.com"}}, + expected: Config{ + Host: "kv.example.com", + Options: defaultOptions, + }, + }, + { + name: "customized-config", + opts: config.EnvOptions{Environment: map[string]string{ + "HOST": "kv.example.com", + "USERNAME": "user", + "PASSWORD": "insecure", + "DATABASE": "23", + }}, + expected: Config{ + Host: "kv.example.com", + Username: "user", + Password: "insecure", + Database: 23, + Options: defaultOptions, + }, + }, + { + name: "tls", + opts: config.EnvOptions{Environment: map[string]string{ + "HOST": "kv.example.com", + "TLS": "true", + "CERT": "/var/empty/db.crt", + "CA": "/var/empty/ca.crt", + }}, + expected: Config{ + Host: "kv.example.com", + TlsOptions: config.TLS{ + Enable: true, + Cert: "/var/empty/db.crt", + Ca: "/var/empty/ca.crt", + }, + Options: defaultOptions, + }, + }, + { + name: "options", + opts: config.EnvOptions{Environment: map[string]string{ + "HOST": "kv.example.com", + "OPTIONS_BLOCK_TIMEOUT": "1m", + "OPTIONS_MAX_HMGET_CONNECTIONS": "1000", + }}, + expected: Config{ + Host: "kv.example.com", + Options: Options{ + BlockTimeout: time.Minute, + HMGetCount: 4096, + HScanCount: 4096, + MaxHMGetConnections: 1000, + Timeout: 30 * time.Second, + XReadCount: 4096, + }, + }, + }, + } + + for _, test := range subtests { + t.Run(test.name, func(t *testing.T) { + var out Config + if err := config.FromEnv(&out, test.opts); test.error { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, test.expected, out) + } + }) + } +}