Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(config): ensure all expected env vars are bound #1113

Merged
merged 2 commits into from Nov 2, 2022

Conversation

GeorgeMac
Copy link
Contributor

@GeorgeMac GeorgeMac commented Nov 2, 2022

While pairing with @markphelps yesterday, we discovered some fields were not getting set via env variables.
This appears to be a regression, since the refactor I did to leverage viper.Unmarshal.

Upon investigating I stumbled across spf13/viper#761 which gave lots of insight into what we're seeing.

Initially, this PR adds a test which runs over each case already in TestLoad.
Instead of just checking loading from YAML works, it also builds up a set of env var keys based on these config files.
Then, using these keys, it runs the test a second time.
This time, configuring the environment to match the same content of the YAML.

Each case added to TestLoad will attempt to validate both the config YAML and equivalent environment variables, to produce the same config result.

If I run this test without the accompanying fix, I get:

--- FAIL: TestLoad (0.02s)
    --- FAIL: TestLoad/deprecated_-_cache_memory_enabled_(YAML) (0.00s)
        config_test.go:433: 
            	Error Trace:	/Users/georgemac/github/flipt-io/flipt/internal/config/config_test.go:433
            	Error:      	Not equal: 
            	            	expected: &config.Config{Log:config.LogConfig{Level:"INFO", File:"", Encoding:0x1, GRPCLevel:"ERROR"}, UI:config.UIConfig{Enabled:true}, Cors:config.CorsConfig{Enabled:false, AllowedOrigins:[]string{"*"}}, Cache:config.CacheConfig{Enabled:true, TTL:-1000000000, Backend:0x1, Memory:config.MemoryCacheConfig{EvictionInterval:300000000000}, Redis:config.RedisCacheConfig{Host:"localhost", Port:6379, Password:"", DB:0}}, Server:config.ServerConfig{Host:"0.0.0.0", Protocol:0x0, HTTPPort:8080, HTTPSPort:443, GRPCPort:9000, CertFile:"", CertKey:""}, Tracing:config.TracingConfig{Jaeger:config.JaegerTracingConfig{Enabled:false, Host:"localhost", Port:6831}}, Database:config.DatabaseConfig{URL:"file:/var/opt/flipt/flipt.db", MaxIdleConn:2, MaxOpenConn:0, ConnMaxLifetime:0, Name:"", User:"", Password:"", Host:"", Port:0, Protocol:0x0}, Meta:config.MetaConfig{CheckForUpdates:true, TelemetryEnabled:true, StateDirectory:""}, Warnings:[]string{"'cache.memory.enabled' is deprecated and will be removed in a future version. Please use 'cache.backend' and 'cache.enabled' instead.", "'cache.memory.expiration' is deprecated and will be removed in a future version. Please use 'cache.ttl' instead."}}
            	            	actual  : &config.Config{Log:config.LogConfig{Level:"INFO", File:"", Encoding:0x1, GRPCLevel:"ERROR"}, UI:config.UIConfig{Enabled:true}, Cors:config.CorsConfig{Enabled:false, AllowedOrigins:[]string{"*"}}, Cache:config.CacheConfig{Enabled:true, TTL:-1, Backend:0x1, Memory:config.MemoryCacheConfig{EvictionInterval:300000000000}, Redis:config.RedisCacheConfig{Host:"localhost", Port:6379, Password:"", DB:0}}, Server:config.ServerConfig{Host:"0.0.0.0", Protocol:0x0, HTTPPort:8080, HTTPSPort:443, GRPCPort:9000, CertFile:"", CertKey:""}, Tracing:config.TracingConfig{Jaeger:config.JaegerTracingConfig{Enabled:false, Host:"localhost", Port:6831}}, Database:config.DatabaseConfig{URL:"file:/var/opt/flipt/flipt.db", MaxIdleConn:2, MaxOpenConn:0, ConnMaxLifetime:0, Name:"", User:"", Password:"", Host:"", Port:0, Protocol:0x0}, Meta:config.MetaConfig{CheckForUpdates:true, TelemetryEnabled:true, StateDirectory:""}, Warnings:[]string{"'cache.memory.enabled' is deprecated and will be removed in a future version. Please use 'cache.backend' and 'cache.enabled' instead.", "'cache.memory.expiration' is deprecated and will be removed in a future version. Please use 'cache.ttl' instead."}}
            	            	
            	            	Diff:
            	            	--- Expected
            	            	+++ Actual
            	            	@@ -18,3 +18,3 @@
            	            	   Enabled: (bool) true,
            	            	-  TTL: (time.Duration) -1000000000,
            	            	+  TTL: (time.Duration) -1,
            	            	   Backend: (config.CacheBackend) 1,
            	Test:       	TestLoad/deprecated_-_cache_memory_enabled_(YAML)
    --- FAIL: TestLoad/deprecated_-_cache_memory_enabled_(ENV) (0.00s)
        config_test.go:466: 
            	Error Trace:	/Users/georgemac/github/flipt-io/flipt/internal/config/config_test.go:466
            	Error:      	Not equal: 
            	            	expected: &config.Config{Log:config.LogConfig{Level:"INFO", File:"", Encoding:0x1, GRPCLevel:"ERROR"}, UI:config.UIConfig{Enabled:true}, Cors:config.CorsConfig{Enabled:false, AllowedOrigins:[]string{"*"}}, Cache:config.CacheConfig{Enabled:true, TTL:-1000000000, Backend:0x1, Memory:config.MemoryCacheConfig{EvictionInterval:300000000000}, Redis:config.RedisCacheConfig{Host:"localhost", Port:6379, Password:"", DB:0}}, Server:config.ServerConfig{Host:"0.0.0.0", Protocol:0x0, HTTPPort:8080, HTTPSPort:443, GRPCPort:9000, CertFile:"", CertKey:""}, Tracing:config.TracingConfig{Jaeger:config.JaegerTracingConfig{Enabled:false, Host:"localhost", Port:6831}}, Database:config.DatabaseConfig{URL:"file:/var/opt/flipt/flipt.db", MaxIdleConn:2, MaxOpenConn:0, ConnMaxLifetime:0, Name:"", User:"", Password:"", Host:"", Port:0, Protocol:0x0}, Meta:config.MetaConfig{CheckForUpdates:true, TelemetryEnabled:true, StateDirectory:""}, Warnings:[]string{"'cache.memory.enabled' is deprecated and will be removed in a future version. Please use 'cache.backend' and 'cache.enabled' instead.", "'cache.memory.expiration' is deprecated and will be removed in a future version. Please use 'cache.ttl' instead."}}
            	            	actual  : &config.Config{Log:config.LogConfig{Level:"INFO", File:"", Encoding:0x1, GRPCLevel:"ERROR"}, UI:config.UIConfig{Enabled:true}, Cors:config.CorsConfig{Enabled:false, AllowedOrigins:[]string{"*"}}, Cache:config.CacheConfig{Enabled:false, TTL:60000000000, Backend:0x1, Memory:config.MemoryCacheConfig{EvictionInterval:300000000000}, Redis:config.RedisCacheConfig{Host:"localhost", Port:6379, Password:"", DB:0}}, Server:config.ServerConfig{Host:"0.0.0.0", Protocol:0x0, HTTPPort:8080, HTTPSPort:443, GRPCPort:9000, CertFile:"", CertKey:""}, Tracing:config.TracingConfig{Jaeger:config.JaegerTracingConfig{Enabled:false, Host:"localhost", Port:6831}}, Database:config.DatabaseConfig{URL:"file:/var/opt/flipt/flipt.db", MaxIdleConn:2, MaxOpenConn:0, ConnMaxLifetime:0, Name:"", User:"", Password:"", Host:"", Port:0, Protocol:0x0}, Meta:config.MetaConfig{CheckForUpdates:true, TelemetryEnabled:true, StateDirectory:""}, Warnings:[]string(nil)}
            	            	
            	            	Diff:
            	            	--- Expected
            	            	+++ Actual
            	            	@@ -17,4 +17,4 @@
            	            	  Cache: (config.CacheConfig) {
            	            	-  Enabled: (bool) true,
            	            	-  TTL: (time.Duration) -1000000000,
            	            	+  Enabled: (bool) false,
            	            	+  TTL: (time.Duration) 60000000000,
            	            	   Backend: (config.CacheBackend) 1,
            	            	@@ -63,6 +63,3 @@
            	            	  },
            	            	- Warnings: ([]string) (len=2) {
            	            	-  (string) (len=133) "'cache.memory.enabled' is deprecated and will be removed in a future version. Please use 'cache.backend' and 'cache.enabled' instead.",
            	            	-  (string) (len=112) "'cache.memory.expiration' is deprecated and will be removed in a future version. Please use 'cache.ttl' instead."
            	            	- }
            	            	+ Warnings: ([]string) <nil>
            	            	 })
            	Test:       	TestLoad/deprecated_-_cache_memory_enabled_(ENV)
    --- FAIL: TestLoad/cache_-_redis_(ENV) (0.00s)
        config_test.go:466: 
            	Error Trace:	/Users/georgemac/github/flipt-io/flipt/internal/config/config_test.go:466
            	Error:      	Not equal: 
            	            	expected: &config.Config{Log:config.LogConfig{Level:"INFO", File:"", Encoding:0x1, GRPCLevel:"ERROR"}, UI:config.UIConfig{Enabled:true}, Cors:config.CorsConfig{Enabled:false, AllowedOrigins:[]string{"*"}}, Cache:config.CacheConfig{Enabled:true, TTL:60000000000, Backend:0x2, Memory:config.MemoryCacheConfig{EvictionInterval:300000000000}, Redis:config.RedisCacheConfig{Host:"localhost", Port:6378, Password:"s3cr3t!", DB:1}}, Server:config.ServerConfig{Host:"0.0.0.0", Protocol:0x0, HTTPPort:8080, HTTPSPort:443, GRPCPort:9000, CertFile:"", CertKey:""}, Tracing:config.TracingConfig{Jaeger:config.JaegerTracingConfig{Enabled:false, Host:"localhost", Port:6831}}, Database:config.DatabaseConfig{URL:"file:/var/opt/flipt/flipt.db", MaxIdleConn:2, MaxOpenConn:0, ConnMaxLifetime:0, Name:"", User:"", Password:"", Host:"", Port:0, Protocol:0x0}, Meta:config.MetaConfig{CheckForUpdates:true, TelemetryEnabled:true, StateDirectory:""}, Warnings:[]string(nil)}
            	            	actual  : &config.Config{Log:config.LogConfig{Level:"INFO", File:"", Encoding:0x1, GRPCLevel:"ERROR"}, UI:config.UIConfig{Enabled:true}, Cors:config.CorsConfig{Enabled:false, AllowedOrigins:[]string{"*"}}, Cache:config.CacheConfig{Enabled:true, TTL:60000000000, Backend:0x2, Memory:config.MemoryCacheConfig{EvictionInterval:300000000000}, Redis:config.RedisCacheConfig{Host:"localhost", Port:6378, Password:"", DB:0}}, Server:config.ServerConfig{Host:"0.0.0.0", Protocol:0x0, HTTPPort:8080, HTTPSPort:443, GRPCPort:9000, CertFile:"", CertKey:""}, Tracing:config.TracingConfig{Jaeger:config.JaegerTracingConfig{Enabled:false, Host:"localhost", Port:6831}}, Database:config.DatabaseConfig{URL:"file:/var/opt/flipt/flipt.db", MaxIdleConn:2, MaxOpenConn:0, ConnMaxLifetime:0, Name:"", User:"", Password:"", Host:"", Port:0, Protocol:0x0}, Meta:config.MetaConfig{CheckForUpdates:true, TelemetryEnabled:true, StateDirectory:""}, Warnings:[]string(nil)}
            	            	
            	            	Diff:
            	            	--- Expected
            	            	+++ Actual
            	            	@@ -26,4 +26,4 @@
            	            	    Port: (int) 6378,
            	            	-   Password: (string) (len=7) "s3cr3t!",
            	            	-   DB: (int) 1
            	            	+   Password: (string) "",
            	            	+   DB: (int) 0
            	            	   }
            	Test:       	TestLoad/cache_-_redis_(ENV)
    --- FAIL: TestLoad/database_key/value_(ENV) (0.00s)
        config_test.go:463: 
            	Error Trace:	/Users/georgemac/github/flipt-io/flipt/internal/config/config_test.go:463
            	Error:      	Received unexpected error:
            	            	field "db.protocol": non-empty value is required
            	Test:       	TestLoad/database_key/value_(ENV)
    --- FAIL: TestLoad/server_-_https_defined_but_not_found_cert_file_(ENV) (0.00s)
        config_test.go:458: field "server.cert_file": non-empty value is required
        config_test.go:459: 
            	Error Trace:	/Users/georgemac/github/flipt-io/flipt/internal/config/config_test.go:459
            	Error:      	Target error should be in err chain:
            	            	expected: "file does not exist"
            	            	in chain: "field \"server.cert_file\": non-empty value is required"
            	            		"non-empty value is required"
            	Test:       	TestLoad/server_-_https_defined_but_not_found_cert_file_(ENV)
    --- FAIL: TestLoad/server_-_https_defined_but_not_found_cert_key_(ENV) (0.00s)
        config_test.go:458: field "server.cert_file": non-empty value is required
        config_test.go:459: 
            	Error Trace:	/Users/georgemac/github/flipt-io/flipt/internal/config/config_test.go:459
            	Error:      	Target error should be in err chain:
            	            	expected: "file does not exist"
            	            	in chain: "field \"server.cert_file\": non-empty value is required"
            	            		"non-empty value is required"
            	Test:       	TestLoad/server_-_https_defined_but_not_found_cert_key_(ENV)
    --- FAIL: TestLoad/advanced_(ENV) (0.00s)
        config_test.go:463: 
            	Error Trace:	/Users/georgemac/github/flipt-io/flipt/internal/config/config_test.go:463
            	Error:      	Received unexpected error:
            	            	field "server.cert_file": non-empty value is required
            	Test:       	TestLoad/advanced_(ENV)
FAIL
FAIL	go.flipt.io/flipt/internal/config	0.173s
FAIL

The PR also delivers on the fix. It follows one of the strategies outlined in the issue above.
One way to attempt to fix this is via SetDefault() for all potential fields.

This has two problems:

  1. Easy to forget in the future and miss again.
  2. SetDefault is misbehaving sometimes, with IsSet leading to faults in the db config.

The better approach is to use both viper.SetDefault for deprecated keys and viper.BindEnv with every key that has a presence in the unmarshal config struct.

This PR does just that. It extends the reflection code used to support adding defaults and validation methods to config structs. It descends into each struct and registers each struct tag appropriately via BindEnv.

Now the tests are passing appropriately.

for _, env := range backup {
key, value, _ := strings.Cut(env, "=")
os.Setenv(key, value)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ love all this

Copy link
Collaborator

@markphelps markphelps left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amazing work. thank you for the investigation and tests/fix

@GeorgeMac GeorgeMac merged commit cfcad38 into main Nov 2, 2022
@GeorgeMac GeorgeMac deleted the gm/test-config-auto-env branch November 2, 2022 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants