Skip to content

Commit

Permalink
Custom env var names (#12)
Browse files Browse the repository at this point in the history
* Support of envvar tag

* Update documentation
  • Loading branch information
DenisPalnitsky authored Oct 8, 2022
1 parent ee6592f commit d717df0
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 26 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Config struct {

type Database struct {
Host string `default:"localhost" validate:"required"`
Password string `validate:"required"`
Password string `validate:"required" envvar:"DB_PASS"`
DbName string `default:"mydb"`
Username string `default:"root"`
Port int `default:"5432"`
Expand All @@ -46,7 +46,7 @@ func main() {
}

```
When you want to change, for example, DB Host of your applications you can do any of the following:
When you want to change, a DB Host of your applications you can do it in 3 ways:
1. create config `myconf.yaml` file in home directory
```
db:
Expand Down Expand Up @@ -101,6 +101,12 @@ To set a flag via environment variable, make all letters uppercase and replace '
You can set a prefix for environment variables. For example `NewConfReader("myconf").WithPrefix("MYAPP")` will search for environment variables like `MYAPP_DB_HOST`

Environment variable names could be set in the struct tag `envvar`. For example
```
Password string `envvar:"DB_PASS"`
```
will use value from environment variable `DB_PASS` to configure `Password` field.
### Command Line Arguments :computer:
To set a configuration field via command line argument you need to pass and argument prefixes wiht `--` and lowercase field name with path. Like `--db.host=localhost`
Expand Down
63 changes: 40 additions & 23 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,32 @@ import (
"github.com/spf13/viper"
)

/* ConfReader reads configuration from config file, environment variables or command line flags.
Flags have precedence over env vars and env vars have precedence over config file.
/*
ConfReader reads configuration from config file, environment variables or command line flags.
Flags have precedence over env vars and env vars have precedence over config file.
For example:
type NestedConf struct {
Foo string
}
type Config struct{
Nested NestedConf
}
type NestedConf struct {
Foo string
}
type Config struct{
Nested NestedConf
}
in that case flag --nested.foo will be mapped automatically
This flag could be also set by NESTED_FOO env var or by creating config file .ukama.yaml:
nested:
foo: bar
foo: bar
*/
type ConfReader struct {
viper *enviper.Enviper
configName string
configDirs []string
envVarPrefix string
Verbose bool
}

// NewConfReader creates new instance of ConfReader
Expand Down Expand Up @@ -115,12 +118,12 @@ func (c *ConfReader) Read(configStruct interface{}) error {

func (c *ConfReader) flagsBinding(conf interface{}) error {
t := reflect.TypeOf(conf)
m := map[string]*flagInfo{}
c.dumpStruct(t, "", m)
tagsInfo := map[string]*flagInfo{}
tagsInfo = c.dumpStruct(t, "", tagsInfo)

var flags = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)

for _, v := range m {
for _, v := range tagsInfo {
switch v.Type.Kind() {
case reflect.String:
flags.String(v.Name, v.DefaultVal, "")
Expand Down Expand Up @@ -175,13 +178,20 @@ func (c *ConfReader) flagsBinding(conf interface{}) error {
flags.BytesBase64(v.Name, []byte{}, "byte array in base64")
}
}

if v.EnvVar != "" {
err := c.viper.BindEnv(v.Name, v.EnvVar)
if err != nil {
return err
}
}
}

err := flags.Parse(os.Args[1:])
if err != nil {
return errors.Wrap(err, "failed to parse flags")
}
for k, v := range m {
for k, v := range tagsInfo {
f := flags.Lookup(v.Name)
if f != nil && f.Changed {
if v.Type.Kind() == reflect.Slice {
Expand Down Expand Up @@ -210,14 +220,17 @@ type flagInfo struct {
Name string
Type reflect.Type
DefaultVal string
EnvVar string
}

func (c *ConfReader) dumpStruct(t reflect.Type, path string, res map[string]*flagInfo) {
fmt.Printf("%s: %s", path, t.Name())
func (c *ConfReader) dumpStruct(t reflect.Type, path string, res map[string]*flagInfo) map[string]*flagInfo {
if c.Verbose {
fmt.Printf("%s: %s", path, t.Name())
}
switch t.Kind() {
case reflect.Ptr:
originalValue := t.Elem()
c.dumpStruct(originalValue, path, res)
res = c.dumpStruct(originalValue, path, res)

// If it is a struct we translate each field
case reflect.Struct:
Expand All @@ -228,36 +241,40 @@ func (c *ConfReader) dumpStruct(t reflect.Type, path string, res map[string]*fla
f.Type.Kind() != reflect.Func && f.Type.Kind() != reflect.Interface && f.Type.Kind() != reflect.UnsafePointer {

// do we have flag name override ?
val := f.Tag.Get("flag")
flagVal := f.Tag.Get("flag")
envVar := f.Tag.Get("envvar")

fieldPath := strings.TrimPrefix(strings.ToLower(path+"."+f.Name), ".")
if val != "" {
if flagVal != "" {
res[fieldPath] = &flagInfo{
Name: val,
Name: flagVal,
Type: f.Type,
DefaultVal: f.Tag.Get("default"),
EnvVar: envVar,
}
} else {
res[fieldPath] = &flagInfo{
Name: fieldPath,
Type: f.Type,
DefaultVal: f.Tag.Get("default"),
EnvVar: envVar,
}
}

} else if f.Type.Kind() == reflect.Struct || f.Type.Kind() == reflect.Ptr {
val := f.Tag.Get("mapstructure")
if strings.Contains(val, "squash") {
c.dumpStruct(f.Type, path, res)
res = c.dumpStruct(f.Type, path, res)
} else {
c.dumpStruct(f.Type, path+"."+f.Name, res)
res = c.dumpStruct(f.Type, path+"."+f.Name, res)
}
}
}

case reflect.Interface:
// Skipping interface
}

return res
}

func (c *ConfReader) WithSearchDirs(s ...string) *ConfReader {
Expand Down
7 changes: 6 additions & 1 deletion config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type LocalConfig struct {
FromConfig string
OverriddenByEvnVar string
OverriddenByArg string
EnvVarName string `envvar:"CUSTOM_ENV_VAR"`
}

func Test_ConfigReader(t *testing.T) {
Expand All @@ -48,6 +49,9 @@ func Test_ConfigReader(t *testing.T) {
os.Setenv("APP_OVERRIDDENBYEVNVAR", overriddenVar)
defer os.Unsetenv("APP_OVERRIDDENBYEVNVAR")

os.Setenv("CUSTOM_ENV_VAR", "valFromEnvVar")
defer os.Unsetenv("CUSTOM_ENV_VAR")

// act
err := confReader.Read(nc)

Expand All @@ -58,6 +62,7 @@ func Test_ConfigReader(t *testing.T) {
assert.Equal(t, "valFromConf", nc.App.FromConfig)
assert.Equal(t, valFromVar, nc.App.FromEnvVar)
assert.Equal(t, fromArgVal, nc.App.OverriddenByArg)
assert.Equal(t, "valFromEnvVar", nc.App.EnvVarName)
}
}

Expand Down Expand Up @@ -129,7 +134,7 @@ type dmSibling struct {
FromEnvVar string
}

func Test_DumpStruct(t *testing.T) {
func Test_dumpStruct(t *testing.T) {
m := map[string]*flagInfo{}
c := &ConfReader{}
c.dumpStruct(reflect.TypeOf(dmParent{}), "", m)
Expand Down

0 comments on commit d717df0

Please sign in to comment.