Add unit tests for Vault KV client
Signed-off-by: Hasan Turken <turkenh@gmail.com>
This commit is contained in:
parent
551b414fc8
commit
df72fd3089
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue