From bbb7ea62e47a61b59b5b39e245fc94ca5f1fc1f1 Mon Sep 17 00:00:00 2001 From: skartikey <1942366+skartikey@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:14:47 +0100 Subject: [PATCH] fix(secretstores.vault): Preserve sibling keys when setting a secret --- plugins/secretstores/vault/vault.go | 12 ++- plugins/secretstores/vault/vault_test.go | 93 ++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/plugins/secretstores/vault/vault.go b/plugins/secretstores/vault/vault.go index 8447fb64c3501..79d963689bfab 100644 --- a/plugins/secretstores/vault/vault.go +++ b/plugins/secretstores/vault/vault.go @@ -119,7 +119,17 @@ func (v *Vault) List() ([]string, error) { } func (v *Vault) Set(key, value string) error { - secretsData := map[string]interface{}{key: value} + // Vault's Put replaces the whole secret at the path instead of merging into + // it, so read the existing secrets first and set the key on top of them to + // avoid removing the sibling keys. + secretsData := make(map[string]any) + switch secret, err := v.getSecret(); { + case err == nil && secret != nil && secret.Data != nil: + maps.Copy(secretsData, secret.Data) + case err != nil && !errors.Is(err, vault.ErrSecretNotFound): + return fmt.Errorf("unable to read secret: %w", err) + } + secretsData[key] = value if v.Engine == "kv-v1" { return v.client.KVv1(v.MountPath).Put(context.Background(), v.SecretPath, secretsData) diff --git a/plugins/secretstores/vault/vault_test.go b/plugins/secretstores/vault/vault_test.go index 36c343032af31..449bb7c86cf20 100644 --- a/plugins/secretstores/vault/vault_test.go +++ b/plugins/secretstores/vault/vault_test.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "path/filepath" + "slices" "testing" "github.com/stretchr/testify/require" @@ -243,6 +244,98 @@ func TestIntegrationAppRoleSecretWrapped(t *testing.T) { require.Equal(t, secretValue, string(secret)) } +func TestIntegrationSetKeepsSiblingsKVv1(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + testSetKeepsSiblings(t, "kv-v1") +} + +func TestIntegrationSetKeepsSiblingsKVv2(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + testSetKeepsSiblings(t, "kv-v2") +} + +func testSetKeepsSiblings(t *testing.T, engine string) { + t.Helper() + + mountPath := "my-mount-path" + secretPath := "my-secret-path" + + container, closer := createContainer(t, []string{ + fmt.Sprintf("secrets enable -path=%s %s", mountPath, engine), + fmt.Sprintf("kv put -mount=%s %s alpha=one beta=two", mountPath, secretPath), + }) + defer closer() + + addr, err := container.HttpHostAddress(context.Background()) + require.NoError(t, err) + + plugin := &Vault{ + ID: "test_" + engine, + Address: addr, + MountPath: mountPath, + SecretPath: secretPath, + Engine: engine, + Token: config.NewSecret([]byte("SomeToken")), + } + require.NoError(t, plugin.Init()) + + require.NoError(t, plugin.Set("gamma", "three")) + + keys, err := plugin.List() + require.NoError(t, err) + slices.Sort(keys) + require.Equal(t, []string{"alpha", "beta", "gamma"}, keys) +} + +func TestIntegrationSetCreatesNewPathKVv1(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + testSetCreatesNewPath(t, "kv-v1") +} + +func TestIntegrationSetCreatesNewPathKVv2(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + testSetCreatesNewPath(t, "kv-v2") +} + +func testSetCreatesNewPath(t *testing.T, engine string) { + t.Helper() + + mountPath := "my-mount-path" + secretPath := "my-secret-path" + + container, closer := createContainer(t, []string{ + fmt.Sprintf("secrets enable -path=%s %s", mountPath, engine), + }) + defer closer() + + addr, err := container.HttpHostAddress(context.Background()) + require.NoError(t, err) + + plugin := &Vault{ + ID: "test_" + engine, + Address: addr, + MountPath: mountPath, + SecretPath: secretPath, + Engine: engine, + Token: config.NewSecret([]byte("SomeToken")), + } + require.NoError(t, plugin.Init()) + + require.NoError(t, plugin.Set("gamma", "three")) + + value, err := plugin.Get("gamma") + require.NoError(t, err) + require.Equal(t, "three", string(value)) +} + func TestInitAuthValidation(t *testing.T) { base := Vault{ ID: "vault",