Browse Source

feat(provider): implement fake provider

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 4 years ago
parent
commit
fe1cb8bc69

+ 27 - 0
apis/externalsecrets/v1alpha1/secretstore_fake_types.go

@@ -0,0 +1,27 @@
+/*
+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 v1alpha1
+
+// 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"`
+}

+ 4 - 0
apis/externalsecrets/v1alpha1/secretstore_types.go

@@ -81,6 +81,10 @@ type SecretStoreProvider struct {
 	// Webhook configures this store to sync secrets using a generic templated webhook
 	// +optional
 	Webhook *WebhookProvider `json:"webhook,omitempty"`
+
+	// Fake configures a store with static key/value pairs
+	// +optional
+	Fake *FakeProvider `json:"fake,omitempty"`
 }
 
 type SecretStoreRetrySettings struct {

+ 49 - 0
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -600,6 +600,50 @@ func (in *ExternalSecretTemplateMetadata) DeepCopy() *ExternalSecretTemplateMeta
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FakeProvider) DeepCopyInto(out *FakeProvider) {
+	*out = *in
+	if in.Data != nil {
+		in, out := &in.Data, &out.Data
+		*out = make([]FakeProviderData, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FakeProvider.
+func (in *FakeProvider) DeepCopy() *FakeProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(FakeProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FakeProviderData) DeepCopyInto(out *FakeProviderData) {
+	*out = *in
+	if in.ValueMap != nil {
+		in, out := &in.ValueMap, &out.ValueMap
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FakeProviderData.
+func (in *FakeProviderData) DeepCopy() *FakeProviderData {
+	if in == nil {
+		return nil
+	}
+	out := new(FakeProviderData)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GCPSMAuth) DeepCopyInto(out *GCPSMAuth) {
 	*out = *in
 	if in.SecretRef != nil {
@@ -939,6 +983,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(WebhookProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Fake != nil {
+		in, out := &in.Fake, &out.Fake
+		*out = new(FakeProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.

+ 23 - 0
deploy/crds/external-secrets.io_clustersecretstores.yaml

@@ -380,6 +380,29 @@ spec:
                     required:
                     - vaultUrl
                     type: object
+                  fake:
+                    description: Fake configures a store with static key/value pairs
+                    properties:
+                      data:
+                        items:
+                          properties:
+                            key:
+                              type: string
+                            value:
+                              type: string
+                            valueMap:
+                              additionalProperties:
+                                type: string
+                              type: object
+                            version:
+                              type: string
+                          required:
+                          - key
+                          type: object
+                        type: array
+                    required:
+                    - data
+                    type: object
                   gcpsm:
                     description: GCPSM configures this store to sync secrets using
                       Google Cloud Platform Secret Manager provider

+ 23 - 0
deploy/crds/external-secrets.io_secretstores.yaml

@@ -380,6 +380,29 @@ spec:
                     required:
                     - vaultUrl
                     type: object
+                  fake:
+                    description: Fake configures a store with static key/value pairs
+                    properties:
+                      data:
+                        items:
+                          properties:
+                            key:
+                              type: string
+                            value:
+                              type: string
+                            valueMap:
+                              additionalProperties:
+                                type: string
+                              type: object
+                            version:
+                              type: string
+                          required:
+                          - key
+                          type: object
+                        type: array
+                    required:
+                    - data
+                    type: object
                   gcpsm:
                     description: GCPSM configures this store to sync secrets using
                       Google Cloud Platform Secret Manager provider

+ 26 - 0
docs/provider-fake.md

@@ -0,0 +1,26 @@
+We provide a `fake` implementation to help with testing. This provider returns static key/value pairs and nothing else.
+To use the `fake` provider simply create a `SecretStore` or `ClusterSecretStore` and configure it like in the following example:
+
+!!! note inline end
+    The provider returns static data configured in `value` or `valueMap`. You can define a `version`, too. If set the `remoteRef` from an ExternalSecret must match otherwise no value is returned.
+
+```yaml
+{% include 'fake-provider-store.yaml' %}
+```
+
+Please note that `value` is intended for exclusive use with `data` and `valueMap` for `dataFrom`.
+Here is an example `ExternalSecret` that displays this behavior:
+
+!!! warning inline end
+    This provider supports specifying different `data[].version` configurations. However, `data[].property` is ignored.
+
+```yaml
+{% include 'fake-provider-es.yaml' %}
+```
+
+This results in the following secret:
+
+
+```yaml
+{% include 'fake-provider-secret.yaml' %}
+```

+ 18 - 0
docs/snippets/fake-provider-es.yaml

@@ -0,0 +1,18 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: example
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: fake
+    kind: ClusterSecretStore
+  target:
+    name: secret-to-be-created
+  data:
+  - secretKey: foo_bar
+    remoteRef:
+      key: /foo/bar
+      version: v1
+  dataFrom:
+  - key: /foo/baz

+ 9 - 0
docs/snippets/fake-provider-secret.yaml

@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: secret-to-be-created
+  namespace: default
+data:
+  foo_bar: SEVMTE8x # HELLO1  (via data)
+  foo: ZXhhbXBsZQ== # example (via dataFrom)
+  other: dGhpbmc=   # thing   (via dataFrom)

+ 20 - 0
docs/snippets/fake-provider-store.yaml

@@ -0,0 +1,20 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: ClusterSecretStore
+metadata:
+  name: fake
+spec:
+  provider:
+    fake:
+      data:
+      - key: "/foo/bar"
+        value: "HELLO1"
+        version: "v1"
+      - key: "/foo/bar"
+        value: "HELLO2"
+        version: "v2"
+      - key: "/foo/baz"
+        valueMap:
+          foo: example
+          other: thing
+
+

+ 336 - 0
docs/spec.md

@@ -714,6 +714,237 @@ string
 <td></td>
 </tr></tbody>
 </table>
+<h3 id="external-secrets.io/v1alpha1.ClusterExternalSecret">ClusterExternalSecret
+</h3>
+<p>
+<p>ClusterExternalSecret is the Schema for the clusterexternalsecrets API.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>metadata</code></br>
+<em>
+<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta">
+Kubernetes meta/v1.ObjectMeta
+</a>
+</em>
+</td>
+<td>
+Refer to the Kubernetes API documentation for the fields of the
+<code>metadata</code> field.
+</td>
+</tr>
+<tr>
+<td>
+<code>spec</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.ClusterExternalSecretSpec">
+ClusterExternalSecretSpec
+</a>
+</em>
+</td>
+<td>
+<br/>
+<br/>
+<table>
+<tr>
+<td>
+<code>externalSecretSpec</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.ExternalSecretSpec">
+ExternalSecretSpec
+</a>
+</em>
+</td>
+<td>
+<p>The spec for the ExternalSecrets to be created</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>externalSecretName</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The name of the external secrets to be created defaults to the name of the ClusterExternalSecret</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>namespaceSelector</code></br>
+<em>
+<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#labelselector-v1-meta">
+Kubernetes meta/v1.LabelSelector
+</a>
+</em>
+</td>
+<td>
+<p>The labels to select by to find the Namespaces to create the ExternalSecrets in.</p>
+</td>
+</tr>
+</table>
+</td>
+</tr>
+<tr>
+<td>
+<code>status</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.ClusterExternalSecretStatus">
+ClusterExternalSecretStatus
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.ClusterExternalSecretConditionType">ClusterExternalSecretConditionType
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.ClusterExternalSecretStatus">ClusterExternalSecretStatus</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;NotReady&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;PartiallyReady&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;Ready&#34;</p></td>
+<td></td>
+</tr></tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.ClusterExternalSecretSpec">ClusterExternalSecretSpec
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.ClusterExternalSecret">ClusterExternalSecret</a>)
+</p>
+<p>
+<p>ClusterExternalSecretSpec defines the desired state of ClusterExternalSecret.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>externalSecretSpec</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.ExternalSecretSpec">
+ExternalSecretSpec
+</a>
+</em>
+</td>
+<td>
+<p>The spec for the ExternalSecrets to be created</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>externalSecretName</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The name of the external secrets to be created defaults to the name of the ClusterExternalSecret</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>namespaceSelector</code></br>
+<em>
+<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#labelselector-v1-meta">
+Kubernetes meta/v1.LabelSelector
+</a>
+</em>
+</td>
+<td>
+<p>The labels to select by to find the Namespaces to create the ExternalSecrets in.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.ClusterExternalSecretStatus">ClusterExternalSecretStatus
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.ClusterExternalSecret">ClusterExternalSecret</a>)
+</p>
+<p>
+<p>ClusterExternalSecretStatus defines the observed state of ClusterExternalSecret.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>type</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.ClusterExternalSecretConditionType">
+ClusterExternalSecretConditionType
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>status</code></br>
+<em>
+<a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#conditionstatus-v1-core">
+Kubernetes core/v1.ConditionStatus
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>externalSecretStatuses</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.ExternalSecretStatus">
+[]ExternalSecretStatus
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1alpha1.ClusterSecretStore">ClusterSecretStore
 </h3>
 <p>
@@ -1084,6 +1315,7 @@ string
 </h3>
 <p>
 (<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.ClusterExternalSecretSpec">ClusterExternalSecretSpec</a>, 
 <a href="#external-secrets.io/v1alpha1.ExternalSecret">ExternalSecret</a>)
 </p>
 <p>
@@ -1171,6 +1403,7 @@ If multiple entries are specified, the Secret keys are merged in the specified o
 </h3>
 <p>
 (<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.ClusterExternalSecretStatus">ClusterExternalSecretStatus</a>, 
 <a href="#external-secrets.io/v1alpha1.ExternalSecret">ExternalSecret</a>)
 </p>
 <p>
@@ -1486,6 +1719,95 @@ map[string]string
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1alpha1.FakeProvider">FakeProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>FakeProvider configures a fake provider that returns static values</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>data</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.FakeProviderData">
+[]FakeProviderData
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.FakeProviderData">FakeProviderData
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.FakeProvider">FakeProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>key</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>value</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>valueMap</code></br>
+<em>
+map[string]string
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>version</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1alpha1.GCPSMAuth">GCPSMAuth
 </h3>
 <p>
@@ -2310,6 +2632,20 @@ WebhookProvider
 <p>Webhook configures this store to sync secrets using a generic templated webhook</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>fake</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.FakeProvider">
+FakeProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Fake configures a store with static key/value pairs</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1alpha1.SecretStoreRef">SecretStoreRef

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -56,6 +56,7 @@ nav:
     - Oracle:
       - Oracle Vault: provider-oracle-vault.md
     - Webhook: provider-webhook.md
+    - Fake: provider-fake.md
   - References:
     - API specification: spec.md
   - Contributing:

+ 1 - 1
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -31,8 +31,8 @@ import (
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	"github.com/external-secrets/external-secrets/pkg/provider"
-	"github.com/external-secrets/external-secrets/pkg/provider/fake"
 	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
 )
 
 var (

+ 51 - 58
pkg/provider/fake/fake.go

@@ -16,6 +16,7 @@ package fake
 
 import (
 	"context"
+	"fmt"
 
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -24,80 +25,72 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/provider/schema"
 )
 
-var _ provider.Provider = &Client{}
+var (
+	errNotFound            = fmt.Errorf("secret value not found")
+	errMissingStore        = fmt.Errorf("missing store provider")
+	errMissingFakeProvider = fmt.Errorf("missing store provider fake")
+)
 
-// Client is a fake client for testing.
-type Client struct {
-	NewFn func(context.Context, esv1alpha1.GenericStore, client.Client,
-		string) (provider.SecretsClient, error)
-	GetSecretFn    func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error)
-	GetSecretMapFn func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
+type Provider struct {
+	config *esv1alpha1.FakeProvider
 }
 
-// New returns a fake provider/client.
-func New() *Client {
-	v := &Client{
-		GetSecretFn: func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
-			return nil, nil
-		},
-		GetSecretMapFn: func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
-			return nil, nil
-		},
-	}
-
-	v.NewFn = func(context.Context, esv1alpha1.GenericStore, client.Client, string) (provider.SecretsClient, error) {
-		return v, nil
+func (p *Provider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+	cfg, err := getProvider(store)
+	if err != nil {
+		return nil, err
 	}
-
-	return v
-}
-
-// RegisterAs registers the fake client in the schema.
-func (v *Client) RegisterAs(provider *esv1alpha1.SecretStoreProvider) {
-	schema.ForceRegister(v, provider)
+	return &Provider{
+		config: cfg,
+	}, nil
 }
 
-// GetSecret implements the provider.Provider interface.
-func (v *Client) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	return v.GetSecretFn(ctx, ref)
+func getProvider(store esv1alpha1.GenericStore) (*esv1alpha1.FakeProvider, error) {
+	if store == nil {
+		return nil, errMissingStore
+	}
+	spc := store.GetSpec()
+	if spc == nil || spc.Provider == nil || spc.Provider.Fake == nil {
+		return nil, errMissingFakeProvider
+	}
+	return spc.Provider.Fake, nil
 }
 
-// WithGetSecret wraps secret data returned by this provider.
-func (v *Client) WithGetSecret(secData []byte, err error) *Client {
-	v.GetSecretFn = func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
-		return secData, err
+// GetSecret returns a single secret from the provider.
+func (p *Provider) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	for _, data := range p.config.Data {
+		if data.Key == ref.Key && data.Version == ref.Version {
+			return []byte(data.Value), nil
+		}
 	}
-	return v
+	return nil, errNotFound
 }
 
-// GetSecretMap imeplements the provider.Provider interface.
-func (v *Client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
-	return v.GetSecretMapFn(ctx, ref)
-}
-func (v *Client) Close(ctx context.Context) error {
-	return nil
+// GetSecretMap returns multiple k/v pairs from the provider.
+func (p *Provider) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	for _, data := range p.config.Data {
+		if data.Key != ref.Key || data.Version != ref.Version || data.ValueMap == nil {
+			continue
+		}
+		return convertMap(data.ValueMap), nil
+	}
+	return nil, errNotFound
 }
 
-// WithGetSecretMap wraps the secret data map returned by this fake provider.
-func (v *Client) WithGetSecretMap(secData map[string][]byte, err error) *Client {
-	v.GetSecretMapFn = func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
-		return secData, err
+func convertMap(in map[string]string) map[string][]byte {
+	m := make(map[string][]byte)
+	for k, v := range in {
+		m[k] = []byte(v)
 	}
-	return v
+	return m
 }
 
-// WithNew wraps the fake provider factory function.
-func (v *Client) WithNew(f func(context.Context, esv1alpha1.GenericStore, client.Client,
-	string) (provider.SecretsClient, error)) *Client {
-	v.NewFn = f
-	return v
+func (p *Provider) Close(ctx context.Context) error {
+	return nil
 }
 
-// NewClient returns a new fake provider.
-func (v *Client) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
-	c, err := v.NewFn(ctx, store, kube, namespace)
-	if err != nil {
-		return nil, err
-	}
-	return c, nil
+func init() {
+	schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{
+		Fake: &esv1alpha1.FakeProvider{},
+	})
 }

+ 194 - 0
pkg/provider/fake/fake_test.go

@@ -0,0 +1,194 @@
+/*
+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 (
+	"context"
+	"testing"
+
+	"github.com/onsi/gomega"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+func TestNewClient(t *testing.T) {
+	p := &Provider{}
+	gomega.RegisterTestingT(t)
+
+	// nil store
+	_, err := p.NewClient(context.Background(), nil, nil, "")
+	gomega.Expect(err).To(gomega.HaveOccurred())
+
+	// missing provider
+	_, err = p.NewClient(context.Background(), &esv1alpha1.SecretStore{}, nil, "")
+	gomega.Expect(err).To(gomega.HaveOccurred())
+}
+
+func TestClose(t *testing.T) {
+	p := &Provider{}
+	gomega.RegisterTestingT(t)
+	err := p.Close(context.TODO())
+	gomega.Expect(err).ToNot(gomega.HaveOccurred())
+}
+
+type testCase struct {
+	name     string
+	input    []esv1alpha1.FakeProviderData
+	request  esv1alpha1.ExternalSecretDataRemoteRef
+	expValue string
+	expErr   string
+}
+
+func TestGetSecret(t *testing.T) {
+	gomega.RegisterTestingT(t)
+	p := &Provider{}
+	tbl := []testCase{
+		{
+			name:  "return err when not found",
+			input: []esv1alpha1.FakeProviderData{},
+			request: esv1alpha1.ExternalSecretDataRemoteRef{
+				Key:     "/foo",
+				Version: "v2",
+			},
+			expErr: "secret value not found",
+		},
+		{
+			name: "get correct value from multiple versions",
+			input: []esv1alpha1.FakeProviderData{
+				{
+					Key:     "/foo",
+					Value:   "bar2",
+					Version: "v2",
+				},
+				{
+					Key:   "junk",
+					Value: "xxxxx",
+				},
+				{
+					Key:     "/foo",
+					Value:   "bar1",
+					Version: "v1",
+				},
+			},
+			request: esv1alpha1.ExternalSecretDataRemoteRef{
+				Key:     "/foo",
+				Version: "v2",
+			},
+			expValue: "bar2",
+		},
+	}
+
+	for _, row := range tbl {
+		t.Run(row.name, func(t *testing.T) {
+			cl, err := p.NewClient(context.Background(), &esv1alpha1.SecretStore{
+				Spec: esv1alpha1.SecretStoreSpec{
+					Provider: &esv1alpha1.SecretStoreProvider{
+						Fake: &esv1alpha1.FakeProvider{
+							Data: row.input,
+						},
+					},
+				},
+			}, nil, "")
+			gomega.Expect(err).ToNot(gomega.HaveOccurred())
+			out, err := cl.GetSecret(context.Background(), row.request)
+			if row.expErr != "" {
+				gomega.Expect(err).To(gomega.MatchError(row.expErr))
+			} else {
+				gomega.Expect(err).ToNot(gomega.HaveOccurred())
+			}
+			gomega.Expect(string(out)).To(gomega.Equal(row.expValue))
+		})
+	}
+}
+
+type testMapCase struct {
+	name     string
+	input    []esv1alpha1.FakeProviderData
+	request  esv1alpha1.ExternalSecretDataRemoteRef
+	expValue map[string][]byte
+	expErr   string
+}
+
+func TestGetSecretMap(t *testing.T) {
+	gomega.RegisterTestingT(t)
+	p := &Provider{}
+	tbl := []testMapCase{
+		{
+			name:  "return err when not found",
+			input: []esv1alpha1.FakeProviderData{},
+			request: esv1alpha1.ExternalSecretDataRemoteRef{
+				Key:     "/foo",
+				Version: "v2",
+			},
+			expErr: "secret value not found",
+		},
+		{
+			name: "get correct value from multiple versions",
+			input: []esv1alpha1.FakeProviderData{
+				{
+					Key: "junk",
+					ValueMap: map[string]string{
+						"junk": "ok",
+					},
+				},
+				{
+					Key: "/foo",
+					ValueMap: map[string]string{
+						"foo": "bar",
+						"baz": "bang",
+					},
+					Version: "v1",
+				},
+				{
+					Key: "/foo",
+					ValueMap: map[string]string{
+						"foo": "bar",
+						"baz": "bang",
+					},
+					Version: "v2",
+				},
+			},
+			request: esv1alpha1.ExternalSecretDataRemoteRef{
+				Key:     "/foo",
+				Version: "v2",
+			},
+			expValue: map[string][]byte{
+				"foo": []byte("bar"),
+				"baz": []byte("bang"),
+			},
+		},
+	}
+
+	for _, row := range tbl {
+		t.Run(row.name, func(t *testing.T) {
+			cl, err := p.NewClient(context.Background(), &esv1alpha1.SecretStore{
+				Spec: esv1alpha1.SecretStoreSpec{
+					Provider: &esv1alpha1.SecretStoreProvider{
+						Fake: &esv1alpha1.FakeProvider{
+							Data: row.input,
+						},
+					},
+				},
+			}, nil, "")
+			gomega.Expect(err).ToNot(gomega.HaveOccurred())
+			out, err := cl.GetSecretMap(context.Background(), row.request)
+			if row.expErr != "" {
+				gomega.Expect(err).To(gomega.MatchError(row.expErr))
+			} else {
+				gomega.Expect(err).ToNot(gomega.HaveOccurred())
+			}
+			gomega.Expect(out).To(gomega.Equal(row.expValue))
+		})
+	}
+}

+ 1 - 0
pkg/provider/register/register.go

@@ -21,6 +21,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/alibaba"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/aws"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/fake"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"

+ 103 - 0
pkg/provider/testing/fake/fake.go

@@ -0,0 +1,103 @@
+/*
+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 (
+	"context"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/pkg/provider"
+	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+)
+
+var _ provider.Provider = &Client{}
+
+// Client is a fake client for testing.
+type Client struct {
+	NewFn func(context.Context, esv1alpha1.GenericStore, client.Client,
+		string) (provider.SecretsClient, error)
+	GetSecretFn    func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error)
+	GetSecretMapFn func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
+}
+
+// New returns a fake provider/client.
+func New() *Client {
+	v := &Client{
+		GetSecretFn: func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+			return nil, nil
+		},
+		GetSecretMapFn: func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+			return nil, nil
+		},
+	}
+
+	v.NewFn = func(context.Context, esv1alpha1.GenericStore, client.Client, string) (provider.SecretsClient, error) {
+		return v, nil
+	}
+
+	return v
+}
+
+// RegisterAs registers the fake client in the schema.
+func (v *Client) RegisterAs(provider *esv1alpha1.SecretStoreProvider) {
+	schema.ForceRegister(v, provider)
+}
+
+// GetSecret implements the provider.Provider interface.
+func (v *Client) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	return v.GetSecretFn(ctx, ref)
+}
+
+// WithGetSecret wraps secret data returned by this provider.
+func (v *Client) WithGetSecret(secData []byte, err error) *Client {
+	v.GetSecretFn = func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+		return secData, err
+	}
+	return v
+}
+
+// GetSecretMap imeplements the provider.Provider interface.
+func (v *Client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	return v.GetSecretMapFn(ctx, ref)
+}
+func (v *Client) Close(ctx context.Context) error {
+	return nil
+}
+
+// WithGetSecretMap wraps the secret data map returned by this fake provider.
+func (v *Client) WithGetSecretMap(secData map[string][]byte, err error) *Client {
+	v.GetSecretMapFn = func(context.Context, esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+		return secData, err
+	}
+	return v
+}
+
+// WithNew wraps the fake provider factory function.
+func (v *Client) WithNew(f func(context.Context, esv1alpha1.GenericStore, client.Client,
+	string) (provider.SecretsClient, error)) *Client {
+	v.NewFn = f
+	return v
+}
+
+// NewClient returns a new fake provider.
+func (v *Client) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+	c, err := v.NewFn(ctx, store, kube, namespace)
+	if err != nil {
+		return nil, err
+	}
+	return c, nil
+}