Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# v1.2.0

- check vault server availability at startup
- add option to completely disable vault source
- test fixes
- update README.md

# v1.1.0

- add ability to find from certain file sources
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,9 @@ Environment file may be any supported file extension - `.json`, `.yaml` (`.yml`)

#### Vault

If you create `vault.EXT` file then fields from that file will proceed to lookup from vault server. Loading vault variables is dynamic too both the same with environment. The field has special syntax `mount_path,secret_path,secret_key` where are listed vault secret mount path, secret path, and secret key separated by comma. Suppose we have vault server with configured postgresql secret and started on `http://localhost:8200`. Lets load for example `username` and `password` keys to our config. And suppose for our simple example that vault setup has the `root` token to access. All again: mount - secret, path - postgresql, keys - username, password. Create directory `config` and place `vault.toml` file in to it with contents:
If you create `vault.EXT` file then fields from that file will proceed to lookup from vault server. When *goconfig* instance was created then vault domain will be checkup. If you want to disable vault lookup on some configurations then use `goconfig.vault.enable` system option. All vault system options will be explained below.

Loading vault variables is dynamic too both the same with environment. The field has special syntax `mount_path,secret_path,secret_key` where are listed vault secret mount path, secret path, and secret key separated by comma. Suppose we have vault server with configured postgresql secret and started on `http://localhost:8200`. Lets load for example `username` and `password` keys to our config. And suppose for our simple example that vault setup has the `root` token to access. All again: mount - secret, path - postgresql, keys - username, password. Create directory `config` and place `vault.toml` file in to it with contents:

```toml
[postgresql]
Expand Down Expand Up @@ -245,12 +247,15 @@ So, before we can use vault keys we must configure vault client. There are two w
```toml
[goconfig.vault]
address = "http://127.0.0.1:8200"
enable = false
min_retry_wait = "3"
max_retry_wait = "5"
max_retries = "10"
timeout = "30"
```

You can specify optional `enable` param to turn off vault on specific configuration environment.

Params `min_retry_wait`, `max_retry_wait`, `max_retries` and `timeout` must be specified as strings. Their values treated as seconds when specified without postfix. Otherwise the value will be supplied to `time.ParseDuration` function.

##### Vault Authorization
Expand Down Expand Up @@ -286,7 +291,7 @@ It is not acceptable in many cases to provide credentials directly to file. So y

```toml
[goconfig.vault.auth]
token = "VAULT_TOKEN"
token = "CUSTOM_VAULT_TOKEN"
```

And so on for other cases.
Expand Down
27 changes: 2 additions & 25 deletions cmd/goconfig/goconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -551,24 +551,8 @@ func TestGoconfigVerboseError(t *testing.T) {
func TestGoconfigVaultTokenError(t *testing.T) {
t.Parallel()

ctx := context.Background()

vaultServer := vaultMock.NewServer("root")
t.Cleanup(vaultServer.Close)
vaultClient := vaultMock.NewClient(vaultServer.URL, "root", vaultServer.Client())

err := vaultClient.WriteSecret(ctx, "secret", "goconfig_cmd_secret", map[string]any{
"password": "abc123",
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
err = vaultClient.DeleteSecret(ctx, "secret", "goconfig_cmd_secret")
if err != nil {
t.Fatal(err)
}
})

d := TmpConfigDir(t)

Expand All @@ -577,7 +561,7 @@ func TestGoconfigVaultTokenError(t *testing.T) {
CreateConfigFile(d, "vault.toml", `password="secret,goconfig_cmd_secret"`)

testCases := [][]string{
{"go", "run", "./goconfig.go", "--config=" + d, "--get=password", "--token", "root1"},
{"go", "run", "./goconfig.go", "--config=" + d, "--get=password", "--token", "broken_token"},
}

for i, testCase := range testCases {
Expand All @@ -589,16 +573,9 @@ func TestGoconfigVaultTokenError(t *testing.T) {
cmd.Stderr = &stderr

err := cmd.Run()
if err != nil {
if err == nil {
t.Fatal(err, stderr.String())
}

if stdout.String() != `qwerty123456` {
t.Fatal(stdout.String())
}
if stderr.String() != "" {
t.Fatal(stderr.String())
}
})
}
}
18 changes: 13 additions & 5 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"github.com/boolka/goconfig/pkg/entry"
tokenRoleAuth "github.com/boolka/goconfig/pkg/vault"
vault "github.com/hashicorp/vault/api"
appRoleAuth "github.com/hashicorp/vault/api/auth/approle"
userPassAuth "github.com/hashicorp/vault/api/auth/userpass"
Expand Down Expand Up @@ -150,6 +151,10 @@ func New(ctx context.Context, options Options) (cfg *Config, err error) {
sources = append(sources, dirSources...)
}

slices.SortFunc(sources, func(a, b configEntry) int {
return int(b.source) - int(a.source)
})

for i, source := range sources {
var replaceEntry entry.Entry
var src cfgSource
Expand All @@ -167,6 +172,13 @@ func New(ctx context.Context, options Options) (cfg *Config, err error) {
// load vault config from source files
vaultCfg := loadVaultConfig(ctx, vault.DefaultConfig(), sources)

// vault disabled
if vaultCfg == nil {
// remove correspond file from sources
sources = slices.Delete(sources, i, i+1)
continue
}

// set goconfig logger
if options.Logger != nil {
vaultCfg.Logger = options.Logger
Expand All @@ -186,7 +198,7 @@ func New(ctx context.Context, options Options) (cfg *Config, err error) {

switch authType {
case tokenVaultConfigAuthType:
vaultClient.SetToken(creds[0])
vaultAuth = tokenRoleAuth.NewTokenAuth(creds[0])
case appRoleVaultConfigAuthType:
roleId := creds[0]
secretId := creds[1]
Expand Down Expand Up @@ -232,10 +244,6 @@ func New(ctx context.Context, options Options) (cfg *Config, err error) {
}
}

slices.SortFunc(sources, func(a, b configEntry) int {
return int(b.source) - int(a.source)
})

if logger != nil && logger.Enabled(ctx, slog.LevelDebug) {
for i, source := range sources {
logger.DebugContext(ctx, fmt.Sprintf(`%d loaded source, file: "%s", cfgSource: %d`, i, source.file, source.source))
Expand Down
8 changes: 4 additions & 4 deletions pkg/config/config_private_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ func TestConfigSourcesPrecedence(t *testing.T) {
t.Fatal("incorrect order of sources")
}

if len(cfg.sources) != 14 {
if len(cfg.sources) != 13 {
for i, source := range cfg.sources {
t.Error(i, source.file)
}
t.Fatal("expected", 14, "found", len(cfg.sources))
t.Fatal("expected", 13, "found", len(cfg.sources))
}
}

Expand All @@ -59,10 +59,10 @@ func TestConfigFileSystem(t *testing.T) {
t.Fatal("incorrect order of sources")
}

if len(cfg.sources) != 14 {
if len(cfg.sources) != 13 {
for i, source := range cfg.sources {
t.Error(i, source.file)
}
t.Fatal("expected", 14, "found", len(cfg.sources))
t.Fatal("expected", 13, "found", len(cfg.sources))
}
}
4 changes: 0 additions & 4 deletions pkg/config/config_test/testdata/config/vault.toml

This file was deleted.

1 change: 1 addition & 0 deletions pkg/config/config_test/testdata/vault/config/vault.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
password1 = "secret,goconfig_secret_1"
broken_field = "broken_value"

[userpass]
password2 = "secret,goconfig_secret_1,password2"
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/config_test/testdata/vault/disabled/default.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[goconfig.vault]
enable = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[goconfig.vault]
enable = true
5 changes: 5 additions & 0 deletions pkg/config/config_test/testdata/vault/disabled/vault.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[goconfig.vault]
address = "http://127.0.0.1:8200"

[goconfig.vault.auth]
token = "invalid_token"
2 changes: 1 addition & 1 deletion pkg/config/config_test/testdata/vault/token/env.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[goconfig.vault.auth]
token = "VAULT_TOKEN"
token = "CUSTOM_VAULT_TOKEN"
38 changes: 37 additions & 1 deletion pkg/config/config_test/vault_auth_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestVaultTokenAuthFromFile(t *testing.T) {
ctx := context.Background()
prepareVaultSecret(ctx, t, "root")

t.Setenv("VAULT_TOKEN", "root")
t.Setenv("CUSTOM_VAULT_TOKEN", "root")

cfg, err := config.New(ctx, config.Options{
Directory: "testdata/vault/token",
Expand Down Expand Up @@ -142,3 +142,39 @@ func TestVaultMustGetCertainSource(t *testing.T) {

cfg.MustGet(ctx, "password1", "vault")
}

func TestVaultInitializationInvalidToken(t *testing.T) {
ctx := context.Background()

t.Setenv("CUSTOM_VAULT_TOKEN", "invalid_token")

_, err := config.New(ctx, config.Options{
Directory: "testdata/vault/token",
})
if err == nil {
t.Fatal(err)
}
}

func TestVaultInitializationDisableVault(t *testing.T) {
ctx := context.Background()

_, err := config.New(ctx, config.Options{
Directory: "testdata/vault/disabled",
})
if err != nil {
t.Fatal(err)
}
}

func TestVaultInitializationEnableVault(t *testing.T) {
ctx := context.Background()

_, err := config.New(ctx, config.Options{
Directory: "testdata/vault/disabled",
Deployment: "production",
})
if err == nil {
t.Fatal(err)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
//go:build vault

package config_test

import (
"bytes"
"context"
"log/slog"
"strings"
"testing"
"time"

Expand All @@ -13,8 +12,6 @@ import (
)

func TestVaultConfig(t *testing.T) {
t.Parallel()

ctx := context.Background()

cfg, err := config.New(ctx, config.Options{
Expand Down Expand Up @@ -53,45 +50,27 @@ func TestVaultConfig(t *testing.T) {
}

func TestUnavailableServer(t *testing.T) {
t.Parallel()

buf := bytes.NewBuffer([]byte{})

ctx := context.Background()

_, err := config.New(ctx, config.Options{
Directory: "testdata/vault/unauthorized",
Logger: slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{
Level: slog.LevelDebug,
})),
})
if err != entry.ErrVaultUnauthorized {
t.Fatal(err)
}
}

func TestBrokenPath(t *testing.T) {
t.Setenv("TEST_FILE_VAULT", "config_file_vault_custom")

buf := bytes.NewBuffer([]byte{})

ctx := context.Background()

cfg, err := config.New(ctx, config.Options{
Directory: "testdata/config",
Logger: slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{
Level: slog.LevelDebug,
})),
Directory: "testdata/vault/config",
})
if err != nil {
t.Fatal(err)
}

if v, ok := cfg.Get(ctx, "vault"); !ok || v != "config_file_vault_custom" {
if v, ok := cfg.Get(ctx, "broken_field"); ok && v != entry.ErrVaultInvalidPath {
t.Fatal(v, ok)
}

if !strings.Contains(buf.String(), "invalid vault path") {
t.Fatal("valid path")
}
}
6 changes: 6 additions & 0 deletions pkg/config/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import (
)

func loadVaultConfig(ctx context.Context, config *vault.Config, entries []configEntry) *vault.Config {
if isEnable, ok := searchThroughSources(ctx, entries, "goconfig.vault.enable"); ok {
if isEnable, ok := isEnable.(bool); ok && !isEnable {
return nil
}
}

if address, ok := searchThroughSources(ctx, entries, "goconfig.vault.address"); ok {
if address, ok := address.(string); ok {
config.Address = address
Expand Down
5 changes: 5 additions & 0 deletions pkg/entry/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ func NewVault(ctx context.Context, entry Entry, client *vault.Client, auth vault
}
}

_, err := client.Auth().Token().LookupSelf()
if err != nil {
return nil, err
}

return &VaultEntry{
client: client,
entry: entry,
Expand Down
18 changes: 11 additions & 7 deletions pkg/entry/vault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,10 @@ func TestVaultBrokenTokenAuth(t *testing.T) {

client.SetToken("broken")

v, err := NewVault(ctx, tomlEntry, client, nil)
if err != nil {
_, err = NewVault(ctx, tomlEntry, client, nil)
if err == nil {
t.Fatal(err)
}

if v, ok := v.Get(ctx, "password1"); ok {
t.Fatal(v, ok)
}
}

func TestVaultUserPassAuth(t *testing.T) {
Expand Down Expand Up @@ -544,6 +540,14 @@ func TestVaultAppRoleAuthDenied(t *testing.T) {
func TestVaultBrokenPath(t *testing.T) {
ctx := context.Background()

vaultServer := vaultMock.NewServer("root")
t.Cleanup(func() {
vaultServer.Close()
})

vaultCfg := vault.DefaultConfig()
vaultCfg.Address = vaultServer.URL

f, err := os.Open("./testdata/vault.toml")
if err != nil {
t.Fatal(err)
Expand All @@ -557,7 +561,7 @@ func TestVaultBrokenPath(t *testing.T) {
t.Fatal(err)
}

client, err := vault.NewClient(nil)
client, err := vault.NewClient(vaultCfg)
if err != nil {
t.Fatal(err)
}
Expand Down
Loading