diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a57bb30..b673462 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,27 @@ name: CI -on: - push: - branches: [ main, dev ] - pull_request: - branches: [ main, dev ] +on: ["push", "pull_request"] jobs: build: runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, 'ci skip')" + steps: - - uses: actions/checkout@v2 + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v2 - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.16 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 - - name: Build - run: go build -v ./... + - name: Build + run: go build -v ./... - - name: Test - run: go test -v ./... + - name: Test + run: go test -v ./... diff --git a/LICENSE b/LICENSE index d9c14b3..0ce0358 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 golang-config +Copyright (c) 2021 gopher-lib Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b433151..ee9a15a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # [config](https://github.com/golang-config/config): Eloquent configuration for Golang apps. ## Features: + +- Substitutes `$VARIABLE` and `${VARIABLE}` with variables found in a shell environment. +- By default loads `.env`, but with optional agruments to `config.LoadFile` can load other files. diff --git a/config.go b/config.go index acc486c..f70288b 100644 --- a/config.go +++ b/config.go @@ -2,55 +2,54 @@ package config import ( "fmt" + "io" "os" "path/filepath" - "regexp" "strings" "github.com/joho/godotenv" "github.com/spf13/viper" ) -// Load loads configuration from provided file, interpolates -// environement variables and then unmarshals it into provided struct. -func Load(rawVal interface{}, filename string, envPath ...string) error { +func LoadFile(rawVal interface{}, filename string, envPath ...string) error { if len(envPath) > 0 && envPath[0] != "" { if err := godotenv.Load(envPath[0]); err != nil { - return fmt.Errorf("failed to load env. file: %v", err) + return fmt.Errorf("config: failed to load env. file: %v", err) } } else { // Ignore error as we are loading default env. file. _ = godotenv.Load(".env") } + f, err := os.Open(filename) + if err != nil { + return fmt.Errorf("config: failed to open config file: %v", err) + } + return Load(f, rawVal, strings.TrimPrefix(filepath.Ext(filename), ".")) +} - viper.SetConfigName(strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filepath.Base(filename)))) - viper.SetConfigType("yaml") - viper.AddConfigPath(filepath.Dir(filename)) - err := viper.ReadInConfig() // Find and read the config file +func Load(in io.Reader, rawVal interface{}, configType string) error { + viper.SetConfigType(configType) + err := viper.ReadConfig(in) if err != nil { - return fmt.Errorf("failed to read in config: %w", err) + return fmt.Errorf("config: failed to read in config: %w", err) } - replaceEnv(viper.AllKeys()) + for _, key := range viper.AllKeys() { + newKey := os.Expand(viper.GetString(key), mapping) + viper.Set(key, newKey) + } err = viper.Unmarshal(&rawVal) if err != nil { - return fmt.Errorf("failed to unmarshal config: %w", err) + return fmt.Errorf("config: failed to unmarshal config: %w", err) } return nil } -var validEnv = regexp.MustCompile(`^\$\{[a-zA-Z_]+[a-zA-Z0-9_]*\}$`) - -func replaceEnv(keys []string) { - for _, key := range keys { - old := viper.GetString(key) - var new string - if validEnv.MatchString(old) { - new = os.Getenv(old[2 : len(old)-1]) - } else { - new = old - } - viper.Set(key, new) +// mapping is second argument for os.Expand function. +func mapping(s string) string { + if strings.HasPrefix(s, "$") { + return s } + return os.Getenv(s) } diff --git a/config_test.go b/config_test.go index dbf24fe..b46736b 100644 --- a/config_test.go +++ b/config_test.go @@ -1,43 +1,67 @@ package config import ( + "os" "reflect" + "strings" "testing" + + "github.com/spf13/viper" ) +func TestLoadFile(t *testing.T) { + viper.Reset() + + type db struct { + User string + Password string + } + type config struct { + Port int + AuthSecret string + Secret string + Dollar string + DB db + ConnectionString string + } + var conf config + err := LoadFile(&conf, "./testdata/config.testing.yaml", "./testdata/.env.testing") + if err != nil { + t.Fatal(err) + } + expected := config{8080, "secret", "", "$dollar", db{"root", "admin"}, "root:admin@tcp(localhost:3306)/core?parseTime=true"} + if !reflect.DeepEqual(conf, expected) { + t.Errorf("not equal: %v != %v", conf, expected) + } +} + func TestLoad(t *testing.T) { - t.Run("simple config with interpolation", func(t *testing.T) { - type Config struct { - Port int - Secret1 string - Secret2 string - } - var conf Config - err := Load(&conf, "testdata/config.testing.yaml", "testdata/.env.testing") - if err != nil { - t.Fatalf("error loading config: %v\n", err) - } - expected := Config{5432, "secret-value", ""} - if !reflect.DeepEqual(conf, expected) { - t.Errorf("not equal: %v != %v", conf, expected) - } - }) + t.Run("variable substitution", func(t *testing.T) { + viper.Reset() - t.Run("more complex config with interpolation", func(t *testing.T) { + os.Setenv("DB_PASSWORD", "root") + const confStr = ` +port: 1234 +db: + user: postgres + password: ${DB_PASSWORD} +empty: emp${T}ty +` type DB struct { User string Password string } type Config struct { - Port int - DB DB + Port int + DB DB + Empty string } var conf Config - err := Load(&conf, "testdata/config.testing.yaml", "testdata/.env.testing") + err := Load(strings.NewReader(confStr), &conf, "yaml") if err != nil { - t.Fatalf("error loading config: %v\n", err) + t.Fatal(err) } - expected := Config{5432, DB{"admin", "root"}} + expected := Config{1234, DB{"postgres", "root"}, "empty"} if !reflect.DeepEqual(conf, expected) { t.Errorf("not equal: %v != %v", conf, expected) } diff --git a/testdata/.env.testing b/testdata/.env.testing index d9eab58..a936591 100644 --- a/testdata/.env.testing +++ b/testdata/.env.testing @@ -1,2 +1,6 @@ -SOME_SECRET=secret-value -DB_PASSWORD=root \ No newline at end of file +AUTH_SECRET=secret +DB_USER=root +DB_PASSWORD=admin +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=core \ No newline at end of file diff --git a/testdata/config.testing.yaml b/testdata/config.testing.yaml index bfa7b65..4a4265b 100644 --- a/testdata/config.testing.yaml +++ b/testdata/config.testing.yaml @@ -1,6 +1,8 @@ -port: 5432 -secret1: ${SOME_SECRET} -secret2: ${SECRET} +port: 8080 +authSecret: ${AUTH_SECRET} +secret: ${SECRET} +dollar: $$dollar db: - user: admin + user: root password: ${DB_PASSWORD} +connectionString: "${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME}?parseTime=true"