internal/config: Switch to using struct scoped viper instance (#332)

* Switch to using struct scoped viper instance

* Rename NewXXXOptions

* Handle unchecked errors from viper.BindEnv
This commit is contained in:
Travis Groth 2019-10-01 18:16:36 -04:00 committed by GitHub
parent 5df0ff500c
commit 251ab0d527
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 98 additions and 52 deletions

View file

@ -25,7 +25,8 @@ import (
// DisableHeaderKey is the key used to check whether to disable setting header
const DisableHeaderKey = "disable"
// Options are the global environmental flags used to set up pomerium's services.
// Options are the global environmental flags used to set up pomerium's services. Use NewXXXOptions() methods
// for a safely initialized data structure.
type Options struct {
// Debug outputs human-readable logs to Stdout.
Debug bool `mapstructure:"pomerium_debug"`
@ -141,6 +142,9 @@ type Options struct {
// GRPC Service Settings
GRPCClientTimeout time.Duration `mapstructure:"grpc_client_timeout"`
GRPCClientDNSRoundRobin bool `mapstructure:"grpc_client_dns_roundrobin"`
// Scoped viper instance
viper *viper.Viper
}
var defaultOptions = Options{
@ -171,41 +175,63 @@ var defaultOptions = Options{
GRPCClientDNSRoundRobin: true,
}
// NewOptions returns a minimal options configuration built from default options.
// NewOptions creates a new Options struct with only viper initialized
func NewOptions() *Options {
o := Options{}
o.viper = viper.New()
return &o
}
// NewDefaultOptions returns an Options struct with defaults set and viper initialized
func NewDefaultOptions() *Options {
o := defaultOptions
o.viper = viper.New()
return &o
}
// NewMinimalOptions returns a minimal options configuration built from default options.
// Any modifications to the structure should be followed up by a subsequent
// call to validate.
func NewOptions(authenticateURL, authorizeURL string) (*Options, error) {
o := defaultOptions
func NewMinimalOptions(authenticateURL, authorizeURL string) (*Options, error) {
o := NewDefaultOptions()
o.AuthenticateURLString = authenticateURL
o.AuthorizeURLString = authorizeURL
if err := o.Validate(); err != nil {
return nil, fmt.Errorf("internal/config: validation error %s", err)
}
return &o, nil
return o, nil
}
// OptionsFromViper builds the main binary's configuration
// options by parsing environmental variables and config file
func OptionsFromViper(configFile string) (*Options, error) {
// start a copy of the default options
o := defaultOptions
o := NewDefaultOptions()
// New viper instance to save into Options later
v := viper.New()
// Load up config
o.bindEnvs()
err := bindEnvs(o, v)
if err != nil {
return nil, fmt.Errorf("failed to bind options to env vars: %w", err)
}
if configFile != "" {
viper.SetConfigFile(configFile)
if err := viper.ReadInConfig(); err != nil {
v.SetConfigFile(configFile)
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("internal/config: failed to read config: %s", err)
}
}
if err := viper.Unmarshal(&o); err != nil {
if err := v.Unmarshal(&o); err != nil {
return nil, fmt.Errorf("internal/config: failed to unmarshal config: %s", err)
}
o.viper = v
if err := o.Validate(); err != nil {
return nil, fmt.Errorf("internal/config: validation error %s", err)
}
return &o, nil
return o, nil
}
// Validate ensures the Options fields are properly formed, present, and hydrated.
@ -270,7 +296,7 @@ func (o *Options) parsePolicy() error {
if err := yaml.Unmarshal(policyBytes, &policies); err != nil {
return fmt.Errorf("could not unmarshal policy yaml: %s", err)
}
} else if err := viper.UnmarshalKey("policy", &policies); err != nil {
} else if err := o.viper.UnmarshalKey("policy", &policies); err != nil {
return err
}
if len(policies) != 0 {
@ -291,7 +317,7 @@ func (o *Options) parseHeaders() error {
var headers map[string]string
if o.HeadersEnv != "" {
// Handle JSON by default via viper
if headers = viper.GetStringMapString("HeadersEnv"); len(headers) == 0 {
if headers = o.viper.GetStringMapString("HeadersEnv"); len(headers) == 0 {
// Try to parse "Key1:Value1,Key2:Value2" syntax
headerSlice := strings.Split(o.HeadersEnv, ",")
for n := range headerSlice {
@ -307,29 +333,41 @@ func (o *Options) parseHeaders() error {
}
o.Headers = headers
} else if viper.IsSet("headers") {
if err := viper.UnmarshalKey("headers", &headers); err != nil {
return fmt.Errorf("header %s failed to parse: %s", viper.Get("headers"), err)
} else if o.viper.IsSet("headers") {
if err := o.viper.UnmarshalKey("headers", &headers); err != nil {
return fmt.Errorf("header %s failed to parse: %s", o.viper.Get("headers"), err)
}
o.Headers = headers
}
return nil
}
// bindEnvs makes sure viper binds to each env var based on the mapstructure tag
func (o *Options) bindEnvs() {
// bindEnvs binds a viper instance to each env var of an Options struct based on the mapstructure tag
func bindEnvs(o *Options, v *viper.Viper) error {
tagName := `mapstructure`
t := reflect.TypeOf(*o)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
envName := field.Tag.Get(tagName)
viper.BindEnv(envName)
err := v.BindEnv(envName)
if err != nil {
return fmt.Errorf("failed to bind field '%s' to env var '%s': %w", field.Name, envName, err)
}
}
// Statically bind fields
viper.BindEnv("PolicyEnv", "POLICY")
viper.BindEnv("HeadersEnv", "HEADERS")
err := v.BindEnv("PolicyEnv", "POLICY")
if err != nil {
return fmt.Errorf("failed to bind field 'PolicyEnv' to env var 'POLICY': %w", err)
}
err = v.BindEnv("HeadersEnv", "HEADERS")
if err != nil {
return fmt.Errorf("failed to bind field 'HeadersEnv' to env var 'HEADERS': %w", err)
}
return nil
}
// OptionsUpdater updates local state based on an Options struct