Add unit tests for Vault KV client

Signed-off-by: Hasan Turken <turkenh@gmail.com>
This commit is contained in:
Hasan Turken 2022-03-02 16:18:40 +03:00
parent 551b414fc8
commit df72fd3089
No known key found for this signature in database
GPG Key ID: D7AA042F8F8B488E
5 changed files with 887 additions and 56 deletions

View File

@ -161,6 +161,7 @@ type VaultAuthConfig struct {
// Method configures which auth method will be used.
Method VaultAuthMethod `json:"method"`
// Token configures Token Auth for Vault.
// +optional
Token *VaultAuthTokenConfig `json:"token,omitempty"`
// Kubernetes configures Kubernetes Auth for Vault.
// +optional

View File

@ -20,50 +20,57 @@ import (
"encoding/json"
"path/filepath"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/hashicorp/vault/api"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/resource"
)
const (
errRead = "cannot read secret"
errWriteData = "cannot write secret data"
errWriteMetadata = "cannot write secret metadata data"
errWriteData = "cannot write secret Data"
errWriteMetadata = "cannot write secret metadata Data"
errNotFound = "secret not found"
)
// KVSecret is a KV Engine secret
// KVSecret is a AdditiveKVClient Engine secret
type KVSecret struct {
customMeta map[string]interface{}
data map[string]interface{}
CustomMeta map[string]interface{}
Data map[string]interface{}
version json.Number
}
// KVOption configures a KV.
type KVOption func(*KV)
// LogicalClient is a client to perform logical backend operations on Vault.
type LogicalClient interface {
Read(path string) (*api.Secret, error)
Write(path string, data map[string]interface{}) (*api.Secret, error)
Delete(path string) (*api.Secret, error)
}
// WithVersion specifies which version of KV Secrets engine to be used.
// KVOption configures a AdditiveKVClient.
type KVOption func(*AdditiveKVClient)
// WithVersion specifies which version of AdditiveKVClient Secrets engine to be used.
func WithVersion(v *v1.VaultKVVersion) KVOption {
return func(kv *KV) {
return func(kv *AdditiveKVClient) {
if v != nil {
kv.version = *v
}
}
}
// KV is a Vault KV Secrets Engine client.
type KV struct {
client *api.Logical
// AdditiveKVClient is a Vault KV Secrets Engine client that adds new data
// to existing ones while applying secrets.
type AdditiveKVClient struct {
client LogicalClient
mountPath string
version v1.VaultKVVersion
}
// NewKV returns a KV.
func NewKV(logical *api.Logical, mountPath string, opts ...KVOption) *KV {
kv := &KV{
// NewAdditiveKVClient returns a AdditiveKVClient.
func NewAdditiveKVClient(logical LogicalClient, mountPath string, opts ...KVOption) *AdditiveKVClient {
kv := &AdditiveKVClient{
client: logical,
mountPath: mountPath,
version: v1.VaultKVVersionV2,
@ -76,8 +83,8 @@ func NewKV(logical *api.Logical, mountPath string, opts ...KVOption) *KV {
return kv
}
// Get returns KVSecret at a given path.
func (k *KV) Get(path string, secret *KVSecret) error {
// Get returns a KVSecret at a given path.
func (k *AdditiveKVClient) Get(path string, secret *KVSecret) error {
dataPath := filepath.Join(k.mountPath, path)
if k.version == v1.VaultKVVersionV2 {
dataPath = filepath.Join(k.mountPath, "data", path)
@ -92,9 +99,9 @@ func (k *KV) Get(path string, secret *KVSecret) error {
return k.parseAsKVSecret(s, secret)
}
// Apply applies given KVSecret at path by patching its data and setting
// Apply applies given KVSecret at path by patching its Data and setting
// provided custom metadata.
func (k *KV) Apply(path string, secret *KVSecret) error {
func (k *AdditiveKVClient) Apply(path string, secret *KVSecret) error {
existing := &KVSecret{}
if err := k.Get(path, existing); resource.Ignore(isNotFound, err) != nil {
return errors.Wrap(err, errGet)
@ -118,9 +125,9 @@ func (k *KV) Apply(path string, secret *KVSecret) error {
}
}
mp, changed := metadataPayload(existing.customMeta, secret.customMeta)
// Update metadata only if there is some data in secret.
if len(existing.data) > 0 && changed {
mp, changed := metadataPayload(existing.CustomMeta, secret.CustomMeta)
// Update metadata only if there is some Data in secret.
if len(existing.Data) > 0 && changed {
if _, err := k.client.Write(filepath.Join(k.mountPath, "metadata", path), mp); err != nil {
return errors.Wrap(err, errWriteMetadata)
}
@ -130,58 +137,65 @@ func (k *KV) Apply(path string, secret *KVSecret) error {
}
// Delete deletes KVSecret at the given path.
func (k *KV) Delete(path string) error {
func (k *AdditiveKVClient) Delete(path string) error {
if k.version == v1.VaultKVVersionV1 {
_, err := k.client.Delete(filepath.Join(k.mountPath, path))
return errors.Wrap(err, errDelete)
}
// Note(turkenh): With KV v2, we need to delete metadata and all versions:
// Note(turkenh): With AdditiveKVClient v2, we need to delete metadata and all versions:
// https://www.vaultproject.io/api-docs/secret/kv/kv-v2#delete-metadata-and-all-versions
_, err := k.client.Delete(filepath.Join(k.mountPath, "metadata", path))
return errors.Wrap(err, errDelete)
}
func dataPayloadV2(existing, new *KVSecret) (map[string]interface{}, bool) {
data := make(map[string]interface{}, len(existing.data)+len(new.data))
for k, v := range existing.data {
data := make(map[string]interface{}, len(existing.Data)+len(new.Data))
for k, v := range existing.Data {
data[k] = v
}
changed := false
for k, v := range new.data {
if ev, ok := existing.data[k]; !ok || ev != v {
for k, v := range new.Data {
if ev, ok := existing.Data[k]; !ok || ev != v {
changed = true
data[k] = v
}
}
ver := json.Number("0")
if existing.version != "" {
ver = existing.version
}
return map[string]interface{}{
"options": map[string]interface{}{
"cas": existing.version,
"cas": ver,
},
"data": data,
}, changed
}
func metadataPayload(existing, new map[string]interface{}) (map[string]interface{}, bool) {
changed := false
payload := map[string]interface{}{
"custom_metadata": new,
}
if len(existing) != len(new) {
return payload, true
}
for k, v := range new {
if ev, ok := existing[k]; !ok || ev != v {
changed = true
return payload, true
}
}
return map[string]interface{}{
"custom_metadata": new,
}, changed
return payload, false
}
func dataPayloadV1(existing, new *KVSecret) (map[string]interface{}, bool) {
data := make(map[string]interface{}, len(existing.data)+len(new.data))
for k, v := range existing.data {
data := make(map[string]interface{}, len(existing.Data)+len(new.Data))
for k, v := range existing.Data {
data[k] = v
}
changed := false
for k, v := range new.data {
if ev, ok := existing.data[k]; !ok || ev != v {
for k, v := range new.Data {
if ev, ok := existing.Data[k]; !ok || ev != v {
changed = true
data[k] = v
}
@ -189,23 +203,23 @@ func dataPayloadV1(existing, new *KVSecret) (map[string]interface{}, bool) {
return data, changed
}
func (k *KV) parseAsKVSecret(s *api.Secret, kv *KVSecret) error {
func (k *AdditiveKVClient) parseAsKVSecret(s *api.Secret, kv *KVSecret) error {
if k.version == v1.VaultKVVersionV1 {
kv.data = s.Data
kv.Data = s.Data
}
// kv version is v2
// Note(turkenh): kv v2 secrets contains another "data" and a "metadata"
// block inside the top level generic "data" field.
// Note(turkenh): kv v2 secrets contains another "Data" and a "metadata"
// block inside the top level generic "Data" field.
// https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1
if sData, ok := s.Data["data"].(map[string]interface{}); ok && sData != nil {
kv.data = sData
kv.Data = sData
}
if sMeta, ok := s.Data["metadata"].(map[string]interface{}); ok && sMeta != nil {
kv.version, _ = sMeta["version"].(json.Number)
if cMeta, ok := sMeta["custom_metadata"].(map[string]interface{}); ok && cMeta != nil {
kv.customMeta = cMeta
kv.CustomMeta = cMeta
}
}
return nil

View File

@ -0,0 +1,775 @@
/*
Copyright 2022 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package vault
import (
"encoding/json"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/vault/api"
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/connection/store/vault/fake"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/test"
)
const (
mountPath = "test-secrets/"
secretName = "conn-unittests"
)
var (
errBoom = errors.New("boom")
kvv1 = v1.VaultKVVersionV1
kvv2 = v1.VaultKVVersionV2
)
func TestKVClientGet(t *testing.T) {
type args struct {
client LogicalClient
version *v1.VaultKVVersion
path string
}
type want struct {
err error
out *KVSecret
}
cases := map[string]struct {
reason string
args
want
}{
"ErrorWhileGettingSecret": {
reason: "Should return a proper error if getting secret failed.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return nil, errBoom
},
},
version: &kvv1,
path: secretName,
},
want: want{
err: errors.Wrap(errBoom, errRead),
out: &KVSecret{},
},
},
"SecretNotFound": {
reason: "Should return a notFound error if secret does not exist.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
// Vault logical client returns both error and secret as
// nil if secret does not exist.
return nil, nil
},
},
version: &kvv1,
path: secretName,
},
want: want{
err: errors.New(errNotFound),
out: &KVSecret{},
},
},
"SuccessfulGetFromV1": {
reason: "Should successfully return secret from v1 KV engine.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return &api.Secret{
Data: map[string]interface{}{
"foo": "bar",
},
}, nil
},
},
version: &kvv1,
path: secretName,
},
want: want{
out: &KVSecret{
Data: map[string]interface{}{
"foo": "bar",
},
},
},
},
"SuccessfulGetFromV2": {
reason: "Should successfully return secret from v2 KV engine.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
if diff := cmp.Diff(filepath.Join(mountPath, "data", secretName), path); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return &api.Secret{
// Using sample response here:
// https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1
Data: map[string]interface{}{
"data": map[string]interface{}{
"foo": "bar",
},
"metadata": map[string]interface{}{
"created_time": "2018-03-22T02:24:06.945319214Z",
"custom_metadata": map[string]interface{}{
"owner": "jdoe",
"mission_critical": "false",
},
"deletion_time": "",
"destroyed": false,
"version": 2,
},
},
}, nil
},
},
version: &kvv2,
path: secretName,
},
want: want{
out: &KVSecret{
Data: map[string]interface{}{
"foo": "bar",
},
CustomMeta: map[string]interface{}{
"owner": "jdoe",
"mission_critical": "false",
},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
k := NewAdditiveKVClient(tc.args.client, mountPath, WithVersion(tc.args.version))
s := KVSecret{}
err := k.Get(tc.args.path, &s)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nkvClient.Get(...): -want error, +got error:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.out, &s, cmpopts.IgnoreUnexported(KVSecret{})); diff != "" {
t.Errorf("\n%s\nkvClient.Get(...): -want, +got:\n%s", tc.reason, diff)
}
})
}
}
func TestKVClientApply(t *testing.T) {
type args struct {
client LogicalClient
version *v1.VaultKVVersion
in *KVSecret
path string
}
type want struct {
err error
}
cases := map[string]struct {
reason string
args
want
}{
"ErrorWhileReadingSecret": {
reason: "Should return a proper error if reading secret failed.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return nil, errBoom
},
},
version: &kvv1,
path: secretName,
},
want: want{
err: errors.Wrap(errors.Wrap(errBoom, errRead), errGet),
},
},
"ErrorWhileWritingV1Data": {
reason: "Should return a proper error if writing secret failed.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return &api.Secret{
Data: map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
}, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
return nil, errBoom
},
},
version: &kvv1,
path: secretName,
in: &KVSecret{
Data: map[string]interface{}{
"key1": "val1updated",
"key3": "val3",
},
},
},
want: want{
err: errors.Wrap(errBoom, errWriteData),
},
},
"ErrorWhileWritingV2Data": {
reason: "Should return a proper error if writing secret failed.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return &api.Secret{
Data: map[string]interface{}{
"data": map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
"metadata": map[string]interface{}{
"custom_metadata": map[string]interface{}{
"foo": "bar",
"baz": "qux",
},
"version": json.Number("2"),
},
},
}, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
return nil, errBoom
},
},
version: &kvv2,
path: secretName,
in: &KVSecret{
Data: map[string]interface{}{
"key1": "val1updated",
"key3": "val3",
},
CustomMeta: map[string]interface{}{
"foo": "bar",
"baz": "qux",
},
},
},
want: want{
err: errors.Wrap(errBoom, errWriteData),
},
},
"ErrorWhileWritingV2Metadata": {
reason: "Should return a proper error if writing secret metadata failed.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return &api.Secret{
Data: map[string]interface{}{
"data": map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
"metadata": map[string]interface{}{
"custom_metadata": map[string]interface{}{
"foo": "bar",
},
"version": json.Number("2"),
},
},
}, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
return nil, errBoom
},
},
version: &kvv2,
path: secretName,
in: &KVSecret{
Data: map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
CustomMeta: map[string]interface{}{
"foo": "baz",
},
},
},
want: want{
err: errors.Wrap(errBoom, errWriteMetadata),
},
},
"AlreadyUpToDateV1": {
reason: "Should not perform a write if a v1 secret is already up to date.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return &api.Secret{
Data: map[string]interface{}{
"foo": "bar",
},
}, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
return nil, errors.New("no write operation expected")
},
},
version: &kvv1,
path: secretName,
in: &KVSecret{
Data: map[string]interface{}{
"foo": "bar",
},
},
},
want: want{
err: nil,
},
},
"AlreadyUpToDateV2": {
reason: "Should not perform a write if a v2 secret is already up to date.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return &api.Secret{
// Using sample response here:
// https://www.vaultproject.io/api/secret/kv/kv-v2#sample-response-1
Data: map[string]interface{}{
"data": map[string]interface{}{
"foo": "bar",
},
"metadata": map[string]interface{}{
"created_time": "2018-03-22T02:24:06.945319214Z",
"custom_metadata": map[string]interface{}{
"owner": "jdoe",
"mission_critical": "false",
},
"deletion_time": "",
"destroyed": false,
"version": 2,
},
},
}, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
return nil, errors.New("no write operation expected")
},
},
version: &kvv2,
path: secretName,
in: &KVSecret{
CustomMeta: map[string]interface{}{
"owner": "jdoe",
"mission_critical": "false",
},
Data: map[string]interface{}{
"foo": "bar",
},
},
},
want: want{
err: nil,
},
},
"SuccessfulCreateV1": {
reason: "Should successfully create with new data if secret does not exists.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
// Vault logical client returns both error and secret as
// nil if secret does not exist.
return nil, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
if diff := cmp.Diff(map[string]interface{}{
"key1": "val1",
"key2": "val2",
}, data); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return nil, nil
},
},
version: &kvv1,
path: secretName,
in: &KVSecret{
Data: map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
},
},
want: want{
err: nil,
},
},
"SuccessfulCreateV2": {
reason: "Should successfully create with new data if secret does not exists.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
// Vault logical client returns both error and secret as
// nil if secret does not exist.
return nil, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
if diff := cmp.Diff(filepath.Join(mountPath, "data", secretName), path); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
if diff := cmp.Diff(map[string]interface{}{
"data": map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
"options": map[string]interface{}{
"cas": json.Number("0"),
},
}, data); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return nil, nil
},
},
version: &kvv2,
path: secretName,
in: &KVSecret{
Data: map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
},
},
want: want{
err: nil,
},
},
"SuccessfulUpdateV1": {
reason: "Should successfully update by appending new data to existing ones.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return &api.Secret{
Data: map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
}, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
if diff := cmp.Diff(map[string]interface{}{
"key1": "val1updated",
"key2": "val2",
"key3": "val3",
}, data); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return nil, nil
},
},
version: &kvv1,
path: secretName,
in: &KVSecret{
Data: map[string]interface{}{
"key1": "val1updated",
"key3": "val3",
},
},
},
want: want{
err: nil,
},
},
"SuccessfulUpdateV2Data": {
reason: "Should successfully update by appending new data to existing ones.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return &api.Secret{
Data: map[string]interface{}{
"data": map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
"metadata": map[string]interface{}{
"custom_metadata": map[string]interface{}{
"foo": "bar",
"baz": "qux",
},
"version": json.Number("2"),
},
},
}, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
if diff := cmp.Diff(filepath.Join(mountPath, "data", secretName), path); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
if diff := cmp.Diff(map[string]interface{}{
"data": map[string]interface{}{
"key1": "val1updated",
"key2": "val2",
"key3": "val3",
},
"options": map[string]interface{}{
"cas": json.Number("2"),
},
}, data); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return nil, nil
},
},
version: &kvv2,
path: secretName,
in: &KVSecret{
Data: map[string]interface{}{
"key1": "val1updated",
"key3": "val3",
},
CustomMeta: map[string]interface{}{
"foo": "bar",
"baz": "qux",
},
},
},
want: want{
err: nil,
},
},
"SuccessfulAddV2Metadata": {
reason: "Should successfully add new metadata.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return &api.Secret{
Data: map[string]interface{}{
"data": map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
"metadata": map[string]interface{}{
"version": json.Number("2"),
},
},
}, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
if diff := cmp.Diff(filepath.Join(mountPath, "metadata", secretName), path); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
if diff := cmp.Diff(map[string]interface{}{
"custom_metadata": map[string]interface{}{
"foo": "bar",
"baz": "qux",
},
}, data); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return nil, nil
},
},
version: &kvv2,
path: secretName,
in: &KVSecret{
Data: map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
CustomMeta: map[string]interface{}{
"foo": "bar",
"baz": "qux",
},
},
},
want: want{
err: nil,
},
},
"SuccessfulUpdateV2Metadata": {
reason: "Should successfully update metadata by overriding the existing ones.",
args: args{
client: &fake.LogicalClient{
ReadFn: func(path string) (*api.Secret, error) {
return &api.Secret{
Data: map[string]interface{}{
"data": map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
"metadata": map[string]interface{}{
"custom_metadata": map[string]interface{}{
"old": "meta",
},
"version": json.Number("2"),
},
},
}, nil
},
WriteFn: func(path string, data map[string]interface{}) (*api.Secret, error) {
if diff := cmp.Diff(filepath.Join(mountPath, "metadata", secretName), path); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
if diff := cmp.Diff(map[string]interface{}{
"custom_metadata": map[string]interface{}{
"foo": "bar",
"baz": "qux",
},
}, data); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return nil, nil
},
},
version: &kvv2,
path: secretName,
in: &KVSecret{
Data: map[string]interface{}{
"key1": "val1",
"key2": "val2",
},
CustomMeta: map[string]interface{}{
"foo": "bar",
"baz": "qux",
},
},
},
want: want{
err: nil,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
k := NewAdditiveKVClient(tc.args.client, mountPath, WithVersion(tc.args.version))
err := k.Apply(tc.args.path, tc.args.in)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nkvClient.Apply(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}
func TestKVClientDelete(t *testing.T) {
type args struct {
client LogicalClient
version *v1.VaultKVVersion
path string
}
type want struct {
err error
}
cases := map[string]struct {
reason string
args
want
}{
"ErrorWhileDeletingSecret": {
reason: "Should return a proper error if deleting secret failed.",
args: args{
client: &fake.LogicalClient{
DeleteFn: func(path string) (*api.Secret, error) {
return nil, errBoom
},
},
version: &kvv1,
path: secretName,
},
want: want{
err: errors.Wrap(errBoom, errDelete),
},
},
"SecretAlreadyDeleted": {
reason: "Should return success if secret already deleted.",
args: args{
client: &fake.LogicalClient{
DeleteFn: func(path string) (*api.Secret, error) {
// Vault logical client returns both error and secret as
// nil if secret does not exist.
return nil, nil
},
},
version: &kvv1,
path: secretName,
},
want: want{
err: nil,
},
},
"SuccessfulDeleteFromV1": {
reason: "Should return no error after successful deletion of a v1 secret.",
args: args{
client: &fake.LogicalClient{
DeleteFn: func(path string) (*api.Secret, error) {
if diff := cmp.Diff(filepath.Join(mountPath, secretName), path); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return &api.Secret{
Data: map[string]interface{}{
"foo": "bar",
},
}, nil
},
},
version: &kvv1,
path: secretName,
},
want: want{},
},
"SuccessfulDeleteFromV2": {
reason: "Should return no error after successful deletion of a v2 secret.",
args: args{
client: &fake.LogicalClient{
DeleteFn: func(path string) (*api.Secret, error) {
if diff := cmp.Diff(filepath.Join(mountPath, "metadata", secretName), path); diff != "" {
t.Errorf("r: -want, +got:\n%s", diff)
}
return nil, nil
},
},
version: &kvv2,
path: secretName,
},
want: want{},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
k := NewAdditiveKVClient(tc.args.client, mountPath, WithVersion(tc.args.version))
err := k.Delete(tc.args.path)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nkvClient.Get(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}

View File

@ -0,0 +1,41 @@
/*
Copyright 2022 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fake
import "github.com/hashicorp/vault/api"
// LogicalClient is a fake LogicalClient
type LogicalClient struct {
ReadFn func(path string) (*api.Secret, error)
WriteFn func(path string, data map[string]interface{}) (*api.Secret, error)
DeleteFn func(path string) (*api.Secret, error)
}
// Read reads secret at the given path.
func (l *LogicalClient) Read(path string) (*api.Secret, error) {
return l.ReadFn(path)
}
// Write writes data to the given path.
func (l *LogicalClient) Write(path string, data map[string]interface{}) (*api.Secret, error) {
return l.WriteFn(path, data)
}
// Delete deletes secret at the given path.
func (l *LogicalClient) Delete(path string) (*api.Secret, error) {
return l.DeleteFn(path)
}

View File

@ -42,7 +42,7 @@ const (
errDelete = "cannot delete secret"
)
// KVClient is a Vault KV Secrets engine client that supports both v1 and v2.
// KVClient is a Vault AdditiveKVClient Secrets engine client that supports both v1 and v2.
type KVClient interface {
Get(path string, secret *KVSecret) error
Apply(path string, secret *KVSecret) error
@ -86,7 +86,7 @@ func NewSecretStore(ctx context.Context, kube client.Client, cfg v1.SecretStoreC
}
return &SecretStore{
client: NewKV(c.Logical(), cfg.Vault.MountPath, WithVersion(cfg.Vault.Version)),
client: NewAdditiveKVClient(c.Logical(), cfg.Vault.MountPath, WithVersion(cfg.Vault.Version)),
defaultParentPath: cfg.DefaultScope,
}, nil
}
@ -97,8 +97,8 @@ func (ss *SecretStore) ReadKeyValues(_ context.Context, i store.Secret) (store.K
if err := ss.client.Get(ss.pathForSecretInstance(i), s); resource.Ignore(isNotFound, err) != nil {
return nil, errors.Wrap(err, errGet)
}
kv := make(store.KeyValues, len(s.data))
for k, v := range s.data {
kv := make(store.KeyValues, len(s.Data))
for k, v := range s.Data {
kv[k] = []byte(v.(string))
}
return kv, nil
@ -111,11 +111,11 @@ func (ss *SecretStore) WriteKeyValues(_ context.Context, i store.Secret, kv stor
data[k] = string(v)
}
kvSecret := &KVSecret{data: data}
kvSecret := &KVSecret{Data: data}
if i.Metadata != nil && len(i.Metadata.Labels) > 0 {
kvSecret.customMeta = make(map[string]interface{}, len(i.Metadata.Labels))
kvSecret.CustomMeta = make(map[string]interface{}, len(i.Metadata.Labels))
for k, v := range i.Metadata.Labels {
kvSecret.customMeta[k] = v
kvSecret.CustomMeta[k] = v
}
}
@ -125,9 +125,9 @@ func (ss *SecretStore) WriteKeyValues(_ context.Context, i store.Secret, kv stor
// DeleteKeyValues delete key value pairs from a given Vault Secret.
// If no kv specified, the whole secret instance is deleted.
// If kv specified, those would be deleted and secret instance will be deleted
// only if there is no data left.
func (ss *SecretStore) DeleteKeyValues(_ context.Context, i store.Secret, kv store.KeyValues) error {
// TODO(turkenh): Handle deletion of partial kv, currently we delete
// only if there is no Data left.
func (ss *SecretStore) DeleteKeyValues(_ context.Context, i store.Secret, _ store.KeyValues) error {
// TODO(turkenh): Handle deletion of partial kv, currently we delete the
// whole secret.
return errors.Wrap(ss.client.Delete(ss.pathForSecretInstance(i)), errDelete)
}