Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bytes_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ wut
func TestUnstrBytesFile(t *testing.T) {
os.Setenv("TESTFILEBYTESUNSTR", "file::./testdata/bytes")
var val []byte
_, err := ReadStructuredCfg("env::TESTFILEBYTESUNSTR", &val)
err := ReadStructuredCfg("env::TESTFILEBYTESUNSTR", &val)
if err != nil {
log.Error(err)
}
Expand Down
142 changes: 60 additions & 82 deletions cfger.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,129 +40,101 @@ var (

// Reads an unstructured configuration-value. See ReadStructuredCfg.
func ReadCfg(val string) (string, error) {
return ReadStructuredCfg(val, nil)
var output string
return output, ReadStructuredCfg(val, &output)
}

// Check if the input has a prefix, e. g. "secret::" or "file::", if it does try to read the file in the given location.
// In the case of neither the input will be returned. If a prefix is given but an error occurs when reading, an empty
// string and the error will be returned. If no prefix is given the given val i returned.
// In the case of neither the input will be returned. If a prefix is given but an error occurs when reading, the error
// is returned,
//
// Valid prefixes are:
// 1. env:: reads from environment variables.
// 2. secret:: reads file from /run/secrets
// 3. file:: reads file with given path
//
// If not prefix is provided, the input structure type is checked. If a string pointer, the value of the pointer
// is set to the input.
//
// Additionally if a file has .yml or .json as a suffix and the given interface{} is not nil, the interface will be used
// to unmarshal the file at the given path.
//
// If a read environment-variable contains a prefix this function will be called with the environment-variable's value.
func ReadStructuredCfg(val string, structure interface{}) (string, error) {
func ReadStructuredCfg(val string, structure interface{}, populateRecursively ...bool) (error) {
if structure == nil {
return errors.New("no output structure provided")
} else if len(populateRecursively) > 1 {
return errors.New("more than one populateRecursively value passed")
}

switch strings.SplitN(val, "::", 2)[0] {
case "secret":
return ReadCfgFile(path.Join(secretRoot, val[secretPrefLen:]), structure)
return ReadCfgFile(path.Join(secretRoot, val[secretPrefLen:]), structure, populateRecursively...)
case "file":
return ReadCfgFile(val[filePrefLen:], structure)
return ReadCfgFile(val[filePrefLen:], structure, populateRecursively...)
case "env":
return ReadEnv(val[envPrefLen:], structure)
return ReadEnv(val[envPrefLen:], structure, populateRecursively...)
default:
return val, nil
switch v := structure.(type) {
case *string:
var t *string
t = structure.(*string)
*t = val
return nil
default:
return fmt.Errorf("unsupported structure type '%s'", v)
}
}
}

// Reads an environment-variable by the given name. If the environment-variable contains a prefix the path will be
// resolved.
func ReadEnv(val string, structure interface{}) (string, error) {
func ReadEnv(val string, structure interface{}, populateRecursively ...bool) (error) {
envVal, ok := os.LookupEnv(val)

if !ok {
return "", errors.New(fmt.Sprintf("Environment variable %q not found", val))
return errors.New(fmt.Sprintf("Environment variable %q not found", val))
}

return ReadStructuredCfg(envVal, structure)
return ReadStructuredCfg(envVal, structure, populateRecursively...)
}

// Reads the file at the given path and returns the contents as a string. If the suffix of the path is .yml/.yaml/.json
// the contents are unmarshalled before they are returned. Returns an empty string and an error if an error is returned
// the contents are unmarshalled before they are returned. Returns error if an error is returned
// while reading or unmarshalling.
func ReadCfgFile(inPath string, structure interface{}) (string, error) {
content, err := ioutil.ReadFile(inPath)
if err != nil {
return "", err
func ReadCfgFile(inPath string, structure interface{}, populateRecursively ...bool) (err error) {
if structure == nil {
return errors.New("no output structure provided")
}

_, ok := structure.(*[]byte)
if ok {
*structure.(*[]byte) = content
return "", nil
var content []byte
if content, err = ioutil.ReadFile(inPath); err != nil {
return
}

if structure != nil {
switch structure.(type) {
case *string:
var t *string
t = structure.(*string)
*t = string(content)
case *[]byte:
var b *[]byte
b = structure.(*[]byte)
*b = content
default:
if strings.HasSuffix(inPath, ".yml") || strings.HasSuffix(inPath, ".yaml") {
err = yaml.Unmarshal(content, structure)
return "", err
} else if strings.HasSuffix(inPath, ".json") {
err = json.Unmarshal(content, structure)
return "", err
} else {
err = errors.New("unsupported file type - supported types are 'yml' and 'json' ")
}
}
return string(content), nil
}

func ReadEnvRecursive(val string, structure interface{}) (string, error) {
envVal, ok := os.LookupEnv(val)

if !ok {
return "", errors.New(fmt.Sprintf("Environment variable %q not found", val))
}

return ReadStructuredCfgRecursive(envVal, structure)
}

func ReadStructuredCfgRecursive(val string, structure interface{}) (string, error) {
switch strings.SplitN(val, "::", 2)[0] {
case "secret":
return "", ReadStructuredCfgFileRecursive(path.Join(secretRoot, val[secretPrefLen:]), structure)
case "file":
return "", ReadStructuredCfgFileRecursive(val[filePrefLen:], structure)
case "env":
return ReadEnvRecursive(val[envPrefLen:], structure)
default:
return val, nil
}
}

func ReadStructuredCfgFileRecursive(inPath string, structure interface{}) error {
content, err := ioutil.ReadFile(inPath)
if err != nil {
return err
}

_, ok := structure.(*[]byte)
if ok {
*structure.(*[]byte) = content
return nil
}

if structure == nil {
return nil
}

if strings.HasSuffix(inPath, ".yml") || strings.HasSuffix(inPath, ".yaml") {
err = yaml.Unmarshal(content, structure)
} else if strings.HasSuffix(inPath, ".json") {
err = json.Unmarshal(content, structure)
}

if err != nil {
return err
}

err = findVal(structure, -1, []int{})
if err != nil {
return err
if err == nil && doRecurse(populateRecursively...) {
return findVal(structure, -1, []int{})
}
}

return nil
return
}

func findVal(structure interface{}, numFields int, fieldIndices []int) error {
Expand Down Expand Up @@ -196,13 +168,19 @@ func findVal(structure interface{}, numFields int, fieldIndices []int) error {
return err
}
} else if field.Kind() == reflect.String {
rv, err := ReadStructuredCfg(field.String(), nil)
//TODO: Generate test to check that this works.
var output string
err := ReadStructuredCfg(field.String(), &output)
if err != nil {
return err
}
field.SetString(rv)
field.SetString(output)
}
}
}
return nil
}

func doRecurse(populateRecursively ...bool) bool {
return len(populateRecursively) > 0 && populateRecursively[0]
}
27 changes: 24 additions & 3 deletions json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,22 @@ var factualStructured = jsonStruct{
},
}

type recurseJsonStruct struct {
Root struct {
Text string `json:"text"`
} `json:"root"`
}

func setupJSON() {
os.Setenv("TESTFILEJSON", "file::./testdata/test.json")
os.Setenv("RECURSE", "file::./testdata/recurse.json")
}

func TestJSON(t *testing.T) {
setupJSON()

a := jsonStruct{}
_, err := ReadStructuredCfg("env::TESTFILEJSON", &a)
err := ReadStructuredCfg("env::TESTFILEJSON", &a)
if err != nil {
log.Error(err)
}
Expand All @@ -63,7 +70,7 @@ func TestJSON(t *testing.T) {
log.Info("env::file:: json file to Go struct passed")

a = jsonStruct{}
_, err = ReadStructuredCfg("file::./testdata/test.json", &a)
err = ReadStructuredCfg("file::./testdata/test.json", &a)

if err != nil {
log.Error(err)
Expand All @@ -74,4 +81,18 @@ func TestJSON(t *testing.T) {
log.Fatal("Read from file failed with inequality-error")
}

}

var b = recurseJsonStruct{}

err = ReadStructuredCfg("env::RECURSE", &b, true)
if b.Root.Text != "data" {
t.Fatalf("Text was expected to be 'data', but was '%s'", b.Root.Text)
} else if err != nil {
t.Fatalf("Got err when none was expected: %s", err.Error())
}

err = ReadStructuredCfg("env::RECURSE", 0, true)
if err == nil {
t.Fatal("Wanted an err, but got none")
}
}
2 changes: 1 addition & 1 deletion tag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestTags(t *testing.T) {

var ello ValueBoi

_, err := ReadStructuredCfgRecursive("env::VAL", &ello)
err := ReadStructuredCfg("env::VAL", &ello, true)
if err != nil {
t.Fatal(err)
}
Expand Down
5 changes: 5 additions & 0 deletions testdata/recurse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"root": {
"text": "file::testdata/text.txt"
}
}
2 changes: 2 additions & 0 deletions testdata/recurse.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
root:
text: "file::testdata/text.txt"
1 change: 1 addition & 0 deletions testdata/text.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data
36 changes: 31 additions & 5 deletions yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package cfger

import (
log "github.com/sirupsen/logrus"
"testing"
"os"
"testing"
)

var factualYAMLStructured = yamlStruct{
Expand Down Expand Up @@ -32,7 +32,7 @@ var factualYAMLStructured = yamlStruct{

type yamlStruct struct {
Version string
Key1 struct {
Key1 struct {
Valkey1 struct {
Version int
}
Expand All @@ -43,16 +43,29 @@ type yamlStruct struct {
}
}

type recurseYamlStruct struct {
Root struct {
Text string `yaml:"text"`
} `yaml:"root"`
}

type recurseMixedStruct struct {
Root struct {
Data recurseJsonStruct `yaml:"data"`
} `yaml:"root"`
}

func setupYAML() {
os.Setenv("TESTFILE", "file::./testdata/test.yml")
os.Setenv("RECURSE", "file::testdata/recurse.yml")
os.Setenv("MIXED", "file::testdata/mixed.yml")
}

func TestYAML(t *testing.T) {
setupYAML()

a := yamlStruct{}
_, err := ReadStructuredCfg("env::TESTFILE", &a)
err := ReadStructuredCfg("env::TESTFILE", &a)
if err != nil {
log.Error(err)
}
Expand All @@ -64,15 +77,28 @@ func TestYAML(t *testing.T) {
log.Info("env::file:: yaml file to Go struct passed")

a = yamlStruct{}
_, err = ReadStructuredCfg("file::./testdata/test.yml", &a)
err = ReadStructuredCfg("file::./testdata/test.yml", &a)

if err != nil {
log.Error(err)
}
log.Info("file:: yaml file to Go struct passed")


if a != factualYAMLStructured {
log.Fatal("Read from file failed with inequality-error")
}

var b = recurseYamlStruct{}

err = ReadStructuredCfg("env::RECURSE", &b, true)
if b.Root.Text != "data" {
t.Fatalf("Text was expected to be 'data', but was '%s'", b.Root.Text)
} else if err != nil {
t.Fatalf("Got err when none was expected: %s", err.Error())
}

err = ReadStructuredCfg("env::RECURSE", 0, true)
if err == nil {
t.Fatal("Wanted an err, but got none")
}
}