-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
KV helper methods for api package (#15305)
* Add Read methods for KVClient * KV write helper * Add changelog * Add Delete method * Use extractVersionMetadata inside extractDataAndVersionMetadata * Return nil, nil for v1 writes * Add test for extracting version metadata * Split kv client into v1 and v2-specific clients * Add ability to set options on Put * Add test for KV helpers * Add custom metadata to top level and allow for getting versions as sorted slice * Update tests * Separate KV v1 and v2 into different files * Add test for GetVersionsAsList, rename Metadata key to VersionMetadata for clarity * Move structs and godoc comments to more appropriate files * Add more tests for extract methods * Rework custom metadata helper to be more consistent with other helpers * Remove KVSecret from custom metadata test now that we don't append to it as part of helper method * Return early for readability and make test value name less confusing
- Loading branch information
Showing
6 changed files
with
1,014 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package api | ||
|
||
// A KVSecret is a key-value secret returned by Vault's KV secrets engine, | ||
// and is the most basic type of secret stored in Vault. | ||
// | ||
// Data contains the key-value pairs of the secret itself, | ||
// while Metadata contains a subset of metadata describing | ||
// this particular version of the secret. | ||
// The Metadata field for a KV v1 secret will always be nil, as | ||
// metadata is only supported starting in KV v2. | ||
// | ||
// The Raw field can be inspected for information about the lease, | ||
// and passed to a LifetimeWatcher object for periodic renewal. | ||
type KVSecret struct { | ||
Data map[string]interface{} | ||
VersionMetadata *KVVersionMetadata | ||
CustomMetadata map[string]interface{} | ||
Raw *Secret | ||
} | ||
|
||
// KVv1 is used to return a client for reads and writes against | ||
// a KV v1 secrets engine in Vault. | ||
// | ||
// The mount path is the location where the target KV secrets engine resides | ||
// in Vault. | ||
// | ||
// While v1 is not necessarily deprecated, Vault development servers tend to | ||
// use v2 as the version of the KV secrets engine, as this is what's mounted | ||
// by default when a server is started in -dev mode. See the kvv2 struct. | ||
// | ||
// Learn more about the KV secrets engine here: | ||
// https://www.vaultproject.io/docs/secrets/kv | ||
func (c *Client) KVv1(mountPath string) *kvv1 { | ||
return &kvv1{c: c, mountPath: mountPath} | ||
} | ||
|
||
// KVv2 is used to return a client for reads and writes against | ||
// a KV v2 secrets engine in Vault. | ||
// | ||
// The mount path is the location where the target KV secrets engine resides | ||
// in Vault. | ||
// | ||
// Vault development servers tend to have "secret" as the mount path, | ||
// as these are the default settings when a server is started in -dev mode. | ||
// | ||
// Learn more about the KV secrets engine here: | ||
// https://www.vaultproject.io/docs/secrets/kv | ||
func (c *Client) KVv2(mountPath string) *kvv2 { | ||
return &kvv2{c: c, mountPath: mountPath} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,334 @@ | ||
package api | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestExtractVersionMetadata(t *testing.T) { | ||
t.Parallel() | ||
|
||
inputCreatedTimeStr := "2022-05-06T23:02:04.865025Z" | ||
inputDeletionTimeStr := "2022-06-17T01:15:03.279013Z" | ||
expectedCreatedTimeParsed, err := time.Parse(time.RFC3339, inputCreatedTimeStr) | ||
if err != nil { | ||
t.Fatalf("unable to parse expected created time: %v", err) | ||
} | ||
expectedDeletionTimeParsed, err := time.Parse(time.RFC3339, inputDeletionTimeStr) | ||
if err != nil { | ||
t.Fatalf("unable to parse expected created time: %v", err) | ||
} | ||
|
||
testCases := []struct { | ||
name string | ||
input *Secret | ||
expected *KVVersionMetadata | ||
}{ | ||
{ | ||
name: "a secret", | ||
input: &Secret{ | ||
Data: map[string]interface{}{ | ||
"data": map[string]interface{}{ | ||
"password": "Hashi123", | ||
}, | ||
"metadata": map[string]interface{}{ | ||
"version": 10, | ||
"created_time": inputCreatedTimeStr, | ||
"deletion_time": "", | ||
"destroyed": false, | ||
"custom_metadata": nil, | ||
}, | ||
}, | ||
}, | ||
expected: &KVVersionMetadata{ | ||
Version: 10, | ||
CreatedTime: expectedCreatedTimeParsed, | ||
DeletionTime: time.Time{}, | ||
Destroyed: false, | ||
}, | ||
}, | ||
{ | ||
name: "a secret that has been deleted", | ||
input: &Secret{ | ||
Data: map[string]interface{}{ | ||
"data": map[string]interface{}{ | ||
"password": "Hashi123", | ||
}, | ||
"metadata": map[string]interface{}{ | ||
"version": 10, | ||
"created_time": inputCreatedTimeStr, | ||
"deletion_time": inputDeletionTimeStr, | ||
"destroyed": false, | ||
"custom_metadata": nil, | ||
}, | ||
}, | ||
}, | ||
expected: &KVVersionMetadata{ | ||
Version: 10, | ||
CreatedTime: expectedCreatedTimeParsed, | ||
DeletionTime: expectedDeletionTimeParsed, | ||
Destroyed: false, | ||
}, | ||
}, | ||
{ | ||
name: "a response from a Write operation", | ||
input: &Secret{ | ||
Data: map[string]interface{}{ | ||
"version": 10, | ||
"created_time": inputCreatedTimeStr, | ||
"deletion_time": "", | ||
"destroyed": false, | ||
"custom_metadata": nil, | ||
}, | ||
}, | ||
expected: &KVVersionMetadata{ | ||
Version: 10, | ||
CreatedTime: expectedCreatedTimeParsed, | ||
DeletionTime: time.Time{}, | ||
Destroyed: false, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
versionMetadata, err := extractVersionMetadata(tc.input) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
if !reflect.DeepEqual(versionMetadata, tc.expected) { | ||
t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, versionMetadata, tc.expected) | ||
} | ||
} | ||
} | ||
|
||
func TestExtractDataAndVersionMetadata(t *testing.T) { | ||
t.Parallel() | ||
|
||
inputCreatedTimeStr := "2022-05-06T23:02:04.865025Z" | ||
inputDeletionTimeStr := "2022-06-17T01:15:03.279013Z" | ||
expectedCreatedTimeParsed, err := time.Parse(time.RFC3339, inputCreatedTimeStr) | ||
if err != nil { | ||
t.Fatalf("unable to parse expected created time: %v", err) | ||
} | ||
expectedDeletionTimeParsed, err := time.Parse(time.RFC3339, inputDeletionTimeStr) | ||
if err != nil { | ||
t.Fatalf("unable to parse expected created time: %v", err) | ||
} | ||
|
||
readResp := &Secret{ | ||
Data: map[string]interface{}{ | ||
"data": map[string]interface{}{ | ||
"password": "Hashi123", | ||
}, | ||
"metadata": map[string]interface{}{ | ||
"version": 10, | ||
"created_time": inputCreatedTimeStr, | ||
"deletion_time": "", | ||
"destroyed": false, | ||
"custom_metadata": nil, | ||
}, | ||
}, | ||
} | ||
|
||
readRespDeleted := &Secret{ | ||
Data: map[string]interface{}{ | ||
"data": nil, | ||
"metadata": map[string]interface{}{ | ||
"version": 10, | ||
"created_time": inputCreatedTimeStr, | ||
"deletion_time": inputDeletionTimeStr, | ||
"destroyed": false, | ||
"custom_metadata": nil, | ||
}, | ||
}, | ||
} | ||
|
||
testCases := []struct { | ||
name string | ||
input *Secret | ||
expected *KVSecret | ||
}{ | ||
{ | ||
name: "a response from a Read operation", | ||
input: readResp, | ||
expected: &KVSecret{ | ||
Data: map[string]interface{}{ | ||
"password": "Hashi123", | ||
}, | ||
VersionMetadata: &KVVersionMetadata{ | ||
Version: 10, | ||
CreatedTime: expectedCreatedTimeParsed, | ||
DeletionTime: time.Time{}, | ||
Destroyed: false, | ||
}, | ||
// it's tempting to test some Secrets with custom_metadata but | ||
// we can't in this test because it isn't until we call the | ||
// extractCustomMetadata function that the custom metadata | ||
// gets added onto the struct. See TestExtractCustomMetadata. | ||
CustomMetadata: nil, | ||
Raw: readResp, | ||
}, | ||
}, | ||
{ | ||
name: "a secret that has been deleted and thus has nil data", | ||
input: readRespDeleted, | ||
expected: &KVSecret{ | ||
Data: nil, | ||
VersionMetadata: &KVVersionMetadata{ | ||
Version: 10, | ||
CreatedTime: expectedCreatedTimeParsed, | ||
DeletionTime: expectedDeletionTimeParsed, | ||
Destroyed: false, | ||
}, | ||
CustomMetadata: nil, | ||
Raw: readRespDeleted, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
dvm, err := extractDataAndVersionMetadata(tc.input) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
if !reflect.DeepEqual(dvm, tc.expected) { | ||
t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, dvm, tc.expected) | ||
} | ||
} | ||
} | ||
|
||
func TestExtractFullMetadata(t *testing.T) { | ||
inputCreatedTimeStr := "2022-05-20T00:51:49.419794Z" | ||
expectedCreatedTimeParsed, err := time.Parse(time.RFC3339, inputCreatedTimeStr) | ||
if err != nil { | ||
t.Fatalf("unable to parse expected created time: %v", err) | ||
} | ||
|
||
inputUpdatedTimeStr := "2022-05-20T20:23:43.284488Z" | ||
expectedUpdatedTimeParsed, err := time.Parse(time.RFC3339, inputUpdatedTimeStr) | ||
if err != nil { | ||
t.Fatalf("unable to parse expected updated time: %v", err) | ||
} | ||
|
||
inputDeletedTimeStr := "2022-05-21T00:05:49.521697Z" | ||
expectedDeletedTimeParsed, err := time.Parse(time.RFC3339, inputDeletedTimeStr) | ||
if err != nil { | ||
t.Fatalf("unable to parse expected deletion time: %v", err) | ||
} | ||
|
||
metadataResp := &Secret{ | ||
Data: map[string]interface{}{ | ||
"cas_required": true, | ||
"created_time": inputCreatedTimeStr, | ||
"current_version": 2, | ||
"custom_metadata": map[string]interface{}{ | ||
"org": "eng", | ||
}, | ||
"delete_version_after": "200s", | ||
"max_versions": 3, | ||
"oldest_version": 1, | ||
"updated_time": inputUpdatedTimeStr, | ||
"versions": map[string]interface{}{ | ||
"2": map[string]interface{}{ | ||
"created_time": inputUpdatedTimeStr, | ||
"deletion_time": "", | ||
"destroyed": false, | ||
}, | ||
"1": map[string]interface{}{ | ||
"created_time": inputCreatedTimeStr, | ||
"deletion_time": inputDeletedTimeStr, | ||
"destroyed": false, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
testCases := []struct { | ||
name string | ||
input *Secret | ||
expected *KVMetadata | ||
}{ | ||
{ | ||
name: "a metadata response", | ||
input: metadataResp, | ||
expected: &KVMetadata{ | ||
CASRequired: true, | ||
CreatedTime: expectedCreatedTimeParsed, | ||
CurrentVersion: 2, | ||
CustomMetadata: map[string]interface{}{ | ||
"org": "eng", | ||
}, | ||
DeleteVersionAfter: time.Duration(200 * time.Second), | ||
MaxVersions: 3, | ||
OldestVersion: 1, | ||
UpdatedTime: expectedUpdatedTimeParsed, | ||
Versions: map[string]KVVersionMetadata{ | ||
"2": { | ||
Version: 2, | ||
CreatedTime: expectedUpdatedTimeParsed, | ||
DeletionTime: time.Time{}, | ||
}, | ||
"1": { | ||
Version: 1, | ||
CreatedTime: expectedCreatedTimeParsed, | ||
DeletionTime: expectedDeletedTimeParsed, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
md, err := extractFullMetadata(tc.input) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
if !reflect.DeepEqual(md, tc.expected) { | ||
t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, md, tc.expected) | ||
} | ||
} | ||
} | ||
|
||
func TestExtractCustomMetadata(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
inputAPIResp *Secret | ||
expected map[string]interface{} | ||
}{ | ||
{ | ||
name: "a read response with some custom metadata", | ||
inputAPIResp: &Secret{ | ||
Data: map[string]interface{}{ | ||
"metadata": map[string]interface{}{ | ||
"custom_metadata": map[string]interface{}{"org": "eng"}, | ||
}, | ||
}, | ||
}, | ||
expected: map[string]interface{}{"org": "eng"}, | ||
}, | ||
{ | ||
name: "a write response with some (pre-existing) custom metadata", | ||
inputAPIResp: &Secret{ | ||
Data: map[string]interface{}{ | ||
"custom_metadata": map[string]interface{}{"org": "eng"}, | ||
}, | ||
}, | ||
expected: map[string]interface{}{"org": "eng"}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
cm, err := extractCustomMetadata(tc.inputAPIResp) | ||
if err != nil { | ||
t.Fatalf("err: %s", err) | ||
} | ||
|
||
if !reflect.DeepEqual(cm, tc.expected) { | ||
t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, cm, tc.expected) | ||
} | ||
} | ||
} |
Oops, something went wrong.