Skip to content

Commit

Permalink
Quarterdeck Config (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
bbengfort committed Sep 20, 2022
1 parent e5498ed commit 26beead
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 12 deletions.
67 changes: 55 additions & 12 deletions pkg/quarterdeck/config/config.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,68 @@
package config

import "github.com/rs/zerolog"
import (
"fmt"

"github.com/gin-gonic/gin"
"github.com/kelseyhightower/envconfig"
"github.com/rotationalio/ensign/pkg/utils/logger"
"github.com/rs/zerolog"
)

// Config uses envconfig to load the required settings from the environment, parse and
// validate them, loading defaults where necessary in preparation for running the
// Quarterdeck API service. This is the top-level config, all sub configurations need
// to be defined as properties of this Config.
type Config struct {
Maintenance bool `split_words:"true" default:"false"`
BindAddr string `split_words:"true" default:"8088"`
Mode string `split_words:"true" default:"release"`
LogLevel string `split_words:"true" default:"info"`
ConsoleLog bool `split_words:"true" default:"false"`
Maintenance bool `default:"false"` // $QUARTERDECK_MAINTENANCE
BindAddr string `split_words:"true" default:":8088"` // $QUARTERDECK_BIND_ADDR
Mode string `default:"release"` // $QUARTERDECK_MODE
LogLevel logger.LevelDecoder `split_words:"true" default:"info"` // $QUARTERDECK_LOG_LEVEL
ConsoleLog bool `split_words:"true" default:"false"` // $QUARTERDECK_CONSOLE_LOG
processed bool // set when the config is properly procesesed from the environment
}

func New() (Config, error) {
return Config{}, nil
// New loads and parses the config from the environment and validates it, marking it as
// processed so that external users can determine if the config is ready for use. This
// should be the only way Config objects are created for use in the application.
func New() (conf Config, err error) {
if err = envconfig.Process("quarterdeck", &conf); err != nil {
return Config{}, err
}

if err = conf.Validate(); err != nil {
return Config{}, err
}

conf.processed = true
return conf, nil
}

// Returns true if the config has not been correctly processed from the environment.
func (c Config) IsZero() bool {
// TODO: implement
return false
return !c.processed
}

// Mark a manually constructed config as processed as long as it is valid.
func (c Config) Mark() (_ Config, err error) {
if err = c.Validate(); err != nil {
return c, err
}
c.processed = true
return c, nil
}

// Custom validations are added here, particularly validations that require one or more
// fields to be processed before the validation occurs.
// NOTE: ensure that all nested config validation methods are called here.
func (c Config) Validate() (err error) {
if c.Mode != gin.ReleaseMode && c.Mode != gin.DebugMode && c.Mode != gin.TestMode {
return fmt.Errorf("invalid configuration: %q is not a valid gin mode", c.Mode)
}

return nil
}

func (c Config) GetLogLevel() zerolog.Level {
// TODO: implement
return zerolog.InfoLevel
return zerolog.Level(c.LogLevel)
}
132 changes: 132 additions & 0 deletions pkg/quarterdeck/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package config_test

import (
"os"
"testing"

"github.com/gin-gonic/gin"
"github.com/rotationalio/ensign/pkg/quarterdeck/config"
"github.com/rotationalio/ensign/pkg/utils/logger"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)

// The test environment for all config tests, manipulated using curEnv and setEnv
var testEnv = map[string]string{
"QUARTERDECK_MAINTENANCE": "false",
"QUARTERDECK_BIND_ADDR": ":3636",
"QUARTERDECK_MODE": gin.TestMode,
"QUARTERDECK_LOG_LEVEL": "error",
"QUARTERDECK_CONSOLE_LOG": "true",
}

func TestConfig(t *testing.T) {
// Set the required environment variables and cleanup after.
prevEnv := curEnv()
t.Cleanup(func() {
for key, val := range prevEnv {
if val != "" {
os.Setenv(key, val)
} else {
os.Unsetenv(key)
}
}
})
setEnv()

// At this point in the test, the environment should contain testEnv
conf, err := config.New()
require.NoError(t, err, "could not create a default config")
require.False(t, conf.IsZero(), "default config should be processed")

// Test the configuration
require.False(t, conf.Maintenance)
require.Equal(t, testEnv["QUARTERDECK_BIND_ADDR"], conf.BindAddr)
require.Equal(t, testEnv["QUARTERDECK_MODE"], conf.Mode)
require.Equal(t, zerolog.ErrorLevel, conf.GetLogLevel())
require.True(t, conf.ConsoleLog)
}

func TestValidation(t *testing.T) {
conf, err := config.New()
require.NoError(t, err, "could not create default config")

modes := []string{gin.ReleaseMode, gin.DebugMode, gin.TestMode}
for _, mode := range modes {
conf.Mode = mode
require.NoError(t, conf.Validate(), "expected config to be valid in %q mode", mode)
}

// Ensure conf is invalid on wrong mode
conf.Mode = "invalid"
require.EqualError(t, conf.Validate(), `invalid configuration: "invalid" is not a valid gin mode`, "expected gin mode validation error")
}

func TestIsZero(t *testing.T) {
// An empty config should always return IsZero
require.True(t, config.Config{}.IsZero(), "an empty config should always be zero valued")

// A processed config should not be zero valued
conf, err := config.New()
require.NoError(t, err, "should have been able to load the config")
require.False(t, conf.IsZero(), "expected a processed config to be non-zero valued")

// Custom config not processed
conf = config.Config{
Maintenance: false,
BindAddr: "127.0.0.1:0",
LogLevel: logger.LevelDecoder(zerolog.TraceLevel),
Mode: "invalid",
}
require.True(t, config.Config{}.IsZero(), "a non-empty config that isn't marked will be zero valued")

// Should not be able to mark a custom config that is invalid
conf, err = conf.Mark()
require.EqualError(t, err, `invalid configuration: "invalid" is not a valid gin mode`, "expected gin mode validation error")

// Should be able to mark a custom config that is valid as processed
conf.Mode = gin.ReleaseMode
conf, err = conf.Mark()
require.NoError(t, err, "should be able to mark a valid config")
require.False(t, conf.IsZero(), "a marked config should not be zero-valued")
}

// Returns the current environment for the specified keys, or if no keys are specified
// then returns the current environment for all keys in testEnv.
func curEnv(keys ...string) map[string]string {
env := make(map[string]string)

if len(keys) > 0 {
// Process the keys passed in by the user
for _, key := range keys {
if val, ok := os.LookupEnv(key); ok {
env[key] = val
}
}
} else {
// Process all the keys in testEnv
for key := range testEnv {
if val, ok := os.LookupEnv(key); ok {
env[key] = val
}
}
}

return env
}

// Sets the environment variables from the testEnv, if no keys are specified then sets
// all environment variables that are specified in the testEnv.
func setEnv(keys ...string) {
if len(keys) > 0 {
for _, key := range keys {
if val, ok := testEnv[key]; ok {
os.Setenv(key, val)
}
}
} else {
for key, val := range testEnv {
os.Setenv(key, val)
}
}
}

0 comments on commit 26beead

Please sign in to comment.