From 6bb21b1500bfe7a827f21746eb6574b89921a5c0 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Fri, 14 Oct 2022 15:35:16 +0100 Subject: [PATCH 01/12] refactor(config): use viper.Sub to extract LogConfig --- go.mod | 4 +-- go.sum | 4 +++ internal/config/config.go | 45 ++++++++++++++++++++++++----- internal/config/log.go | 60 ++++++++++++++++++--------------------- 4 files changed, 71 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index a90fb6bc2d..15336c3f80 100644 --- a/go.mod +++ b/go.mod @@ -112,9 +112,9 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect - golang.org/x/exp v0.0.0-20210916165020-5cb4fee858ee // indirect + golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 // indirect golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 05ff98ee48..1a693fc805 100644 --- a/go.sum +++ b/go.sum @@ -1191,6 +1191,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20210916165020-5cb4fee858ee h1:qlrAyYdKz4o7rWVUjiKqQJMa4PEpd55fqBU8jpsl4Iw= golang.org/x/exp v0.0.0-20210916165020-5cb4fee858ee/go.mod h1:a3o/VtDNHN+dCVLEpzjjUHOzR+Ln3DHX056ZPzoZGGA= +golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg= +golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1420,6 +1422,8 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/internal/config/config.go b/internal/config/config.go index c55ceb4b02..6aeb98efa7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,10 +4,13 @@ import ( "encoding/json" "fmt" "net/http" + "reflect" "strings" "time" + "github.com/mitchellh/mapstructure" "github.com/spf13/viper" + "golang.org/x/exp/constraints" jaeger "github.com/uber/jaeger-client-go" ) @@ -26,12 +29,6 @@ type Config struct { func Default() *Config { return &Config{ - Log: LogConfig{ - Level: "INFO", - Encoding: LogEncodingConsole, - GRPCLevel: "ERROR", - }, - UI: UIConfig{ Enabled: true, }, @@ -101,7 +98,6 @@ func Load(path string) (*Config, error) { for _, initializer := range []interface { init() (warnings []string, err error) }{ - &cfg.Log, &cfg.UI, &cfg.Cors, &cfg.Cache, @@ -118,6 +114,22 @@ func Load(path string) (*Config, error) { cfg.Warnings = append(cfg.Warnings, warnings...) } + for _, unmarshaller := range []interface { + viperKey() string + unmarshalViper(*viper.Viper) (warnings []string, err error) + }{ + &cfg.Log, + } { + if v := viper.Sub(unmarshaller.viperKey()); v != nil { + warnings, err := unmarshaller.unmarshalViper(v) + if err != nil { + return nil, err + } + + cfg.Warnings = append(cfg.Warnings, warnings...) + } + } + return cfg, nil } @@ -143,3 +155,22 @@ func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } } + +// StringToEnumHookFunc returns a DecodeHookFunc that converts strings to a target enum +func StringToEnumHookFunc[T constraints.Integer](mappings map[string]T) mapstructure.DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + if t != reflect.TypeOf(T(0)) { + return data, nil + } + + enum, _ := mappings[data.(string)] + + return enum, nil + } +} diff --git a/internal/config/log.go b/internal/config/log.go index 9c87461ae9..1a9d5dee98 100644 --- a/internal/config/log.go +++ b/internal/config/log.go @@ -3,53 +3,41 @@ package config import ( "encoding/json" + "github.com/mitchellh/mapstructure" "github.com/spf13/viper" ) -const ( - // configuration keys - logLevel = "log.level" - logFile = "log.file" - logEncoding = "log.encoding" - logGRPCLevel = "log.grpc_level" - - // encoding enum - _ LogEncoding = iota - LogEncodingConsole - LogEncodingJSON -) - // LogConfig contains fields which control, direct and filter // the logging telemetry produces by Flipt. type LogConfig struct { - Level string `json:"level,omitempty"` - File string `json:"file,omitempty"` - Encoding LogEncoding `json:"encoding,omitempty"` - GRPCLevel string `json:"grpc_level,omitempty"` + Level string `json:"level,omitempty" mapstructure:"level"` + File string `json:"file,omitempty" mapstructure:"file"` + Encoding LogEncoding `json:"encoding,omitempty" mapstructure:"encoding"` + GRPCLevel string `json:"grpc_level,omitempty" mapstructure:"grpc_level"` } -func (c *LogConfig) init() (_ []string, _ error) { - if viper.IsSet(logLevel) { - c.Level = viper.GetString(logLevel) - } - - if viper.IsSet(logFile) { - c.File = viper.GetString(logFile) - } +var ( + logDecodeHooks = mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + StringToEnumHookFunc(stringToLogEncoding), + ) +) - if viper.IsSet(logEncoding) { - c.Encoding = stringToLogEncoding[viper.GetString(logEncoding)] - } +func (c *LogConfig) viperKey() string { + return "log" +} - if viper.IsSet(logGRPCLevel) { - c.GRPCLevel = viper.GetString(logGRPCLevel) - } +func (c *LogConfig) unmarshalViper(v *viper.Viper) (_ []string, _ error) { + v.SetDefault("level", "INFO") + v.SetDefault("encoding", "console") + v.SetDefault("grpc_level", "ERROR") - return + return nil, v.Unmarshal(c, viper.DecodeHook(logDecodeHooks)) } var ( - logEncodingToString = map[LogEncoding]string{ + logEncodingToString = [...]string{ LogEncodingConsole: "console", LogEncodingJSON: "json", } @@ -63,6 +51,12 @@ var ( // LogEncoding is either console or JSON type LogEncoding uint8 +const ( + _ LogEncoding = iota + LogEncodingConsole + LogEncodingJSON +) + func (e LogEncoding) String() string { return logEncodingToString[e] } From 7203ef6c45a5e4d564bd24f52ebf457541f3c21a Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Fri, 14 Oct 2022 16:19:40 +0100 Subject: [PATCH 02/12] refactor(config): use viper.Sub to extract UIConfig --- internal/config/config.go | 6 +----- internal/config/ui.go | 19 ++++++++----------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 6aeb98efa7..c791273d13 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,10 +29,6 @@ type Config struct { func Default() *Config { return &Config{ - UI: UIConfig{ - Enabled: true, - }, - Cors: CorsConfig{ Enabled: false, AllowedOrigins: []string{"*"}, @@ -98,7 +94,6 @@ func Load(path string) (*Config, error) { for _, initializer := range []interface { init() (warnings []string, err error) }{ - &cfg.UI, &cfg.Cors, &cfg.Cache, &cfg.Server, @@ -119,6 +114,7 @@ func Load(path string) (*Config, error) { unmarshalViper(*viper.Viper) (warnings []string, err error) }{ &cfg.Log, + &cfg.UI, } { if v := viper.Sub(unmarshaller.viperKey()); v != nil { warnings, err := unmarshaller.unmarshalViper(v) diff --git a/internal/config/ui.go b/internal/config/ui.go index 6d9a397391..3dce40eab7 100644 --- a/internal/config/ui.go +++ b/internal/config/ui.go @@ -2,21 +2,18 @@ package config import "github.com/spf13/viper" -const ( - // configuration keys - uiEnabled = "ui.enabled" -) - // UIConfig contains fields, which control the behaviour // of Flipt's user interface. type UIConfig struct { - Enabled bool `json:"enabled"` + Enabled bool `json:"enabled" mapstructure:"enabled"` +} + +func (c *UIConfig) viperKey() string { + return "ui" } -func (c *UIConfig) init() (_ []string, _ error) { - if viper.IsSet(uiEnabled) { - c.Enabled = viper.GetBool(uiEnabled) - } +func (c *UIConfig) unmarshalViper(v *viper.Viper) (_ []string, _ error) { + v.SetDefault("enabled", true) - return + return nil, v.Unmarshal(c) } From 29875878b7fcd527a8de05c87f15e787c1c2763e Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Fri, 14 Oct 2022 16:24:21 +0100 Subject: [PATCH 03/12] refactor(config): use viper.Sub to extract CorsConfig --- internal/config/config.go | 7 +------ internal/config/cors.go | 23 +++++++---------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index c791273d13..2c6f719215 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,11 +29,6 @@ type Config struct { func Default() *Config { return &Config{ - Cors: CorsConfig{ - Enabled: false, - AllowedOrigins: []string{"*"}, - }, - Cache: CacheConfig{ Enabled: false, Backend: CacheMemory, @@ -94,7 +89,6 @@ func Load(path string) (*Config, error) { for _, initializer := range []interface { init() (warnings []string, err error) }{ - &cfg.Cors, &cfg.Cache, &cfg.Server, &cfg.Tracing, @@ -115,6 +109,7 @@ func Load(path string) (*Config, error) { }{ &cfg.Log, &cfg.UI, + &cfg.Cors, } { if v := viper.Sub(unmarshaller.viperKey()); v != nil { warnings, err := unmarshaller.unmarshalViper(v) diff --git a/internal/config/cors.go b/internal/config/cors.go index 138817d102..9a38705ac8 100644 --- a/internal/config/cors.go +++ b/internal/config/cors.go @@ -2,26 +2,17 @@ package config import "github.com/spf13/viper" -const ( - corsEnabled = "cors.enabled" - corsAllowedOrigins = "cors.allowed_origins" -) - // CorsConfig contains fields, which configure behaviour in the // HTTPServer relating to the CORS header-based mechanisms. type CorsConfig struct { - Enabled bool `json:"enabled"` - AllowedOrigins []string `json:"allowedOrigins,omitempty"` + Enabled bool `json:"enabled" mapstructure:"enabled"` + AllowedOrigins []string `json:"allowedOrigins,omitempty" mapstructure:"allowed_origins"` } -func (c *CorsConfig) init() (_ []string, _ error) { - if viper.IsSet(corsEnabled) { - c.Enabled = viper.GetBool(corsEnabled) - - if viper.IsSet(corsAllowedOrigins) { - c.AllowedOrigins = viper.GetStringSlice(corsAllowedOrigins) - } - } +func (c *CorsConfig) viperKey() string { + return "cors" +} - return +func (c *CorsConfig) unmarshalViper(v *viper.Viper) (_ []string, _ error) { + return nil, v.Unmarshal(c) } From b2d627aa653d186265eb12bfd9511107ba4a3ec9 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Fri, 14 Oct 2022 17:52:25 +0100 Subject: [PATCH 04/12] refactor(config): use viper.Sub to extract CacheConfig --- internal/config/cache.go | 106 +++++++++++++++------------------ internal/config/config.go | 18 +----- internal/config/config_test.go | 1 + 3 files changed, 50 insertions(+), 75 deletions(-) diff --git a/internal/config/cache.go b/internal/config/cache.go index 64a47e7984..9a02a95555 100644 --- a/internal/config/cache.go +++ b/internal/config/cache.go @@ -4,21 +4,14 @@ import ( "encoding/json" "time" + "github.com/mitchellh/mapstructure" "github.com/spf13/viper" ) -const ( - // configuration keys - cacheBackend = "cache.backend" - cacheEnabled = "cache.enabled" - cacheTTL = "cache.ttl" - cacheMemoryEnabled = "cache.memory.enabled" // deprecated in v1.10.0 - cacheMemoryExpiration = "cache.memory.expiration" // deprecated in v1.10.0 - cacheMemoryEvictionInterval = "cache.memory.eviction_interval" - cacheRedisHost = "cache.redis.host" - cacheRedisPort = "cache.redis.port" - cacheRedisPassword = "cache.redis.password" - cacheRedisDB = "cache.redis.db" +var cacheDecodeHooks = mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + StringToEnumHookFunc(stringToCacheBackend), ) // CacheConfig contains fields, which enable and configure @@ -26,54 +19,51 @@ const ( // // Currently, flipt support in-memory and redis backed caching. type CacheConfig struct { - Enabled bool `json:"enabled"` - TTL time.Duration `json:"ttl,omitempty"` - Backend CacheBackend `json:"backend,omitempty"` - Memory MemoryCacheConfig `json:"memory,omitempty"` - Redis RedisCacheConfig `json:"redis,omitempty"` + Enabled bool `json:"enabled" mapstructure:"enabled"` + TTL time.Duration `json:"ttl,omitempty" mapstructure:"ttl"` + Backend CacheBackend `json:"backend,omitempty" mapstructure:"backend"` + Memory MemoryCacheConfig `json:"memory,omitempty" mapstructure:"memory"` + Redis RedisCacheConfig `json:"redis,omitempty" mapstructure:"redis"` } -func (c *CacheConfig) init() (warnings []string, _ error) { - if viper.GetBool(cacheMemoryEnabled) { // handle deprecated memory config - c.Backend = CacheMemory - c.Enabled = true - - warnings = append(warnings, deprecatedMsgMemoryEnabled) +func (c *CacheConfig) viperKey() string { + return "cache" +} - if viper.IsSet(cacheMemoryExpiration) { - c.TTL = viper.GetDuration(cacheMemoryExpiration) - warnings = append(warnings, deprecatedMsgMemoryExpiration) +func (c *CacheConfig) unmarshalViper(v *viper.Viper) (warnings []string, err error) { + if mem := v.Sub("memory"); mem != nil { + // handle legacy memory structure + if mem.GetBool("enabled") { + // forcibly set top-level `enabled` to true + v.Set("enabled", true) + // ensure ttl is mapped to the value at memory.expiration + v.RegisterAlias("ttl", "memory.expiration") + } else { + mem.SetDefault("eviction_interval", 5*time.Minute) } + } - } else if viper.IsSet(cacheEnabled) { - c.Enabled = viper.GetBool(cacheEnabled) - if viper.IsSet(cacheBackend) { - c.Backend = stringToCacheBackend[viper.GetString(cacheBackend)] - } - if viper.IsSet(cacheTTL) { - c.TTL = viper.GetDuration(cacheTTL) - } + if redis := v.Sub("redis"); redis != nil { + redis.SetDefault("host", "localhost") + redis.SetDefault("port", 6379) + redis.SetDefault("password", "") + redis.SetDefault("db", 0) } - if c.Enabled { - switch c.Backend { - case CacheRedis: - if viper.IsSet(cacheRedisHost) { - c.Redis.Host = viper.GetString(cacheRedisHost) - } - if viper.IsSet(cacheRedisPort) { - c.Redis.Port = viper.GetInt(cacheRedisPort) - } - if viper.IsSet(cacheRedisPassword) { - c.Redis.Password = viper.GetString(cacheRedisPassword) - } - if viper.IsSet(cacheRedisDB) { - c.Redis.DB = viper.GetInt(cacheRedisDB) - } - case CacheMemory: - if viper.IsSet(cacheMemoryEvictionInterval) { - c.Memory.EvictionInterval = viper.GetDuration(cacheMemoryEvictionInterval) - } + if v.GetBool("enabled") { + v.SetDefault("backend", CacheMemory) + v.SetDefault("ttl", 1*time.Minute) + } + + if err = v.Unmarshal(c, viper.DecodeHook(cacheDecodeHooks)); err != nil { + return + } + + if v.GetBool("memory.enabled") { // handle deprecated memory config + warnings = append(warnings, deprecatedMsgMemoryEnabled) + + if v.IsSet("memory.expiration") { + warnings = append(warnings, deprecatedMsgMemoryExpiration) } } @@ -113,14 +103,14 @@ var ( // MemoryCacheConfig contains fields, which configure in-memory caching. type MemoryCacheConfig struct { - EvictionInterval time.Duration `json:"evictionInterval,omitempty"` + EvictionInterval time.Duration `json:"evictionInterval,omitempty" mapstructure:"eviction_interval"` } // RedisCacheConfig contains fields, which configure the connection // credentials for redis backed caching. type RedisCacheConfig struct { - Host string `json:"host,omitempty"` - Port int `json:"port,omitempty"` - Password string `json:"password,omitempty"` - DB int `json:"db,omitempty"` + Host string `json:"host,omitempty" mapstructure:"host"` + Port int `json:"port,omitempty" mapstructure:"port"` + Password string `json:"password,omitempty" mapstructure:"password"` + DB int `json:"db,omitempty" mapstructure:"db"` } diff --git a/internal/config/config.go b/internal/config/config.go index 2c6f719215..6def052c1c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,7 +6,6 @@ import ( "net/http" "reflect" "strings" - "time" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" @@ -29,21 +28,6 @@ type Config struct { func Default() *Config { return &Config{ - Cache: CacheConfig{ - Enabled: false, - Backend: CacheMemory, - TTL: 1 * time.Minute, - Memory: MemoryCacheConfig{ - EvictionInterval: 5 * time.Minute, - }, - Redis: RedisCacheConfig{ - Host: "localhost", - Port: 6379, - Password: "", - DB: 0, - }, - }, - Server: ServerConfig{ Host: "0.0.0.0", Protocol: HTTP, @@ -89,7 +73,6 @@ func Load(path string) (*Config, error) { for _, initializer := range []interface { init() (warnings []string, err error) }{ - &cfg.Cache, &cfg.Server, &cfg.Tracing, &cfg.Database, @@ -110,6 +93,7 @@ func Load(path string) (*Config, error) { &cfg.Log, &cfg.UI, &cfg.Cors, + &cfg.Cache, } { if v := viper.Sub(unmarshaller.viperKey()); v != nil { warnings, err := unmarshaller.unmarshalViper(v) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 8b3cbefa62..a7c7d2ebf8 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -290,6 +290,7 @@ func TestLoad(t *testing.T) { } cfg.Cache.Enabled = true cfg.Cache.Backend = CacheMemory + cfg.Cache.TTL = 1 * time.Minute cfg.Cache.Memory = MemoryCacheConfig{ EvictionInterval: 5 * time.Minute, } From f55c146bb36c891695ba82d7343efdf4c7d6bab0 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Fri, 14 Oct 2022 18:19:40 +0100 Subject: [PATCH 05/12] refactor(config): use viper.Sub to extract ServerConfig --- internal/config/config.go | 10 +----- internal/config/server.go | 65 ++++++++++++++------------------------- 2 files changed, 24 insertions(+), 51 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 6def052c1c..10147a0409 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,14 +28,6 @@ type Config struct { func Default() *Config { return &Config{ - Server: ServerConfig{ - Host: "0.0.0.0", - Protocol: HTTP, - HTTPPort: 8080, - HTTPSPort: 443, - GRPCPort: 9000, - }, - Tracing: TracingConfig{ Jaeger: JaegerTracingConfig{ Enabled: false, @@ -73,7 +65,6 @@ func Load(path string) (*Config, error) { for _, initializer := range []interface { init() (warnings []string, err error) }{ - &cfg.Server, &cfg.Tracing, &cfg.Database, &cfg.Meta, @@ -94,6 +85,7 @@ func Load(path string) (*Config, error) { &cfg.UI, &cfg.Cors, &cfg.Cache, + &cfg.Server, } { if v := viper.Sub(unmarshaller.viperKey()); v != nil { warnings, err := unmarshaller.unmarshalViper(v) diff --git a/internal/config/server.go b/internal/config/server.go index 1658419c52..e78516f511 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -4,60 +4,41 @@ import ( "encoding/json" "os" + "github.com/mitchellh/mapstructure" "github.com/spf13/viper" ) -const ( - // configuration keys - serverHost = "server.host" - serverProtocol = "server.protocol" - serverHTTPPort = "server.http_port" - serverHTTPSPort = "server.https_port" - serverGRPCPort = "server.grpc_port" - serverCertFile = "server.cert_file" - serverCertKey = "server.cert_key" +var serverDecodeHooks = mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + StringToEnumHookFunc(stringToScheme), ) // ServerConfig contains fields, which configure both HTTP and gRPC // API serving. type ServerConfig struct { - Host string `json:"host,omitempty"` - Protocol Scheme `json:"protocol,omitempty"` - HTTPPort int `json:"httpPort,omitempty"` - HTTPSPort int `json:"httpsPort,omitempty"` - GRPCPort int `json:"grpcPort,omitempty"` - CertFile string `json:"certFile,omitempty"` - CertKey string `json:"certKey,omitempty"` + Host string `json:"host,omitempty" mapstructure:"host"` + Protocol Scheme `json:"protocol,omitempty" mapstructure:"protocol"` + HTTPPort int `json:"httpPort,omitempty" mapstructure:"http_port"` + HTTPSPort int `json:"httpsPort,omitempty" mapstructure:"https_port"` + GRPCPort int `json:"grpcPort,omitempty" mapstructure:"grpc_port"` + CertFile string `json:"certFile,omitempty" mapstructure:"cert_file"` + CertKey string `json:"certKey,omitempty" mapstructure:"cert_key"` } -func (c *ServerConfig) init() (warnings []string, _ error) { - // read in configuration via viper - if viper.IsSet(serverHost) { - c.Host = viper.GetString(serverHost) - } - - if viper.IsSet(serverProtocol) { - c.Protocol = stringToScheme[viper.GetString(serverProtocol)] - } - - if viper.IsSet(serverHTTPPort) { - c.HTTPPort = viper.GetInt(serverHTTPPort) - } - - if viper.IsSet(serverHTTPSPort) { - c.HTTPSPort = viper.GetInt(serverHTTPSPort) - } - - if viper.IsSet(serverGRPCPort) { - c.GRPCPort = viper.GetInt(serverGRPCPort) - } +func (c *ServerConfig) viperKey() string { + return "server" +} - if viper.IsSet(serverCertFile) { - c.CertFile = viper.GetString(serverCertFile) - } +func (c *ServerConfig) unmarshalViper(v *viper.Viper) (warnings []string, err error) { + v.SetDefault("host", "0.0.0.0") + v.SetDefault("protocol", HTTP) + v.SetDefault("http_port", 8080) + v.SetDefault("https_port", 443) + v.SetDefault("grpc_port", 9000) - if viper.IsSet(serverCertKey) { - c.CertKey = viper.GetString(serverCertKey) + if err = v.Unmarshal(c, viper.DecodeHook(serverDecodeHooks)); err != nil { + return } // validate configuration is as expected From 7ff51f78ffd5ae2c55fa31d4133e39ba2160714e Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Sat, 15 Oct 2022 08:56:37 +0100 Subject: [PATCH 06/12] refactor(config): use viper.Sub to extract TracingConfig --- internal/config/config.go | 12 +----------- internal/config/tracing.go | 35 +++++++++++++---------------------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 10147a0409..1a3bd0cbf6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,8 +10,6 @@ import ( "github.com/mitchellh/mapstructure" "github.com/spf13/viper" "golang.org/x/exp/constraints" - - jaeger "github.com/uber/jaeger-client-go" ) type Config struct { @@ -28,14 +26,6 @@ type Config struct { func Default() *Config { return &Config{ - Tracing: TracingConfig{ - Jaeger: JaegerTracingConfig{ - Enabled: false, - Host: jaeger.DefaultUDPSpanServerHost, - Port: jaeger.DefaultUDPSpanServerPort, - }, - }, - Database: DatabaseConfig{ URL: "file:/var/opt/flipt/flipt.db", MigrationsPath: "/etc/flipt/config/migrations", @@ -65,7 +55,6 @@ func Load(path string) (*Config, error) { for _, initializer := range []interface { init() (warnings []string, err error) }{ - &cfg.Tracing, &cfg.Database, &cfg.Meta, } { @@ -86,6 +75,7 @@ func Load(path string) (*Config, error) { &cfg.Cors, &cfg.Cache, &cfg.Server, + &cfg.Tracing, } { if v := viper.Sub(unmarshaller.viperKey()); v != nil { warnings, err := unmarshaller.unmarshalViper(v) diff --git a/internal/config/tracing.go b/internal/config/tracing.go index a7618f333b..ceb8d76b88 100644 --- a/internal/config/tracing.go +++ b/internal/config/tracing.go @@ -2,19 +2,12 @@ package config import "github.com/spf13/viper" -const ( - // configuration keys - tracingJaegerEnabled = "tracing.jaeger.enabled" - tracingJaegerHost = "tracing.jaeger.host" - tracingJaegerPort = "tracing.jaeger.port" -) - // JaegerTracingConfig contains fields, which configure specifically // Jaeger span and tracing output destination. type JaegerTracingConfig struct { - Enabled bool `json:"enabled,omitempty"` - Host string `json:"host,omitempty"` - Port int `json:"port,omitempty"` + Enabled bool `json:"enabled,omitempty" mapstructure:"enabled"` + Host string `json:"host,omitempty" mapstructure:"host"` + Port int `json:"port,omitempty" mapstructure:"port"` } // TracingConfig contains fields, which configure tracing telemetry @@ -23,18 +16,16 @@ type TracingConfig struct { Jaeger JaegerTracingConfig `json:"jaeger,omitempty"` } -func (c *TracingConfig) init() ([]string, error) { - if viper.IsSet(tracingJaegerEnabled) { - c.Jaeger.Enabled = viper.GetBool(tracingJaegerEnabled) - - if viper.IsSet(tracingJaegerHost) { - c.Jaeger.Host = viper.GetString(tracingJaegerHost) - } +func (c *TracingConfig) viperKey() string { + return "tracing" +} - if viper.IsSet(tracingJaegerPort) { - c.Jaeger.Port = viper.GetInt(tracingJaegerPort) - } +func (c *TracingConfig) unmarshalViper(v *viper.Viper) ([]string, error) { + if v.IsSet("jaeger.enabled") { + v.SetDefault("jaeger", map[string]any{ + "host": "localhost", + "port": 6831, + }) } - - return nil, nil + return nil, v.Unmarshal(c) } From 61b2a857f42afecd31773ea57c3f24bd20eb95c8 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Sat, 15 Oct 2022 11:04:52 +0100 Subject: [PATCH 07/12] refactor(config): use viper.Sub to extract DatabaseConfig --- internal/config/config.go | 8 +-- internal/config/database.go | 103 +++++++++++++----------------------- internal/config/log.go | 10 ++-- 3 files changed, 41 insertions(+), 80 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 1a3bd0cbf6..6bc9ca47a9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,12 +26,6 @@ type Config struct { func Default() *Config { return &Config{ - Database: DatabaseConfig{ - URL: "file:/var/opt/flipt/flipt.db", - MigrationsPath: "/etc/flipt/config/migrations", - MaxIdleConn: 2, - }, - Meta: MetaConfig{ CheckForUpdates: true, TelemetryEnabled: true, @@ -55,7 +49,6 @@ func Load(path string) (*Config, error) { for _, initializer := range []interface { init() (warnings []string, err error) }{ - &cfg.Database, &cfg.Meta, } { warnings, err := initializer.init() @@ -76,6 +69,7 @@ func Load(path string) (*Config, error) { &cfg.Cache, &cfg.Server, &cfg.Tracing, + &cfg.Database, } { if v := viper.Sub(unmarshaller.viperKey()); v != nil { warnings, err := unmarshaller.unmarshalViper(v) diff --git a/internal/config/database.go b/internal/config/database.go index a815dd0d08..7ae87065da 100644 --- a/internal/config/database.go +++ b/internal/config/database.go @@ -4,23 +4,11 @@ import ( "encoding/json" "time" + "github.com/mitchellh/mapstructure" "github.com/spf13/viper" ) const ( - // configuration keys - dbURL = "db.url" - dbMigrationsPath = "db.migrations.path" - dbMaxIdleConn = "db.max_idle_conn" - dbMaxOpenConn = "db.max_open_conn" - dbConnMaxLifetime = "db.conn_max_lifetime" - dbName = "db.name" - dbUser = "db.user" - dbPassword = "db.password" - dbHost = "db.host" - dbPort = "db.port" - dbProtocol = "db.protocol" - // database protocol enum _ DatabaseProtocol = iota // DatabaseSQLite ... @@ -31,71 +19,52 @@ const ( DatabaseMySQL ) +var dbProtocolDecodeHooks = mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + StringToEnumHookFunc(stringToDatabaseProtocol), +) + // DatabaseConfig contains fields, which configure the various relational database backends. // // Flipt currently supports SQLite, Postgres and MySQL backends. type DatabaseConfig struct { - MigrationsPath string `json:"migrationsPath,omitempty"` - URL string `json:"url,omitempty"` - MaxIdleConn int `json:"maxIdleConn,omitempty"` - MaxOpenConn int `json:"maxOpenConn,omitempty"` - ConnMaxLifetime time.Duration `json:"connMaxLifetime,omitempty"` - Name string `json:"name,omitempty"` - User string `json:"user,omitempty"` - Password string `json:"password,omitempty"` - Host string `json:"host,omitempty"` - Port int `json:"port,omitempty"` - Protocol DatabaseProtocol `json:"protocol,omitempty"` + MigrationsPath string `json:"migrationsPath,omitempty" mapstructure:"migrations_path"` + URL string `json:"url,omitempty" mapstructure:"url"` + MaxIdleConn int `json:"maxIdleConn,omitempty" mapstructure:"max_idle_conn"` + MaxOpenConn int `json:"maxOpenConn,omitempty" mapstructure:"max_open_conn"` + ConnMaxLifetime time.Duration `json:"connMaxLifetime,omitempty" mapstructure:"conn_max_lifetime"` + Name string `json:"name,omitempty" mapstructure:"name"` + User string `json:"user,omitempty" mapstructure:"user"` + Password string `json:"password,omitempty" mapstructure:"password"` + Host string `json:"host,omitempty" mapstructure:"host"` + Port int `json:"port,omitempty" mapstructure:"port"` + Protocol DatabaseProtocol `json:"protocol,omitempty" mapstructure:"protocol"` } -func (c *DatabaseConfig) init() (warnings []string, _ error) { - // read in configuration via viper - if viper.IsSet(dbURL) { - c.URL = viper.GetString(dbURL) - - } else if viper.IsSet(dbProtocol) || viper.IsSet(dbName) || viper.IsSet(dbUser) || viper.IsSet(dbPassword) || viper.IsSet(dbHost) || viper.IsSet(dbPort) { - c.URL = "" - - if viper.IsSet(dbProtocol) { - c.Protocol = stringToDatabaseProtocol[viper.GetString(dbProtocol)] - } - - if viper.IsSet(dbName) { - c.Name = viper.GetString(dbName) - } - - if viper.IsSet(dbUser) { - c.User = viper.GetString(dbUser) - } - - if viper.IsSet(dbPassword) { - c.Password = viper.GetString(dbPassword) - } - - if viper.IsSet(dbHost) { - c.Host = viper.GetString(dbHost) - } - - if viper.IsSet(dbPort) { - c.Port = viper.GetInt(dbPort) - } - - } - - if viper.IsSet(dbMigrationsPath) { - c.MigrationsPath = viper.GetString(dbMigrationsPath) - } +func (c *DatabaseConfig) viperKey() string { + return "db" +} - if viper.IsSet(dbMaxIdleConn) { - c.MaxIdleConn = viper.GetInt(dbMaxIdleConn) +func (c *DatabaseConfig) unmarshalViper(v *viper.Viper) (warnings []string, err error) { + // supports nesting `path` beneath `migrations` key + v.RegisterAlias("migrations_path", "migrations.path") + v.SetDefault("migrations_path", "/etc/flipt/config/migrations") + v.SetDefault("max_idle_conn", 2) + + // URL default is only set given that none of the alternative + // database connections parameters are provided + setDefaultURL := true + for _, field := range []string{"name", "user", "password", "host", "port", "protocol"} { + setDefaultURL = setDefaultURL && !v.IsSet(field) } - if viper.IsSet(dbMaxOpenConn) { - c.MaxOpenConn = viper.GetInt(dbMaxOpenConn) + if setDefaultURL { + v.SetDefault("url", "file:/var/opt/flipt/flipt.db") } - if viper.IsSet(dbConnMaxLifetime) { - c.ConnMaxLifetime = viper.GetDuration(dbConnMaxLifetime) + if err = v.Unmarshal(c, viper.DecodeHook(dbProtocolDecodeHooks)); err != nil { + return } // validation diff --git a/internal/config/log.go b/internal/config/log.go index 1a9d5dee98..5d22123b38 100644 --- a/internal/config/log.go +++ b/internal/config/log.go @@ -16,12 +16,10 @@ type LogConfig struct { GRPCLevel string `json:"grpc_level,omitempty" mapstructure:"grpc_level"` } -var ( - logDecodeHooks = mapstructure.ComposeDecodeHookFunc( - mapstructure.StringToTimeDurationHookFunc(), - mapstructure.StringToSliceHookFunc(","), - StringToEnumHookFunc(stringToLogEncoding), - ) +var logDecodeHooks = mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + StringToEnumHookFunc(stringToLogEncoding), ) func (c *LogConfig) viperKey() string { From 8840324f633b2b06720e9d8c9f85ad79c2d59786 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Sat, 15 Oct 2022 11:55:14 +0100 Subject: [PATCH 08/12] refactor(config): use viper.Sub to extract MetaConfig + fixes --- internal/config/cache.go | 25 +++++------ internal/config/config.go | 40 +++++------------ internal/config/config_test.go | 81 ++++++++++++++++++++++++++++++---- internal/config/cors.go | 1 + internal/config/meta.go | 33 +++++--------- internal/config/tracing.go | 10 ++--- 6 files changed, 109 insertions(+), 81 deletions(-) diff --git a/internal/config/cache.go b/internal/config/cache.go index 9a02a95555..d13ad3812d 100644 --- a/internal/config/cache.go +++ b/internal/config/cache.go @@ -31,30 +31,27 @@ func (c *CacheConfig) viperKey() string { } func (c *CacheConfig) unmarshalViper(v *viper.Viper) (warnings []string, err error) { + v.SetDefault("backend", CacheMemory) + v.SetDefault("ttl", 1*time.Minute) + v.SetDefault("redis", map[string]any{ + "host": "localhost", + "port": 6379, + }) + v.SetDefault("memory", map[string]any{ + "eviction_interval": 5 * time.Minute, + }) + if mem := v.Sub("memory"); mem != nil { + mem.SetDefault("eviction_interval", 5*time.Minute) // handle legacy memory structure if mem.GetBool("enabled") { // forcibly set top-level `enabled` to true v.Set("enabled", true) // ensure ttl is mapped to the value at memory.expiration v.RegisterAlias("ttl", "memory.expiration") - } else { - mem.SetDefault("eviction_interval", 5*time.Minute) } } - if redis := v.Sub("redis"); redis != nil { - redis.SetDefault("host", "localhost") - redis.SetDefault("port", 6379) - redis.SetDefault("password", "") - redis.SetDefault("db", 0) - } - - if v.GetBool("enabled") { - v.SetDefault("backend", CacheMemory) - v.SetDefault("ttl", 1*time.Minute) - } - if err = v.Unmarshal(c, viper.DecodeHook(cacheDecodeHooks)); err != nil { return } diff --git a/internal/config/config.go b/internal/config/config.go index 6bc9ca47a9..c1588bbdd9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,16 +24,6 @@ type Config struct { Warnings []string `json:"warnings,omitempty"` } -func Default() *Config { - return &Config{ - Meta: MetaConfig{ - CheckForUpdates: true, - TelemetryEnabled: true, - StateDirectory: "", - }, - } -} - func Load(path string) (*Config, error) { viper.SetEnvPrefix("FLIPT") viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) @@ -45,19 +35,7 @@ func Load(path string) (*Config, error) { return nil, fmt.Errorf("loading configuration: %w", err) } - cfg := Default() - for _, initializer := range []interface { - init() (warnings []string, err error) - }{ - &cfg.Meta, - } { - warnings, err := initializer.init() - if err != nil { - return nil, err - } - - cfg.Warnings = append(cfg.Warnings, warnings...) - } + cfg := &Config{} for _, unmarshaller := range []interface { viperKey() string @@ -70,15 +48,19 @@ func Load(path string) (*Config, error) { &cfg.Server, &cfg.Tracing, &cfg.Database, + &cfg.Meta, } { - if v := viper.Sub(unmarshaller.viperKey()); v != nil { - warnings, err := unmarshaller.unmarshalViper(v) - if err != nil { - return nil, err - } + v := viper.Sub(unmarshaller.viperKey()) + if v == nil { + v = viper.New() + } - cfg.Warnings = append(cfg.Warnings, warnings...) + warnings, err := unmarshaller.unmarshalViper(v) + if err != nil { + return nil, err } + + cfg.Warnings = append(cfg.Warnings, warnings...) } return cfg, nil diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a7c7d2ebf8..a0481c4ba5 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/uber/jaeger-client-go" ) func TestScheme(t *testing.T) { @@ -150,6 +151,68 @@ func TestLogEncoding(t *testing.T) { } } +func defaultConfig() *Config { + return &Config{ + Log: LogConfig{ + Level: "INFO", + Encoding: LogEncodingConsole, + GRPCLevel: "ERROR", + }, + + UI: UIConfig{ + Enabled: true, + }, + + Cors: CorsConfig{ + Enabled: false, + AllowedOrigins: []string{"*"}, + }, + + Cache: CacheConfig{ + Enabled: false, + Backend: CacheMemory, + TTL: 1 * time.Minute, + Memory: MemoryCacheConfig{ + EvictionInterval: 5 * time.Minute, + }, + Redis: RedisCacheConfig{ + Host: "localhost", + Port: 6379, + Password: "", + DB: 0, + }, + }, + + Server: ServerConfig{ + Host: "0.0.0.0", + Protocol: HTTP, + HTTPPort: 8080, + HTTPSPort: 443, + GRPCPort: 9000, + }, + + Tracing: TracingConfig{ + Jaeger: JaegerTracingConfig{ + Enabled: false, + Host: jaeger.DefaultUDPSpanServerHost, + Port: jaeger.DefaultUDPSpanServerPort, + }, + }, + + Database: DatabaseConfig{ + URL: "file:/var/opt/flipt/flipt.db", + MigrationsPath: "/etc/flipt/config/migrations", + MaxIdleConn: 2, + }, + + Meta: MetaConfig{ + CheckForUpdates: true, + TelemetryEnabled: true, + StateDirectory: "", + }, + } +} + func TestLoad(t *testing.T) { tests := []struct { name string @@ -160,18 +223,18 @@ func TestLoad(t *testing.T) { { name: "defaults", path: "./testdata/default.yml", - expected: Default, + expected: defaultConfig, }, { name: "deprecated - cache memory items defaults", path: "./testdata/deprecated/cache_memory_items.yml", - expected: Default, + expected: defaultConfig, }, { name: "deprecated - cache memory enabled", path: "./testdata/deprecated/cache_memory_enabled.yml", expected: func() *Config { - cfg := Default() + cfg := defaultConfig() cfg.Cache.Enabled = true cfg.Cache.Backend = CacheMemory cfg.Cache.TTL = -1 @@ -183,7 +246,7 @@ func TestLoad(t *testing.T) { name: "cache - no backend set", path: "./testdata/cache/default.yml", expected: func() *Config { - cfg := Default() + cfg := defaultConfig() cfg.Cache.Enabled = true cfg.Cache.Backend = CacheMemory cfg.Cache.TTL = 30 * time.Minute @@ -194,7 +257,7 @@ func TestLoad(t *testing.T) { name: "cache - memory", path: "./testdata/cache/memory.yml", expected: func() *Config { - cfg := Default() + cfg := defaultConfig() cfg.Cache.Enabled = true cfg.Cache.Backend = CacheMemory cfg.Cache.TTL = 5 * time.Minute @@ -206,7 +269,7 @@ func TestLoad(t *testing.T) { name: "cache - redis", path: "./testdata/cache/redis.yml", expected: func() *Config { - cfg := Default() + cfg := defaultConfig() cfg.Cache.Enabled = true cfg.Cache.Backend = CacheRedis cfg.Cache.TTL = time.Minute @@ -221,7 +284,7 @@ func TestLoad(t *testing.T) { name: "database key/value", path: "./testdata/database.yml", expected: func() *Config { - cfg := Default() + cfg := defaultConfig() cfg.Database = DatabaseConfig{ Protocol: DatabaseMySQL, Host: "localhost", @@ -274,7 +337,7 @@ func TestLoad(t *testing.T) { name: "advanced", path: "./testdata/advanced.yml", expected: func() *Config { - cfg := Default() + cfg := defaultConfig() cfg.Log = LogConfig{ Level: "WARN", File: "testLogFile.txt", @@ -356,7 +419,7 @@ func TestLoad(t *testing.T) { func TestServeHTTP(t *testing.T) { var ( - cfg = Default() + cfg = defaultConfig() req = httptest.NewRequest("GET", "http://example.com/foo", nil) w = httptest.NewRecorder() ) diff --git a/internal/config/cors.go b/internal/config/cors.go index 9a38705ac8..1acb7183b8 100644 --- a/internal/config/cors.go +++ b/internal/config/cors.go @@ -14,5 +14,6 @@ func (c *CorsConfig) viperKey() string { } func (c *CorsConfig) unmarshalViper(v *viper.Viper) (_ []string, _ error) { + v.SetDefault("allowed_origins", "*") return nil, v.Unmarshal(c) } diff --git a/internal/config/meta.go b/internal/config/meta.go index a263f8cbba..275a6fc9be 100644 --- a/internal/config/meta.go +++ b/internal/config/meta.go @@ -2,32 +2,19 @@ package config import "github.com/spf13/viper" -const ( - // configuration keys - metaCheckForUpdates = "meta.check_for_updates" - metaTelemetryEnabled = "meta.telemetry_enabled" - metaStateDirectory = "meta.state_directory" -) - // MetaConfig contains a variety of meta configuration fields. type MetaConfig struct { - CheckForUpdates bool `json:"checkForUpdates"` - TelemetryEnabled bool `json:"telemetryEnabled"` - StateDirectory string `json:"stateDirectory"` + CheckForUpdates bool `json:"checkForUpdates" mapstructure:"check_for_updates"` + TelemetryEnabled bool `json:"telemetryEnabled" mapstructure:"telemetry_enabled"` + StateDirectory string `json:"stateDirectory" mapstructure:"state_directory"` } -func (c *MetaConfig) init() (warnings []string, _ error) { - if viper.IsSet(metaCheckForUpdates) { - c.CheckForUpdates = viper.GetBool(metaCheckForUpdates) - } - - if viper.IsSet(metaTelemetryEnabled) { - c.TelemetryEnabled = viper.GetBool(metaTelemetryEnabled) - } - - if viper.IsSet(metaStateDirectory) { - c.StateDirectory = viper.GetString(metaStateDirectory) - } +func (c *MetaConfig) viperKey() string { + return "meta" +} - return +func (c *MetaConfig) unmarshalViper(v *viper.Viper) (warnings []string, _ error) { + v.SetDefault("check_for_updates", true) + v.SetDefault("telemetry_enabled", true) + return nil, v.Unmarshal(c) } diff --git a/internal/config/tracing.go b/internal/config/tracing.go index ceb8d76b88..cf934ed6c9 100644 --- a/internal/config/tracing.go +++ b/internal/config/tracing.go @@ -21,11 +21,9 @@ func (c *TracingConfig) viperKey() string { } func (c *TracingConfig) unmarshalViper(v *viper.Viper) ([]string, error) { - if v.IsSet("jaeger.enabled") { - v.SetDefault("jaeger", map[string]any{ - "host": "localhost", - "port": 6831, - }) - } + v.SetDefault("jaeger", map[string]any{ + "host": "localhost", + "port": 6831, + }) return nil, v.Unmarshal(c) } From 5a4079c9a47133555472ddfd56e5cfcfc04ed756 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Sat, 15 Oct 2022 12:08:53 +0100 Subject: [PATCH 09/12] refactor(config): simplify cache config unmarshal --- internal/config/cache.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/internal/config/cache.go b/internal/config/cache.go index d13ad3812d..f70c0968bf 100644 --- a/internal/config/cache.go +++ b/internal/config/cache.go @@ -45,26 +45,20 @@ func (c *CacheConfig) unmarshalViper(v *viper.Viper) (warnings []string, err err mem.SetDefault("eviction_interval", 5*time.Minute) // handle legacy memory structure if mem.GetBool("enabled") { + warnings = append(warnings, deprecatedMsgMemoryEnabled) // forcibly set top-level `enabled` to true v.Set("enabled", true) // ensure ttl is mapped to the value at memory.expiration v.RegisterAlias("ttl", "memory.expiration") - } - } - if err = v.Unmarshal(c, viper.DecodeHook(cacheDecodeHooks)); err != nil { - return - } - - if v.GetBool("memory.enabled") { // handle deprecated memory config - warnings = append(warnings, deprecatedMsgMemoryEnabled) + } - if v.IsSet("memory.expiration") { + if mem.IsSet("expiration") { warnings = append(warnings, deprecatedMsgMemoryExpiration) } } - return + return warnings, v.Unmarshal(c, viper.DecodeHook(cacheDecodeHooks)) } // CacheBackend is either memory or redis From 86d4c673ad5136ad0bb24bd5ba8bc7df481dd915 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Tue, 18 Oct 2022 13:58:57 +0100 Subject: [PATCH 10/12] refactor(config): separate unmarshalViper into setDefaults and validate --- internal/config/cache.go | 42 ++++++------- internal/config/config.go | 118 +++++++++++++++++++++++++----------- internal/config/cors.go | 11 ++-- internal/config/database.go | 44 ++++++-------- internal/config/log.go | 23 +++---- internal/config/meta.go | 13 ++-- internal/config/server.go | 39 +++++------- internal/config/tracing.go | 17 +++--- internal/config/ui.go | 12 ++-- 9 files changed, 165 insertions(+), 154 deletions(-) diff --git a/internal/config/cache.go b/internal/config/cache.go index f70c0968bf..fd735ae76d 100644 --- a/internal/config/cache.go +++ b/internal/config/cache.go @@ -4,16 +4,9 @@ import ( "encoding/json" "time" - "github.com/mitchellh/mapstructure" "github.com/spf13/viper" ) -var cacheDecodeHooks = mapstructure.ComposeDecodeHookFunc( - mapstructure.StringToTimeDurationHookFunc(), - mapstructure.StringToSliceHookFunc(","), - StringToEnumHookFunc(stringToCacheBackend), -) - // CacheConfig contains fields, which enable and configure // Flipt's various caching mechanisms. // @@ -26,31 +19,30 @@ type CacheConfig struct { Redis RedisCacheConfig `json:"redis,omitempty" mapstructure:"redis"` } -func (c *CacheConfig) viperKey() string { - return "cache" -} - -func (c *CacheConfig) unmarshalViper(v *viper.Viper) (warnings []string, err error) { - v.SetDefault("backend", CacheMemory) - v.SetDefault("ttl", 1*time.Minute) - v.SetDefault("redis", map[string]any{ - "host": "localhost", - "port": 6379, - }) - v.SetDefault("memory", map[string]any{ - "eviction_interval": 5 * time.Minute, +func (c *CacheConfig) setDefaults(v *viper.Viper) (warnings []string) { + v.SetDefault("cache", map[string]any{ + "backend": CacheMemory, + "ttl": 1 * time.Minute, + "redis": map[string]any{ + "host": "localhost", + "port": 6379, + }, + "memory": map[string]any{ + "eviction_interval": 5 * time.Minute, + }, }) - if mem := v.Sub("memory"); mem != nil { + if mem := v.Sub("cache.memory"); mem != nil { mem.SetDefault("eviction_interval", 5*time.Minute) // handle legacy memory structure if mem.GetBool("enabled") { warnings = append(warnings, deprecatedMsgMemoryEnabled) // forcibly set top-level `enabled` to true - v.Set("enabled", true) + v.Set("cache.enabled", true) // ensure ttl is mapped to the value at memory.expiration - v.RegisterAlias("ttl", "memory.expiration") - + v.RegisterAlias("cache.ttl", "cache.memory.expiration") + // ensure ttl default is set + v.SetDefault("cache.memory.expiration", 1*time.Minute) } if mem.IsSet("expiration") { @@ -58,7 +50,7 @@ func (c *CacheConfig) unmarshalViper(v *viper.Viper) (warnings []string, err err } } - return warnings, v.Unmarshal(c, viper.DecodeHook(cacheDecodeHooks)) + return } // CacheBackend is either memory or redis diff --git a/internal/config/config.go b/internal/config/config.go index c1588bbdd9..46ca55d60f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,60 +12,106 @@ import ( "golang.org/x/exp/constraints" ) +var decodeHooks = mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + StringToEnumHookFunc(stringToLogEncoding), + StringToEnumHookFunc(stringToCacheBackend), + StringToEnumHookFunc(stringToScheme), + StringToEnumHookFunc(stringToDatabaseProtocol), +) + +// Config contains all of Flipts configuration needs. +// +// The root of this structure contains a collection of sub-configuration categories, +// along with a set of warnings derived once the configuration has been loaded. +// +// Each sub-configuration (e.g. LogConfig) optionally implements either or both of +// the defaulter or validator interfaces. +// Given the sub-config implements a `setDefaults(*viper.Viper) []string` method +// then this will be called with the viper context before unmarshalling. +// This allows the sub-configuration to set any appropriate defaults. +// Given the sub-config implements a `validate() error` method +// then this will be called after unmarshalling, such that the function can emit +// any errors derived from the resulting state of the configuration. type Config struct { - Log LogConfig `json:"log,omitempty"` - UI UIConfig `json:"ui,omitempty"` - Cors CorsConfig `json:"cors,omitempty"` - Cache CacheConfig `json:"cache,omitempty"` - Server ServerConfig `json:"server,omitempty"` - Tracing TracingConfig `json:"tracing,omitempty"` - Database DatabaseConfig `json:"database,omitempty"` - Meta MetaConfig `json:"meta,omitempty"` + Log LogConfig `json:"log,omitempty" mapstructure:"log"` + UI UIConfig `json:"ui,omitempty" mapstructure:"ui"` + Cors CorsConfig `json:"cors,omitempty" mapstructure:"cors"` + Cache CacheConfig `json:"cache,omitempty" mapstructure:"cache"` + Server ServerConfig `json:"server,omitempty" mapstructure:"server"` + Tracing TracingConfig `json:"tracing,omitempty" mapstructure:"tracing"` + Database DatabaseConfig `json:"database,omitempty" mapstructure:"db"` + Meta MetaConfig `json:"meta,omitempty" mapstructure:"meta"` Warnings []string `json:"warnings,omitempty"` } func Load(path string) (*Config, error) { - viper.SetEnvPrefix("FLIPT") - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - viper.AutomaticEnv() + v := viper.New() + v.SetEnvPrefix("FLIPT") + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + v.AutomaticEnv() - viper.SetConfigFile(path) + v.SetConfigFile(path) - if err := viper.ReadInConfig(); err != nil { + if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("loading configuration: %w", err) } - cfg := &Config{} - - for _, unmarshaller := range []interface { - viperKey() string - unmarshalViper(*viper.Viper) (warnings []string, err error) - }{ - &cfg.Log, - &cfg.UI, - &cfg.Cors, - &cfg.Cache, - &cfg.Server, - &cfg.Tracing, - &cfg.Database, - &cfg.Meta, - } { - v := viper.Sub(unmarshaller.viperKey()) - if v == nil { - v = viper.New() - } + var ( + cfg = &Config{} + fields = cfg.fields() + ) + + // set viper defaults per field + for _, defaulter := range fields.defaulters { + cfg.Warnings = append(cfg.Warnings, defaulter.setDefaults(v)...) + } + + if err := v.Unmarshal(cfg, viper.DecodeHook(decodeHooks)); err != nil { + return nil, err + } - warnings, err := unmarshaller.unmarshalViper(v) - if err != nil { + // run any validation steps + for _, validator := range fields.validators { + if err := validator.validate(); err != nil { return nil, err } - - cfg.Warnings = append(cfg.Warnings, warnings...) } return cfg, nil } +type defaulter interface { + setDefaults(v *viper.Viper) []string +} + +type validator interface { + validate() error +} + +type fields struct { + defaulters []defaulter + validators []validator +} + +func (c *Config) fields() (fields fields) { + structVal := reflect.ValueOf(c).Elem() + for i := 0; i < structVal.NumField(); i++ { + field := structVal.Field(i).Addr().Interface() + + if defaulter, ok := field.(defaulter); ok { + fields.defaulters = append(fields.defaulters, defaulter) + } + + if validator, ok := field.(validator); ok { + fields.validators = append(fields.validators, validator) + } + } + + return +} + func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) { var ( out []byte diff --git a/internal/config/cors.go b/internal/config/cors.go index 1acb7183b8..6414a35757 100644 --- a/internal/config/cors.go +++ b/internal/config/cors.go @@ -9,11 +9,10 @@ type CorsConfig struct { AllowedOrigins []string `json:"allowedOrigins,omitempty" mapstructure:"allowed_origins"` } -func (c *CorsConfig) viperKey() string { - return "cors" -} +func (c *CorsConfig) setDefaults(v *viper.Viper) []string { + v.SetDefault("cors", map[string]any{ + "allowed_origins": "*", + }) -func (c *CorsConfig) unmarshalViper(v *viper.Viper) (_ []string, _ error) { - v.SetDefault("allowed_origins", "*") - return nil, v.Unmarshal(c) + return nil } diff --git a/internal/config/database.go b/internal/config/database.go index 7ae87065da..2eae6f2572 100644 --- a/internal/config/database.go +++ b/internal/config/database.go @@ -4,11 +4,12 @@ import ( "encoding/json" "time" - "github.com/mitchellh/mapstructure" "github.com/spf13/viper" ) const ( + defaultMigrationsPath = "/etc/flipt/config/migrations" + // database protocol enum _ DatabaseProtocol = iota // DatabaseSQLite ... @@ -19,12 +20,6 @@ const ( DatabaseMySQL ) -var dbProtocolDecodeHooks = mapstructure.ComposeDecodeHookFunc( - mapstructure.StringToTimeDurationHookFunc(), - mapstructure.StringToSliceHookFunc(","), - StringToEnumHookFunc(stringToDatabaseProtocol), -) - // DatabaseConfig contains fields, which configure the various relational database backends. // // Flipt currently supports SQLite, Postgres and MySQL backends. @@ -42,43 +37,44 @@ type DatabaseConfig struct { Protocol DatabaseProtocol `json:"protocol,omitempty" mapstructure:"protocol"` } -func (c *DatabaseConfig) viperKey() string { - return "db" -} - -func (c *DatabaseConfig) unmarshalViper(v *viper.Viper) (warnings []string, err error) { +func (c *DatabaseConfig) setDefaults(v *viper.Viper) []string { // supports nesting `path` beneath `migrations` key - v.RegisterAlias("migrations_path", "migrations.path") - v.SetDefault("migrations_path", "/etc/flipt/config/migrations") - v.SetDefault("max_idle_conn", 2) + v.RegisterAlias("db.migrations_path", "db.migrations.path") + + v.SetDefault("db", map[string]any{ + "migrations_path": defaultMigrationsPath, + "migrations": map[string]any{ + "path": defaultMigrationsPath, + }, + "max_idle_conn": 2, + }) // URL default is only set given that none of the alternative // database connections parameters are provided setDefaultURL := true for _, field := range []string{"name", "user", "password", "host", "port", "protocol"} { - setDefaultURL = setDefaultURL && !v.IsSet(field) + setDefaultURL = setDefaultURL && !v.IsSet("db."+field) } if setDefaultURL { - v.SetDefault("url", "file:/var/opt/flipt/flipt.db") + v.SetDefault("db.url", "file:/var/opt/flipt/flipt.db") } - if err = v.Unmarshal(c, viper.DecodeHook(dbProtocolDecodeHooks)); err != nil { - return - } + return nil +} - // validation +func (c *DatabaseConfig) validate() (err error) { if c.URL == "" { if c.Protocol == 0 { - return nil, errFieldRequired("database.protocol") + return errFieldRequired("db.protocol") } if c.Host == "" { - return nil, errFieldRequired("database.host") + return errFieldRequired("db.host") } if c.Name == "" { - return nil, errFieldRequired("database.name") + return errFieldRequired("db.name") } } diff --git a/internal/config/log.go b/internal/config/log.go index 5d22123b38..642d971244 100644 --- a/internal/config/log.go +++ b/internal/config/log.go @@ -3,7 +3,6 @@ package config import ( "encoding/json" - "github.com/mitchellh/mapstructure" "github.com/spf13/viper" ) @@ -16,22 +15,14 @@ type LogConfig struct { GRPCLevel string `json:"grpc_level,omitempty" mapstructure:"grpc_level"` } -var logDecodeHooks = mapstructure.ComposeDecodeHookFunc( - mapstructure.StringToTimeDurationHookFunc(), - mapstructure.StringToSliceHookFunc(","), - StringToEnumHookFunc(stringToLogEncoding), -) - -func (c *LogConfig) viperKey() string { - return "log" -} - -func (c *LogConfig) unmarshalViper(v *viper.Viper) (_ []string, _ error) { - v.SetDefault("level", "INFO") - v.SetDefault("encoding", "console") - v.SetDefault("grpc_level", "ERROR") +func (c *LogConfig) setDefaults(v *viper.Viper) []string { + v.SetDefault("log", map[string]any{ + "level": "INFO", + "encoding": "console", + "grpc_level": "ERROR", + }) - return nil, v.Unmarshal(c, viper.DecodeHook(logDecodeHooks)) + return nil } var ( diff --git a/internal/config/meta.go b/internal/config/meta.go index 275a6fc9be..93f3e67308 100644 --- a/internal/config/meta.go +++ b/internal/config/meta.go @@ -9,12 +9,11 @@ type MetaConfig struct { StateDirectory string `json:"stateDirectory" mapstructure:"state_directory"` } -func (c *MetaConfig) viperKey() string { - return "meta" -} +func (c *MetaConfig) setDefaults(v *viper.Viper) []string { + v.SetDefault("meta", map[string]any{ + "check_for_updates": true, + "telemetry_enabled": true, + }) -func (c *MetaConfig) unmarshalViper(v *viper.Viper) (warnings []string, _ error) { - v.SetDefault("check_for_updates", true) - v.SetDefault("telemetry_enabled", true) - return nil, v.Unmarshal(c) + return nil } diff --git a/internal/config/server.go b/internal/config/server.go index e78516f511..096d82e435 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -4,16 +4,9 @@ import ( "encoding/json" "os" - "github.com/mitchellh/mapstructure" "github.com/spf13/viper" ) -var serverDecodeHooks = mapstructure.ComposeDecodeHookFunc( - mapstructure.StringToTimeDurationHookFunc(), - mapstructure.StringToSliceHookFunc(","), - StringToEnumHookFunc(stringToScheme), -) - // ServerConfig contains fields, which configure both HTTP and gRPC // API serving. type ServerConfig struct { @@ -26,37 +19,35 @@ type ServerConfig struct { CertKey string `json:"certKey,omitempty" mapstructure:"cert_key"` } -func (c *ServerConfig) viperKey() string { - return "server" -} - -func (c *ServerConfig) unmarshalViper(v *viper.Viper) (warnings []string, err error) { - v.SetDefault("host", "0.0.0.0") - v.SetDefault("protocol", HTTP) - v.SetDefault("http_port", 8080) - v.SetDefault("https_port", 443) - v.SetDefault("grpc_port", 9000) +func (c *ServerConfig) setDefaults(v *viper.Viper) []string { + v.SetDefault("server", map[string]any{ + "host": "0.0.0.0", + "protocol": HTTP, + "http_port": 8080, + "https_port": 443, + "grpc_port": 9000, + }) - if err = v.Unmarshal(c, viper.DecodeHook(serverDecodeHooks)); err != nil { - return - } + return nil +} +func (c *ServerConfig) validate() (err error) { // validate configuration is as expected if c.Protocol == HTTPS { if c.CertFile == "" { - return nil, errFieldRequired("server.cert_file") + return errFieldRequired("server.cert_file") } if c.CertKey == "" { - return nil, errFieldRequired("server.cert_key") + return errFieldRequired("server.cert_key") } if _, err := os.Stat(c.CertFile); err != nil { - return nil, errFieldWrap("server.cert_file", err) + return errFieldWrap("server.cert_file", err) } if _, err := os.Stat(c.CertKey); err != nil { - return nil, errFieldWrap("server.cert_key", err) + return errFieldWrap("server.cert_key", err) } } diff --git a/internal/config/tracing.go b/internal/config/tracing.go index cf934ed6c9..540ad693ab 100644 --- a/internal/config/tracing.go +++ b/internal/config/tracing.go @@ -16,14 +16,13 @@ type TracingConfig struct { Jaeger JaegerTracingConfig `json:"jaeger,omitempty"` } -func (c *TracingConfig) viperKey() string { - return "tracing" -} - -func (c *TracingConfig) unmarshalViper(v *viper.Viper) ([]string, error) { - v.SetDefault("jaeger", map[string]any{ - "host": "localhost", - "port": 6831, +func (c *TracingConfig) setDefaults(v *viper.Viper) []string { + v.SetDefault("tracing", map[string]any{ + "jaeger": map[string]any{ + "host": "localhost", + "port": 6831, + }, }) - return nil, v.Unmarshal(c) + + return nil } diff --git a/internal/config/ui.go b/internal/config/ui.go index 3dce40eab7..ccb9bdcac8 100644 --- a/internal/config/ui.go +++ b/internal/config/ui.go @@ -8,12 +8,10 @@ type UIConfig struct { Enabled bool `json:"enabled" mapstructure:"enabled"` } -func (c *UIConfig) viperKey() string { - return "ui" -} - -func (c *UIConfig) unmarshalViper(v *viper.Viper) (_ []string, _ error) { - v.SetDefault("enabled", true) +func (c *UIConfig) setDefaults(v *viper.Viper) []string { + v.SetDefault("ui", map[string]any{ + "enabled": true, + }) - return nil, v.Unmarshal(c) + return nil } From 6a0add140741d003d7564af297666be51d93d7f3 Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Tue, 18 Oct 2022 14:01:36 +0100 Subject: [PATCH 11/12] refactor(config): unexport stringToEnumHookFunc --- internal/config/config.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 46ca55d60f..decae61a0f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,10 +15,10 @@ import ( var decodeHooks = mapstructure.ComposeDecodeHookFunc( mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToSliceHookFunc(","), - StringToEnumHookFunc(stringToLogEncoding), - StringToEnumHookFunc(stringToCacheBackend), - StringToEnumHookFunc(stringToScheme), - StringToEnumHookFunc(stringToDatabaseProtocol), + stringToEnumHookFunc(stringToLogEncoding), + stringToEnumHookFunc(stringToCacheBackend), + stringToEnumHookFunc(stringToScheme), + stringToEnumHookFunc(stringToDatabaseProtocol), ) // Config contains all of Flipts configuration needs. @@ -135,8 +135,8 @@ func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -// StringToEnumHookFunc returns a DecodeHookFunc that converts strings to a target enum -func StringToEnumHookFunc[T constraints.Integer](mappings map[string]T) mapstructure.DecodeHookFunc { +// stringToEnumHookFunc returns a DecodeHookFunc that converts strings to a target enum +func stringToEnumHookFunc[T constraints.Integer](mappings map[string]T) mapstructure.DecodeHookFunc { return func( f reflect.Type, t reflect.Type, From f029ca3be8dcf3da4c95b648be6b1134bea6817b Mon Sep 17 00:00:00 2001 From: George MacRorie Date: Tue, 18 Oct 2022 14:24:59 +0100 Subject: [PATCH 12/12] chore: cleanup for linters --- internal/config/config.go | 2 +- internal/config/cors.go | 3 +++ internal/config/database.go | 3 +++ internal/config/log.go | 3 +++ internal/config/meta.go | 3 +++ internal/config/server.go | 3 +++ internal/config/tracing.go | 3 +++ internal/config/ui.go | 3 +++ 8 files changed, 22 insertions(+), 1 deletion(-) diff --git a/internal/config/config.go b/internal/config/config.go index decae61a0f..3becdc8c38 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -148,7 +148,7 @@ func stringToEnumHookFunc[T constraints.Integer](mappings map[string]T) mapstruc return data, nil } - enum, _ := mappings[data.(string)] + enum := mappings[data.(string)] return enum, nil } diff --git a/internal/config/cors.go b/internal/config/cors.go index 6414a35757..c515e79777 100644 --- a/internal/config/cors.go +++ b/internal/config/cors.go @@ -2,6 +2,9 @@ package config import "github.com/spf13/viper" +// cheers up the unparam linter +var _ defaulter = (*CorsConfig)(nil) + // CorsConfig contains fields, which configure behaviour in the // HTTPServer relating to the CORS header-based mechanisms. type CorsConfig struct { diff --git a/internal/config/database.go b/internal/config/database.go index e680b43499..840d36d819 100644 --- a/internal/config/database.go +++ b/internal/config/database.go @@ -7,6 +7,9 @@ import ( "github.com/spf13/viper" ) +// cheers up the unparam linter +var _ defaulter = (*DatabaseConfig)(nil) + const ( defaultMigrationsPath = "/etc/flipt/config/migrations" diff --git a/internal/config/log.go b/internal/config/log.go index 642d971244..9abb33a366 100644 --- a/internal/config/log.go +++ b/internal/config/log.go @@ -6,6 +6,9 @@ import ( "github.com/spf13/viper" ) +// cheers up the unparam linter +var _ defaulter = (*LogConfig)(nil) + // LogConfig contains fields which control, direct and filter // the logging telemetry produces by Flipt. type LogConfig struct { diff --git a/internal/config/meta.go b/internal/config/meta.go index 93f3e67308..dd239481e9 100644 --- a/internal/config/meta.go +++ b/internal/config/meta.go @@ -2,6 +2,9 @@ package config import "github.com/spf13/viper" +// cheers up the unparam linter +var _ defaulter = (*MetaConfig)(nil) + // MetaConfig contains a variety of meta configuration fields. type MetaConfig struct { CheckForUpdates bool `json:"checkForUpdates" mapstructure:"check_for_updates"` diff --git a/internal/config/server.go b/internal/config/server.go index 096d82e435..6610376048 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -7,6 +7,9 @@ import ( "github.com/spf13/viper" ) +// cheers up the unparam linter +var _ defaulter = (*ServerConfig)(nil) + // ServerConfig contains fields, which configure both HTTP and gRPC // API serving. type ServerConfig struct { diff --git a/internal/config/tracing.go b/internal/config/tracing.go index 540ad693ab..16204e75b2 100644 --- a/internal/config/tracing.go +++ b/internal/config/tracing.go @@ -2,6 +2,9 @@ package config import "github.com/spf13/viper" +// cheers up the unparam linter +var _ defaulter = (*TracingConfig)(nil) + // JaegerTracingConfig contains fields, which configure specifically // Jaeger span and tracing output destination. type JaegerTracingConfig struct { diff --git a/internal/config/ui.go b/internal/config/ui.go index ccb9bdcac8..56da74efd3 100644 --- a/internal/config/ui.go +++ b/internal/config/ui.go @@ -2,6 +2,9 @@ package config import "github.com/spf13/viper" +// cheers up the unparam linter +var _ defaulter = (*UIConfig)(nil) + // UIConfig contains fields, which control the behaviour // of Flipt's user interface. type UIConfig struct {