Skip to content

Commit

Permalink
feat: add fake secret store provider for test (#739)
Browse files Browse the repository at this point in the history
  • Loading branch information
adohe committed Jan 8, 2024
1 parent 687408f commit 9141195
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 0 deletions.
15 changes: 15 additions & 0 deletions pkg/apis/core/v1/workspace.go
Expand Up @@ -227,6 +227,9 @@ type ProviderSpec struct {

// Azure configures a store to retrieve secrets from Azure KeyVault.
Azure *AzureKVProvider `yaml:"azure,omitempty" json:"azure,omitempty"`

// Fake configures a store with static key/value pairs
Fake *FakeProvider `yaml:"fake,omitempty" json:"fake,omitempty"`
}

// AlicloudProvider configures a store to retrieve secrets from Alicloud Secrets Manager.
Expand Down Expand Up @@ -284,3 +287,15 @@ type AzureKVProvider struct {
// Ref: https://github.com/Azure/go-autorest/blob/main/autorest/azure/environments.go#L152
EnvironmentType AzureEnvironmentType `yaml:"environmentType,omitempty" json:"environmentType,omitempty"`
}

// FakeProvider configures a fake provider that returns static values.
type FakeProvider struct {
Data []FakeProviderData `json:"data"`
}

type FakeProviderData struct {
Key string `json:"key"`
Value string `json:"value,omitempty"`
ValueMap map[string]string `json:"valueMap,omitempty"`
Version string `json:"version,omitempty"`
}
10 changes: 10 additions & 0 deletions pkg/secrets/interfaces.go
Expand Up @@ -17,3 +17,13 @@ type SecretStoreProvider interface {
// NewSecretStore constructs a usable secret store with specific provider spec.
NewSecretStore(spec v1.SecretStoreSpec) (SecretStore, error)
}

var NoSecretErr = NoSecretError{}

// NoSecretError will be returned when GetSecret call can not find the
// desired secret.
type NoSecretError struct{}

func (NoSecretError) Error() string {
return "Secret does not exist"
}
89 changes: 89 additions & 0 deletions pkg/secrets/providers/fake/fake.go
@@ -0,0 +1,89 @@
package fake

import (
"context"
"fmt"

"github.com/tidwall/gjson"

v1 "kusionstack.io/kusion/pkg/apis/core/v1"
"kusionstack.io/kusion/pkg/secrets"
)

const (
errMissingProviderSpec = "secret store spec is missing provider"
errMissingFakeProvider = "invalid provider spec. Missing Fake field in secret store provider spec"
)

type SecretData struct {
Value string
Version string
ValueMap map[string]string
}

// DefaultSecretStoreProvider should implement the secrets.SecretStoreProvider interface
var _ secrets.SecretStoreProvider = &DefaultSecretStoreProvider{}

// smSecretStore should implement the secrets.SecretStore interface
var _ secrets.SecretStore = &fakeSecretStore{}

type DefaultSecretStoreProvider struct{}

// NewSecretStore constructs a fake secret store instance.
func (p *DefaultSecretStoreProvider) NewSecretStore(spec v1.SecretStoreSpec) (secrets.SecretStore, error) {
providerSpec := spec.Provider
if providerSpec == nil {
return nil, fmt.Errorf(errMissingProviderSpec)
}
if providerSpec.Fake == nil {
return nil, fmt.Errorf(errMissingFakeProvider)
}

dataMap := make(map[string]*SecretData)
for _, data := range providerSpec.Fake.Data {
key := mapKey(data.Key, data.Version)
dataMap[key] = &SecretData{
Value: data.Value,
Version: data.Version,
}
if data.ValueMap != nil {
dataMap[key].ValueMap = data.ValueMap
}
}

return &fakeSecretStore{dataMap: dataMap}, nil
}

type fakeSecretStore struct {
dataMap map[string]*SecretData
}

// GetSecret retrieves ref secret value from backend data map.
func (f *fakeSecretStore) GetSecret(_ context.Context, ref v1.ExternalSecretRef) ([]byte, error) {
data, ok := f.dataMap[mapKey(ref.Name, ref.Version)]
if !ok || data.Version != ref.Version {
return nil, secrets.NoSecretErr
}

if ref.Property != "" {
val := gjson.Get(data.Value, ref.Property)
if !val.Exists() {
return nil, secrets.NoSecretErr
}

return []byte(val.String()), nil
}

return []byte(data.Value), nil
}

func mapKey(key, version string) string {
// Add the version suffix to preserve entries with the old versions as well.
return fmt.Sprintf("%v%v", key, version)
}

func init() {
secrets.Register(&DefaultSecretStoreProvider{}, &v1.ProviderSpec{
Fake: &v1.FakeProvider{},
})
}
165 changes: 165 additions & 0 deletions pkg/secrets/providers/fake/fake_test.go
@@ -0,0 +1,165 @@
package fake

import (
"context"
"errors"
"reflect"
"testing"

"github.com/google/go-cmp/cmp"

v1 "kusionstack.io/kusion/pkg/apis/core/v1"
"kusionstack.io/kusion/pkg/secrets"
)

func TestGetSecret(t *testing.T) {
p := &DefaultSecretStoreProvider{}
testCases := []struct {
name string
input []v1.FakeProviderData
ref v1.ExternalSecretRef
expErr string
expValue string
wantErr bool
}{
{
name: "return err when not found",
input: []v1.FakeProviderData{},
ref: v1.ExternalSecretRef{
Name: "/secret-name",
Version: "v2",
},
expErr: secrets.NoSecretErr.Error(),
},
{
name: "get correct value from multiple versions",
input: []v1.FakeProviderData{
{
Key: "/beep",
Value: "one",
Version: "v1",
},
{
Key: "/bar",
Value: "xxxxx",
},
{
Key: "/beep",
Value: "two",
Version: "v2",
},
},
ref: v1.ExternalSecretRef{
Name: "/beep",
Version: "v2",
},
expValue: "two",
},
{
name: "get correct value from multiple properties",
input: []v1.FakeProviderData{
{
Key: "junk",
Value: "xxxxx",
},
{
Key: "/customer",
Value: `{"name":"Tony","age":"24"}`,
},
},
ref: v1.ExternalSecretRef{
Name: "/customer",
Property: "name",
},
expValue: "Tony",
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ss, _ := p.NewSecretStore(v1.SecretStoreSpec{
Provider: &v1.ProviderSpec{
Fake: &v1.FakeProvider{
Data: tt.input,
},
},
})
got, err := ss.GetSecret(context.Background(), tt.ref)
if len(tt.expErr) > 0 && tt.expErr != err.Error() {
t.Errorf("expected error %s, got %s", tt.expErr, err.Error())
} else if len(tt.expErr) == 0 && err != nil {
t.Errorf("unexpected error %v", err)
}
if len(tt.expValue) > 0 && tt.expValue != string(got) {
t.Errorf("expected result %s, got %s", tt.expValue, string(got))
}
})
}
}

func TestNewSecretStore(t *testing.T) {
testCases := map[string]struct {
spec v1.SecretStoreSpec
expectedErr error
}{
"InvalidSecretStoreSpec": {
spec: v1.SecretStoreSpec{},
expectedErr: errors.New(errMissingProviderSpec),
},
"InvalidProviderSpec": {
spec: v1.SecretStoreSpec{
Provider: &v1.ProviderSpec{},
},
expectedErr: errors.New(errMissingFakeProvider),
},
"ValidFakeProviderSpec": {
spec: v1.SecretStoreSpec{
Provider: &v1.ProviderSpec{
Fake: &v1.FakeProvider{},
},
},
expectedErr: nil,
},
"ValidFakeProviderSpec_WithData": {
spec: v1.SecretStoreSpec{
Provider: &v1.ProviderSpec{
Fake: &v1.FakeProvider{
Data: []v1.FakeProviderData{
{
Key: "secret-name",
Value: "some sensitive info",
Version: "1",
},
},
},
},
},
expectedErr: nil,
},
}

provider := DefaultSecretStoreProvider{}
for name, tc := range testCases {
_, err := provider.NewSecretStore(tc.spec)
if diff := cmp.Diff(err, tc.expectedErr, EquateErrors()); diff != "" {
t.Errorf("\n%s\ngot unexpected error:\n%s", name, diff)
}
}
}

// EquateErrors returns true if the supplied errors are of the same type and
// produce same error message.
func EquateErrors() cmp.Option {
return cmp.Comparer(func(a, b error) bool {
if a == nil || b == nil {
return a == nil && b == nil
}

av := reflect.ValueOf(a)
bv := reflect.ValueOf(b)
if av.Type() != bv.Type() {
return false
}

return a.Error() == b.Error()
})
}
1 change: 1 addition & 0 deletions pkg/secrets/providers/register/register.go
Expand Up @@ -6,5 +6,6 @@ import (
_ "kusionstack.io/kusion/pkg/secrets/providers/alicloud/secretsmanager"
_ "kusionstack.io/kusion/pkg/secrets/providers/aws/secretsmanager"
_ "kusionstack.io/kusion/pkg/secrets/providers/azure/keyvault"
_ "kusionstack.io/kusion/pkg/secrets/providers/fake"
_ "kusionstack.io/kusion/pkg/secrets/providers/hashivault"
)

0 comments on commit 9141195

Please sign in to comment.