Browse Source

initial commit, one test fails which is expected

master
forestjohnson 5 years ago
commit
038b7cc2bf
3 changed files with 337 additions and 0 deletions
  1. +7
    -0
      ReadMe.md
  2. +158
    -0
      override.go
  3. +172
    -0
      override_test.go

+ 7
- 0
ReadMe.md View File

@ -0,0 +1,7 @@
Uses reflection to override values in an arbitrary object based on environment variables, following the [convention outlined by Grafana/InfluxData](http://docs.grafana.org/installation/configuration/#using-environment-variables).
Ripped from [influxdb source code](
https://github.com/influxdata/influxdb/blob/77e2c80a4f220770a2da00bc1ff048c762f8cc66/cmd/influxd/run/config.go#L182) with some modifications.
See the test for a usage example.

+ 158
- 0
override.go View File

@ -0,0 +1,158 @@
package influxStyleEnvOverride
import (
"fmt"
"os"
"reflect"
"strconv"
"strings"
"time"
)
type keyValueRetriever interface {
get(key string) string
}
type environmentVariableKeyValueRetriever struct{}
func (this environmentVariableKeyValueRetriever) get(key string) string {
return os.Getenv(key)
}
// ApplyEnvOverrides apply any convention-driven environment varibles on top of the original object.
func ApplyInfluxStyleEnvOverrides(prefix string, originalObject *interface{}) error {
return applyEnvOverrides(environmentVariableKeyValueRetriever{}, prefix, reflect.ValueOf(originalObject))
}
func applyEnvOverrides(kv keyValueRetriever, prefix string, spec reflect.Value) error {
// If we have a pointer, dereference it
s := spec
if spec.Kind() == reflect.Ptr {
s = spec.Elem()
}
// Make sure we have struct
if s.Kind() != reflect.Struct {
return nil
}
typeOfSpec := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
// Get the toml tag to determine what env var name to use
configName := typeOfSpec.Field(i).Tag.Get("toml")
if configName == "" {
configName = typeOfSpec.Field(i).Tag.Get("json")
}
if configName == "" {
configName = typeOfSpec.Field(i).Name
}
// Replace hyphens with underscores to avoid issues with shells
configName = strings.Replace(configName, "-", "_", -1)
fieldKey := typeOfSpec.Field(i).Name
// Use the upper-case prefix and toml name for the env var
key := strings.ToUpper(configName)
if prefix != "" {
key = strings.ToUpper(fmt.Sprintf("%s_%s", prefix, configName))
}
//fmt.Printf("%v, %v, %v\n", key, f.Kind(), f.CanSet())
// If it's a sub-config, recursively apply
if f.Kind() == reflect.Struct || f.Kind() == reflect.Ptr {
if err := applyEnvOverrides(kv, key, f); err != nil {
return err
}
continue
}
// Skip any fields that we cannot set
canSet := f.CanSet() || f.Kind() == reflect.Slice
value := kv.get(key)
if !canSet && value != "" {
//fmt.Printf("failed to apply %v to %v: %v is not settable according to golang reflection", key, fieldKey, fieldKey)
return fmt.Errorf("failed to apply %v to %v: %v is not settable according to golang reflection", key, fieldKey, fieldKey)
}
if canSet {
//fmt.Printf("%v=%v\n", key, value)
}
if canSet {
// If the type is s slice, apply to each using the index as a suffix
// e.g. GRAPHITE_0
if f.Kind() == reflect.Slice || f.Kind() == reflect.Array {
for i := 0; i < f.Len(); i++ {
if err := applyEnvOverrides(kv, key, f.Index(i)); err != nil {
return err
}
if err := applyEnvOverrides(kv, fmt.Sprintf("%s_%d", key, i), f.Index(i)); err != nil {
return err
}
}
continue
}
// Skip any fields we don't have a value to set
if value == "" {
continue
}
switch f.Kind() {
case reflect.String:
f.SetString(value)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
var intValue int64
// Handle toml.Duration
if f.Type().Name() == "Duration" {
dur, err := time.ParseDuration(value)
if err != nil {
return fmt.Errorf("failed to apply %v to %v using type %v and value '%v'", key, fieldKey, f.Type().String(), value)
}
intValue = dur.Nanoseconds()
} else {
var err error
intValue, err = strconv.ParseInt(value, 0, f.Type().Bits())
if err != nil {
return fmt.Errorf("failed to apply %v to %v using type %v and value '%v'", key, fieldKey, f.Type().String(), value)
}
}
f.SetInt(intValue)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
var intValue uint64
var err error
intValue, err = strconv.ParseUint(value, 0, f.Type().Bits())
if err != nil {
return fmt.Errorf("failed to apply %v to %v using type %v and value '%v'", key, fieldKey, f.Type().String(), value)
}
f.SetUint(intValue)
case reflect.Bool:
boolValue, err := strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("failed to apply %v to %v using type %v and value '%v'", key, fieldKey, f.Type().String(), value)
}
f.SetBool(boolValue)
case reflect.Float32, reflect.Float64:
floatValue, err := strconv.ParseFloat(value, f.Type().Bits())
if err != nil {
return fmt.Errorf("failed to apply %v to %v using type %v and value '%v'", key, fieldKey, f.Type().String(), value)
}
f.SetFloat(floatValue)
default:
if err := applyEnvOverrides(kv, key, f); err != nil {
return err
}
}
}
}
return nil
}

+ 172
- 0
override_test.go View File

@ -0,0 +1,172 @@
package influxStyleEnvOverride
import (
"encoding/json"
"reflect"
"strings"
"testing"
)
type ExampleObject struct {
A string
Other *ExampleSubObject
Others []ExampleSubObject
}
type ExampleSubObject struct {
Integer int
B string
unexported int
Thing interface{}
Things []interface{}
}
type mockKeyValueRetriever struct {
KeyValues map[string]string
}
func (this mockKeyValueRetriever) get(key string) string {
return this.KeyValues[key]
}
type testCase struct {
mutateExampleObject func(*ExampleObject)
environment map[string]string
expectedError string
}
func TestApplyEnvOverridesBasic(t *testing.T) {
toTest := testCase{
mutateExampleObject: func(example *ExampleObject) {
example.A = "asd2"
example.Other.B = "asd2"
example.Other.Integer = 10
things0 := example.Others[0].Things[0].(ExampleSubObject)
things0.B = "asd2"
example.Others[0].B = "asd2"
thing := example.Others[0].Thing.(ExampleSubObject)
thing.B = "asd2"
},
environment: map[string]string{
"TEST_A": "asd2",
"TEST_OTHER_B": "asd2",
"TEST_OTHER_INTEGER": "10",
"TEST_OTHERS_0_THINGS_0_B": "asd2",
"TEST_OTHERS_0_B": "asd2",
"TEST_OTHERS_0_THING_B": "asd2",
},
}
toTest.execute(t)
}
func TestApplyEnvOverridesWithInvalidInteger(t *testing.T) {
toTest := testCase{
mutateExampleObject: func(example *ExampleObject) {
},
environment: map[string]string{
"TEST_OTHER_INTEGER": "o no",
},
expectedError: "failed to apply TEST_OTHER_INTEGER to Integer",
}
toTest.execute(t)
}
func TestApplyEnvOverridesWithUnsettableField(t *testing.T) {
toTest := testCase{
mutateExampleObject: func(example *ExampleObject) {
},
environment: map[string]string{
"TEST_OTHERS_0_UNEXPORTED": "o no",
},
expectedError: "is not settable",
}
toTest.execute(t)
}
// Note currently this test is expected to fail. Haven't implemented additional slice elements yet.
func TestApplyEnvOverridesWithNonExistentObject(t *testing.T) {
toTest := testCase{
mutateExampleObject: func(example *ExampleObject) {
example.Others = append(example.Others, ExampleSubObject{
B: "asd2",
})
},
environment: map[string]string{
"TEST_OTHERS_1_B": "asd2",
},
}
toTest.execute(t)
}
func (this testCase) execute(t *testing.T) {
ExampleObjectUnderTest := newExampleObject()
ExampleObjectForComparison := newExampleObject()
exampleKeyValueRetriever := mockKeyValueRetriever{
KeyValues: this.environment,
}
err := applyEnvOverrides(exampleKeyValueRetriever, "TEST", reflect.ValueOf(&ExampleObjectUnderTest))
this.mutateExampleObject(&ExampleObjectForComparison)
if err != nil || this.expectedError != "" {
actualError := ""
if err != nil {
actualError = err.Error()
}
if !strings.Contains(actualError, this.expectedError) || this.expectedError == "" {
expectedErrorDisplay := "nil"
if this.expectedError != "" {
expectedErrorDisplay = this.expectedError
}
t.Errorf("Expected Error: %s, Actual Error: %s", expectedErrorDisplay, actualError)
return
}
}
jsonA, err := json.MarshalIndent(ExampleObjectUnderTest, "", " ")
if err != nil {
t.Error(err)
}
jsonB, err := json.MarshalIndent(ExampleObjectForComparison, "", " ")
if err != nil {
t.Error(err)
}
if string(jsonA) != string(jsonB) {
t.Errorf("Expected Value: \n%s,\n Actual Value: \n%s\n\n", string(jsonB), string(jsonA))
}
}
func newExampleObject() ExampleObject {
toReturn := ExampleObject{
A: "asd",
Other: &ExampleSubObject{
B: "bsd",
Integer: 2,
},
Others: []ExampleSubObject{
ExampleSubObject{
B: "bsd",
Thing: ExampleSubObject{
B: "bsd",
},
},
},
}
toReturn.Others[0].Things = make([]interface{}, 0)
toReturn.Others[0].Things = append(
toReturn.Others[0].Things,
ExampleSubObject{
B: "bsd",
},
)
return toReturn
}

Loading…
Cancel
Save