From 5b9f7674f4d7846d8809e7d2943523c9a529c770 Mon Sep 17 00:00:00 2001 From: Derek Tamsen Date: Fri, 2 Apr 2021 13:06:28 -0700 Subject: [PATCH] feat: add support for hashicorp vault dynamic database secrets --- README.md | 15 ++++++++++ pkg/core/types.go | 11 ++++---- pkg/providers/hashicorp_vault.go | 19 +++++++++++-- pkg/providers/hashicorp_vault_test.go | 40 ++++++++++++++++++++------- pkg/teller.go | 8 ++++++ pkg/teller_test.go | 23 ++++++++++----- 6 files changed, 91 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 8732815f..b2ca6cef 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,8 @@ We use the following general structure to specify sync mapping for all providers # you can use either `env_sync` or `env` or both env_sync: path: ... # path to mapping + remap: + PROVIDER_VAR1: VAR3 # Maps PROVIDER_VAR1 to local env var VAR3 env: VAR1: path: ... # path to value or mapping @@ -190,6 +192,19 @@ env: path: ... ``` +### Remapping Provider Variables + +Providers which support syncing a list of keys and values can be remapped to different environment variable keys. Typically, when teller syncs paths from `env_sync`, the key returned from the provider is directly mapped to the environment variable key. In some cases it might be necessary to have the provider key mapped to a different variable without changing the provider settings. This can be useful when using `env_sync` for [Hashicorp Vault Dynamic Database credentials](https://www.vaultproject.io/docs/secrets/databases): + +```yaml +env_sync: + path: database/roles/my-role + remap: + username: PGUSER + password: PGPASSWORD +``` + +After remapping, the local environment variable `PGUSER` will contain the provider value for `username` and `PGPASSWORD` will contain the provider value for `password`. ## Hashicorp Vault diff --git a/pkg/core/types.go b/pkg/core/types.go index 36ef91f3..23e7fb19 100644 --- a/pkg/core/types.go +++ b/pkg/core/types.go @@ -1,11 +1,12 @@ package core type KeyPath struct { - Env string `yaml:"env,omitempty"` - Path string `yaml:"path"` - Field string `yaml:"field,omitempty"` - Decrypt bool `yaml:"decrypt,omitempty"` - Optional bool `yaml:"optional,omitempty"` + Env string `yaml:"env,omitempty"` + Path string `yaml:"path"` + Field string `yaml:"field,omitempty"` + Remap map[string]string `yaml:"remap,omitempty"` + Decrypt bool `yaml:"decrypt,omitempty"` + Optional bool `yaml:"optional,omitempty"` } type WizardAnswers struct { Project string diff --git a/pkg/providers/hashicorp_vault.go b/pkg/providers/hashicorp_vault.go index 5acfaea7..85b38f81 100644 --- a/pkg/providers/hashicorp_vault.go +++ b/pkg/providers/hashicorp_vault.go @@ -41,7 +41,13 @@ func (h *HashicorpVault) GetMapping(p core.KeyPath) ([]core.EnvEntry, error) { return nil, err } - k := secret.Data["data"].(map[string]interface{}) + // vault returns a secret kv struct as either data{} or data.data{} depending on engine + var k map[string]interface{} + if val, ok := secret.Data["data"]; ok { + k = val.(map[string]interface{}) + } else { + k = secret.Data + } entries := []core.EnvEntry{} for k, v := range k { @@ -57,7 +63,14 @@ func (h *HashicorpVault) Get(p core.KeyPath) (*core.EnvEntry, error) { return nil, err } - data := secret.Data["data"].(map[string]interface{}) + // vault returns a secret kv struct as either data{} or data.data{} depending on engine + var data map[string]interface{} + if val, ok := secret.Data["data"]; ok { + data = val.(map[string]interface{}) + } else { + data = secret.Data + } + k := data[p.Env] if p.Field != "" { k = data[p.Field] @@ -81,7 +94,7 @@ func (h *HashicorpVault) getSecret(kp core.KeyPath) (*api.Secret, error) { return nil, err } - if secret == nil || secret.Data["data"] == nil { + if secret == nil || len(secret.Data) == 0 { return nil, fmt.Errorf("data not found at '%s'", kp.Path) } diff --git a/pkg/providers/hashicorp_vault_test.go b/pkg/providers/hashicorp_vault_test.go index d8e52df6..635da274 100644 --- a/pkg/providers/hashicorp_vault_test.go +++ b/pkg/providers/hashicorp_vault_test.go @@ -19,20 +19,40 @@ func TestHashicorpVault(t *testing.T) { client := mock_providers.NewMockHashicorpClient(ctrl) path := "settings/prod/billing-svc" pathmap := "settings/prod/billing-svc/all" - out := api.Secret{ - Data: map[string]interface{}{ - "data": map[string]interface{}{ - "MG_KEY": "shazam", - "SMTP_PASS": "mailman", + + tests := map[string]struct { + out api.Secret + }{ + "data.data": { + out: api.Secret{ + Data: map[string]interface{}{ + "data": map[string]interface{}{ + "MG_KEY": "shazam", + "SMTP_PASS": "mailman", + }, + }, + }, + }, + "data": { + out: api.Secret{ + Data: map[string]interface{}{ + "MG_KEY": "shazam", + "SMTP_PASS": "mailman", + }, }, }, } - client.EXPECT().Read(gomock.Eq(path)).Return(&out, nil).AnyTimes() - client.EXPECT().Read(gomock.Eq(pathmap)).Return(&out, nil).AnyTimes() - s := HashicorpVault{ - client: client, + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + client.EXPECT().Read(gomock.Eq(path)).Return(&tc.out, nil).AnyTimes() + client.EXPECT().Read(gomock.Eq(pathmap)).Return(&tc.out, nil).AnyTimes() + s := HashicorpVault{ + client: client, + } + AssertProvider(t, &s, true) + }) } - AssertProvider(t, &s, true) } func TestHashicorpVaultFailures(t *testing.T) { diff --git a/pkg/teller.go b/pkg/teller.go index e4bd788f..ee7917f0 100644 --- a/pkg/teller.go +++ b/pkg/teller.go @@ -164,6 +164,14 @@ func (tl *Teller) Collect() error { if err != nil { return err } + + // optionally remap environment variables synced from the provider + for k, v := range es { + if val, ok := conf.EnvMapping.Remap[v.Key]; ok { + es[k].Key = val + } + } + entries = append(entries, es...) } if conf.Env != nil { diff --git a/pkg/teller_test.go b/pkg/teller_test.go index e8b58dbc..01104b19 100644 --- a/pkg/teller_test.go +++ b/pkg/teller_test.go @@ -31,8 +31,9 @@ func (im *InMemProvider) Name() string { func NewInMemProvider(alwaysError bool) (Providers, error) { return &InMemProvider{ inmem: map[string]string{ - "prod/billing/FOO": "foo_shazam", - "prod/billing/MG_KEY": "mg_shazam", + "prod/billing/FOO": "foo_shazam", + "prod/billing/MG_KEY": "mg_shazam", + "prod/billing/BEFORE_REMAP": "test_env_remap", }, alwaysError: alwaysError, }, nil @@ -144,6 +145,9 @@ func TestTellerCollectWithSync(t *testing.T) { "inmem": { EnvMapping: &core.KeyPath{ Path: "{{stage}}/billing", + Remap: map[string]string{ + "prod/billing/BEFORE_REMAP": "prod/billing/REMAPED", + }, }, }, }, @@ -151,16 +155,21 @@ func TestTellerCollectWithSync(t *testing.T) { } err := tl.Collect() assert.Nil(t, err) - assert.Equal(t, len(tl.Entries), 2) - assert.Equal(t, tl.Entries[0].Key, "prod/billing/MG_KEY") - assert.Equal(t, tl.Entries[0].Value, "mg_shazam") + assert.Equal(t, len(tl.Entries), 3) + assert.Equal(t, tl.Entries[0].Key, "prod/billing/REMAPED") + assert.Equal(t, tl.Entries[0].Value, "test_env_remap") assert.Equal(t, tl.Entries[0].ResolvedPath, "prod/billing") assert.Equal(t, tl.Entries[0].Provider, "inmem") - assert.Equal(t, tl.Entries[1].Key, "prod/billing/FOO") - assert.Equal(t, tl.Entries[1].Value, "foo_shazam") + assert.Equal(t, tl.Entries[1].Key, "prod/billing/MG_KEY") + assert.Equal(t, tl.Entries[1].Value, "mg_shazam") assert.Equal(t, tl.Entries[1].ResolvedPath, "prod/billing") assert.Equal(t, tl.Entries[1].Provider, "inmem") + + assert.Equal(t, tl.Entries[2].Key, "prod/billing/FOO") + assert.Equal(t, tl.Entries[2].Value, "foo_shazam") + assert.Equal(t, tl.Entries[2].ResolvedPath, "prod/billing") + assert.Equal(t, tl.Entries[2].Provider, "inmem") } func TestTellerCollectWithErrors(t *testing.T) { var b bytes.Buffer