Browse Source

feat: allow generators to be referenced from a PushSecret (#3965)

This removes the need for an intermediary Kind=ExternalSecret and
Kind=Secret when using a generator.

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 1 year ago
parent
commit
76cf8ad263

+ 8 - 1
apis/externalsecrets/v1alpha1/pushsecret_types.go

@@ -92,9 +92,16 @@ type PushSecretSecret struct {
 	Name string `json:"name"`
 	Name string `json:"name"`
 }
 }
 
 
+// +kubebuilder:validation:MinProperties=1
+// +kubebuilder:validation:MaxProperties=1
 type PushSecretSelector struct {
 type PushSecretSelector struct {
 	// Select a Secret to Push.
 	// Select a Secret to Push.
-	Secret PushSecretSecret `json:"secret"`
+	// +optional
+	Secret *PushSecretSecret `json:"secret,omitempty"`
+
+	// Point to a generator to create a Secret.
+	// +optional
+	GeneratorRef *esv1beta1.GeneratorRef `json:"generatorRef,omitempty"`
 }
 }
 
 
 type PushSecretRemoteRef struct {
 type PushSecretRemoteRef struct {

+ 11 - 2
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -1208,7 +1208,16 @@ func (in *PushSecretSecret) DeepCopy() *PushSecretSecret {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PushSecretSelector) DeepCopyInto(out *PushSecretSelector) {
 func (in *PushSecretSelector) DeepCopyInto(out *PushSecretSelector) {
 	*out = *in
 	*out = *in
-	out.Secret = in.Secret
+	if in.Secret != nil {
+		in, out := &in.Secret, &out.Secret
+		*out = new(PushSecretSecret)
+		**out = **in
+	}
+	if in.GeneratorRef != nil {
+		in, out := &in.GeneratorRef, &out.GeneratorRef
+		*out = new(v1beta1.GeneratorRef)
+		**out = **in
+	}
 }
 }
 
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretSelector.
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretSelector.
@@ -1236,7 +1245,7 @@ func (in *PushSecretSpec) DeepCopyInto(out *PushSecretSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 		}
 	}
 	}
-	out.Selector = in.Selector
+	in.Selector.DeepCopyInto(&out.Selector)
 	if in.Data != nil {
 	if in.Data != nil {
 		in, out := &in.Data, &out.Data
 		in, out := &in.Data, &out.Data
 		*out = make([]PushSecretData, len(*in))
 		*out = make([]PushSecretData, len(*in))

+ 1 - 0
cmd/root.go

@@ -216,6 +216,7 @@ var rootCmd = &cobra.Command{
 				Log:             ctrl.Log.WithName("controllers").WithName("PushSecret"),
 				Log:             ctrl.Log.WithName("controllers").WithName("PushSecret"),
 				Scheme:          mgr.GetScheme(),
 				Scheme:          mgr.GetScheme(),
 				ControllerClass: controllerClass,
 				ControllerClass: controllerClass,
+				RestConfig:      mgr.GetConfig(),
 				RequeueInterval: time.Hour,
 				RequeueInterval: time.Hour,
 			}).SetupWithManager(mgr); err != nil {
 			}).SetupWithManager(mgr); err != nil {
 				setupLog.Error(err, errCreateController, "controller", "PushSecret")
 				setupLog.Error(err, errCreateController, "controller", "PushSecret")

+ 20 - 2
config/crds/bases/external-secrets.io_pushsecrets.yaml

@@ -165,7 +165,27 @@ spec:
                 type: array
                 type: array
               selector:
               selector:
                 description: The Secret Selector (k8s source) for the Push Secret
                 description: The Secret Selector (k8s source) for the Push Secret
+                maxProperties: 1
+                minProperties: 1
                 properties:
                 properties:
+                  generatorRef:
+                    description: Point to a generator to create a Secret.
+                    properties:
+                      apiVersion:
+                        default: generators.external-secrets.io/v1alpha1
+                        description: Specify the apiVersion of the generator resource
+                        type: string
+                      kind:
+                        description: Specify the Kind of the resource, e.g. Password,
+                          ACRAccessToken etc.
+                        type: string
+                      name:
+                        description: Specify the name of the generator resource
+                        type: string
+                    required:
+                    - kind
+                    - name
+                    type: object
                   secret:
                   secret:
                     description: Select a Secret to Push.
                     description: Select a Secret to Push.
                     properties:
                     properties:
@@ -176,8 +196,6 @@ spec:
                     required:
                     required:
                     - name
                     - name
                     type: object
                     type: object
-                required:
-                - secret
                 type: object
                 type: object
               template:
               template:
                 description: Template defines a blueprint for the created Secret resource.
                 description: Template defines a blueprint for the created Secret resource.

+ 19 - 2
deploy/crds/bundle.yaml

@@ -6258,7 +6258,26 @@ spec:
                   type: array
                   type: array
                 selector:
                 selector:
                   description: The Secret Selector (k8s source) for the Push Secret
                   description: The Secret Selector (k8s source) for the Push Secret
+                  maxProperties: 1
+                  minProperties: 1
                   properties:
                   properties:
+                    generatorRef:
+                      description: Point to a generator to create a Secret.
+                      properties:
+                        apiVersion:
+                          default: generators.external-secrets.io/v1alpha1
+                          description: Specify the apiVersion of the generator resource
+                          type: string
+                        kind:
+                          description: Specify the Kind of the resource, e.g. Password, ACRAccessToken etc.
+                          type: string
+                        name:
+                          description: Specify the name of the generator resource
+                          type: string
+                      required:
+                        - kind
+                        - name
+                      type: object
                     secret:
                     secret:
                       description: Select a Secret to Push.
                       description: Select a Secret to Push.
                       properties:
                       properties:
@@ -6268,8 +6287,6 @@ spec:
                       required:
                       required:
                         - name
                         - name
                       type: object
                       type: object
-                  required:
-                    - secret
                   type: object
                   type: object
                 template:
                 template:
                   description: Template defines a blueprint for the created Secret resource.
                   description: Template defines a blueprint for the created Secret resource.

+ 9 - 0
docs/guides/pushsecrets.md

@@ -43,3 +43,12 @@ This will _marshal_ the entire secret data and push it into this single property
 
 
 ### Key conversion strategy
 ### Key conversion strategy
 You can also set `data[*].conversionStrategy: ReverseUnicode` to reverse the invalid character replaced by the `conversionStrategy: Unicode` configuration in the `ExternalSecret` object as [documented here](../guides/getallsecrets.md#avoiding-name-conflicts).
 You can also set `data[*].conversionStrategy: ReverseUnicode` to reverse the invalid character replaced by the `conversionStrategy: Unicode` configuration in the `ExternalSecret` object as [documented here](../guides/getallsecrets.md#avoiding-name-conflicts).
+
+## Rotate Secrets
+
+You can use ESO to rotate secrets by using the PushSecret and Generator resources. ESO will consult the `Kind=Generator` to generate a new secret and then ESO will store it.
+Every `spec.refreshInterval` the secret will be rotated and the value will be replaced in the store unless `spec.updatePolicy=IfNotExist` is set. Then ESO will generate the secret once and won't rotate it.
+
+```yaml
+{% include 'pushsecret-generator-rotation-example.yaml' %}
+```

+ 5 - 0
docs/snippets/full-pushsecret.yaml

@@ -14,6 +14,11 @@ spec:
   selector:
   selector:
     secret:
     secret:
       name: pokedex-credentials # Source Kubernetes secret to be pushed
       name: pokedex-credentials # Source Kubernetes secret to be pushed
+    # Alternatively, you can point to a generator that produces values to be pushed
+    generatorRef:
+      apiVersion: external-secrets.io/v1alpha1
+      kind: ECRAuthorizationToken
+      name: prod-registry-credentials
   template:
   template:
     metadata:
     metadata:
       annotations: { }
       annotations: { }

+ 33 - 0
docs/snippets/pushsecret-generator-rotation-example.yaml

@@ -0,0 +1,33 @@
+{% raw %}
+apiVersion: generators.external-secrets.io/v1alpha1
+kind: Password
+metadata:
+  name: strong-password
+spec:
+  length: 128
+  digits: 5
+  symbols: 5
+  symbolCharacters: "-_$@"
+  noUpper: false
+  allowRepeat: true
+---
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: pushsecret-example
+spec:
+  refreshInterval: 6h
+  secretStoreRefs:
+    - name: aws-parameter-store
+      kind: SecretStore
+  selector:
+    generatorRef:
+      apiVersion: generators.external-secrets.io/v1alpha1
+      kind: Password
+      name: strong-password
+  data:
+    - match:
+        secretKey: password # property in the generator output
+        remoteRef:
+          remoteKey: prod/myql/password
+{% endraw %}

+ 1 - 1
e2e/suites/provider/cases/template/template.go

@@ -133,7 +133,7 @@ func genericPushSecretTemplate(f *framework.Framework) (string, func(*framework.
 			Type: v1.SecretTypeOpaque,
 			Type: v1.SecretTypeOpaque,
 		}
 		}
 		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
 		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
-			Secret: esv1alpha1.PushSecretSecret{
+			Secret: &esv1alpha1.PushSecretSecret{
 				Name: secretKey1,
 				Name: secretKey1,
 			},
 			},
 		}
 		}

+ 3 - 2
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -48,6 +48,7 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics"
 	"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics"
 	ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
 	ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 	"github.com/external-secrets/external-secrets/pkg/utils"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
 
 
 	// Loading registered generators.
 	// Loading registered generators.
 	_ "github.com/external-secrets/external-secrets/pkg/generator/register"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/register"
@@ -549,11 +550,11 @@ func shouldSkipUnmanagedStore(ctx context.Context, namespace string, r *Reconcil
 
 
 		// verify that generator's controllerClass matches
 		// verify that generator's controllerClass matches
 		if ref.SourceRef != nil && ref.SourceRef.GeneratorRef != nil {
 		if ref.SourceRef != nil && ref.SourceRef.GeneratorRef != nil {
-			genDef, err := r.getGeneratorDefinition(ctx, namespace, ref.SourceRef.GeneratorRef)
+			_, obj, err := resolvers.GeneratorRef(ctx, r.RestConfig, namespace, ref.SourceRef.GeneratorRef)
 			if err != nil {
 			if err != nil {
 				return false, err
 				return false, err
 			}
 			}
-			skipGenerator, err := shouldSkipGenerator(r, genDef)
+			skipGenerator, err := shouldSkipGenerator(r, obj)
 			if err != nil {
 			if err != nil {
 				return false, err
 				return false, err
 			}
 			}

+ 4 - 55
pkg/controllers/externalsecret/externalsecret_controller_secret.go

@@ -22,17 +22,13 @@ import (
 
 
 	v1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
 	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/client-go/discovery"
-	"k8s.io/client-go/dynamic"
-	"k8s.io/client-go/restmapper"
 
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
 	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
 	// Loading registered providers.
 	// Loading registered providers.
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 	"github.com/external-secrets/external-secrets/pkg/utils"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
 
 
 	// Loading registered generators.
 	// Loading registered generators.
 	_ "github.com/external-secrets/external-secrets/pkg/generator/register"
 	_ "github.com/external-secrets/external-secrets/pkg/generator/register"
@@ -116,15 +112,11 @@ func toStoreGenSourceRef(ref *esv1beta1.StoreSourceRef) *esv1beta1.StoreGenerato
 }
 }
 
 
 func (r *Reconciler) handleGenerateSecrets(ctx context.Context, namespace string, remoteRef esv1beta1.ExternalSecretDataFromRemoteRef, i int) (map[string][]byte, error) {
 func (r *Reconciler) handleGenerateSecrets(ctx context.Context, namespace string, remoteRef esv1beta1.ExternalSecretDataFromRemoteRef, i int) (map[string][]byte, error) {
-	genDef, err := r.getGeneratorDefinition(ctx, namespace, remoteRef.SourceRef.GeneratorRef)
+	gen, obj, err := resolvers.GeneratorRef(ctx, r.RestConfig, namespace, remoteRef.SourceRef.GeneratorRef)
 	if err != nil {
 	if err != nil {
-		return nil, err
-	}
-	gen, err := genv1alpha1.GetGenerator(genDef)
-	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("unable to resolve generator: %w", err)
 	}
 	}
-	secretMap, err := gen.Generate(ctx, genDef, r.Client, namespace)
+	secretMap, err := gen.Generate(ctx, obj, r.Client, namespace)
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf(errGenerate, i, err)
 		return nil, fmt.Errorf(errGenerate, i, err)
 	}
 	}
@@ -138,49 +130,6 @@ func (r *Reconciler) handleGenerateSecrets(ctx context.Context, namespace string
 	return secretMap, err
 	return secretMap, err
 }
 }
 
 
-// getGeneratorDefinition returns the generator JSON for a given sourceRef
-// when it uses a generatorRef it fetches the resource and returns the JSON.
-func (r *Reconciler) getGeneratorDefinition(ctx context.Context, namespace string, generatorRef *esv1beta1.GeneratorRef) (*apiextensions.JSON, error) {
-	// client-go dynamic client needs a GVR to fetch the resource
-	// But we only have the GVK in our generatorRef.
-	//
-	// TODO: there is no need to discover the GroupVersionResource
-	//       this should be cached.
-	c := discovery.NewDiscoveryClientForConfigOrDie(r.RestConfig)
-	groupResources, err := restmapper.GetAPIGroupResources(c)
-	if err != nil {
-		return nil, err
-	}
-
-	gv, err := schema.ParseGroupVersion(generatorRef.APIVersion)
-	if err != nil {
-		return nil, err
-	}
-	mapper := restmapper.NewDiscoveryRESTMapper(groupResources)
-	mapping, err := mapper.RESTMapping(schema.GroupKind{
-		Group: gv.Group,
-		Kind:  generatorRef.Kind,
-	})
-	if err != nil {
-		return nil, err
-	}
-	d, err := dynamic.NewForConfig(r.RestConfig)
-	if err != nil {
-		return nil, err
-	}
-	res, err := d.Resource(mapping.Resource).
-		Namespace(namespace).
-		Get(ctx, generatorRef.Name, metav1.GetOptions{})
-	if err != nil {
-		return nil, err
-	}
-	jsonRes, err := res.MarshalJSON()
-	if err != nil {
-		return nil, err
-	}
-	return &apiextensions.JSON{Raw: jsonRes}, nil
-}
-
 func (r *Reconciler) handleExtractSecrets(ctx context.Context, externalSecret *esv1beta1.ExternalSecret, remoteRef esv1beta1.ExternalSecretDataFromRemoteRef, cmgr *secretstore.Manager, i int) (map[string][]byte, error) {
 func (r *Reconciler) handleExtractSecrets(ctx context.Context, externalSecret *esv1beta1.ExternalSecret, remoteRef esv1beta1.ExternalSecretDataFromRemoteRef, cmgr *secretstore.Manager, i int) (map[string][]byte, error) {
 	client, err := cmgr.Get(ctx, externalSecret.Spec.SecretStoreRef, externalSecret.Namespace, remoteRef.SourceRef)
 	client, err := cmgr.Get(ctx, externalSecret.Spec.SecretStoreRef, externalSecret.Namespace, remoteRef.SourceRef)
 	if err != nil {
 	if err != nil {

+ 37 - 7
pkg/controllers/pushsecret/pushsecret_controller.go

@@ -28,6 +28,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/tools/record"
 	ctrl "sigs.k8s.io/controller-runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -40,6 +41,10 @@ import (
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
 	"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
 	"github.com/external-secrets/external-secrets/pkg/provider/util/locks"
 	"github.com/external-secrets/external-secrets/pkg/provider/util/locks"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 	"github.com/external-secrets/external-secrets/pkg/utils"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+
+	// load generators.
+	_ "github.com/external-secrets/external-secrets/pkg/generator/register"
 )
 )
 
 
 const (
 const (
@@ -59,6 +64,7 @@ type Reconciler struct {
 	Log             logr.Logger
 	Log             logr.Logger
 	Scheme          *runtime.Scheme
 	Scheme          *runtime.Scheme
 	recorder        record.EventRecorder
 	recorder        record.EventRecorder
+	RestConfig      *rest.Config
 	RequeueInterval time.Duration
 	RequeueInterval time.Duration
 	ControllerClass string
 	ControllerClass string
 }
 }
@@ -148,7 +154,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 	default:
 	default:
 	}
 	}
 
 
-	secret, err := r.GetSecret(ctx, ps)
+	secret, err := r.resolveSecret(ctx, ps)
 	if err != nil {
 	if err != nil {
 		r.markAsFailed(errFailedGetSecret, &ps, nil)
 		r.markAsFailed(errFailedGetSecret, &ps, nil)
 
 
@@ -347,14 +353,38 @@ func secretKeyExists(key string, secret *v1.Secret) bool {
 	return key == "" || ok
 	return key == "" || ok
 }
 }
 
 
-func (r *Reconciler) GetSecret(ctx context.Context, ps esapi.PushSecret) (*v1.Secret, error) {
-	secretName := types.NamespacedName{Name: ps.Spec.Selector.Secret.Name, Namespace: ps.Namespace}
-	secret := &v1.Secret{}
-	err := r.Client.Get(ctx, secretName, secret)
+func (r *Reconciler) resolveSecret(ctx context.Context, ps esapi.PushSecret) (*v1.Secret, error) {
+	if ps.Spec.Selector.Secret != nil {
+		secretName := types.NamespacedName{Name: ps.Spec.Selector.Secret.Name, Namespace: ps.Namespace}
+		secret := &v1.Secret{}
+		err := r.Client.Get(ctx, secretName, secret)
+		if err != nil {
+			return nil, err
+		}
+		return secret, nil
+	}
+	if ps.Spec.Selector.GeneratorRef != nil {
+		return r.resolveSecretFromGenerator(ctx, ps.Namespace, ps.Spec.Selector.GeneratorRef)
+	}
+	return nil, errors.New("no secret selector provided")
+}
+
+func (r *Reconciler) resolveSecretFromGenerator(ctx context.Context, namespace string, generatorRef *v1beta1.GeneratorRef) (*v1.Secret, error) {
+	gen, obj, err := resolvers.GeneratorRef(ctx, r.RestConfig, namespace, generatorRef)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("unable to resolve generator: %w", err)
 	}
 	}
-	return secret, nil
+	secretMap, err := gen.Generate(ctx, obj, r.Client, namespace)
+	if err != nil {
+		return nil, fmt.Errorf("unable to generate: %w", err)
+	}
+	return &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "___generated-secret",
+			Namespace: namespace,
+		},
+		Data: secretMap,
+	}, err
 }
 }
 
 
 func (r *Reconciler) GetSecretStores(ctx context.Context, ps esapi.PushSecret) (map[esapi.PushSecretStoreRef]v1beta1.GenericStore, error) {
 func (r *Reconciler) GetSecretStores(ctx context.Context, ps esapi.PushSecret) (map[esapi.PushSecretStoreRef]v1beta1.GenericStore, error) {

+ 48 - 9
pkg/controllers/pushsecret/pushsecret_controller_test.go

@@ -30,6 +30,7 @@ import (
 
 
 	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
 	ctest "github.com/external-secrets/external-secrets/pkg/controllers/commontest"
 	ctest "github.com/external-secrets/external-secrets/pkg/controllers/commontest"
 	"github.com/external-secrets/external-secrets/pkg/controllers/pushsecret/psmetrics"
 	"github.com/external-secrets/external-secrets/pkg/controllers/pushsecret/psmetrics"
 	"github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
 	"github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
@@ -99,6 +100,21 @@ var _ = Describe("PushSecret controller", func() {
 		PushSecretNamespace, err = ctest.CreateNamespace("test-ns", k8sClient)
 		PushSecretNamespace, err = ctest.CreateNamespace("test-ns", k8sClient)
 		Expect(err).ToNot(HaveOccurred())
 		Expect(err).ToNot(HaveOccurred())
 		fakeProvider.Reset()
 		fakeProvider.Reset()
+
+		Expect(k8sClient.Create(context.Background(), &genv1alpha1.Fake{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "Fake",
+				APIVersion: "generators.external-secrets.io/v1alpha1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "test",
+				Namespace: PushSecretNamespace,
+			},
+			Spec: genv1alpha1.FakeSpec{
+				Data: map[string]string{
+					"key": "foo-bar-from-generator",
+				},
+			}})).ToNot(HaveOccurred())
 	})
 	})
 
 
 	AfterEach(func() {
 	AfterEach(func() {
@@ -162,7 +178,7 @@ var _ = Describe("PushSecret controller", func() {
 						},
 						},
 					},
 					},
 					Selector: v1alpha1.PushSecretSelector{
 					Selector: v1alpha1.PushSecretSelector{
-						Secret: v1alpha1.PushSecretSecret{
+						Secret: &v1alpha1.PushSecretSecret{
 							Name: SecretName,
 							Name: SecretName,
 						},
 						},
 					},
 					},
@@ -395,7 +411,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 					},
 				},
 				},
 				Selector: v1alpha1.PushSecretSelector{
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 						Name: SecretName,
 					},
 					},
 				},
 				},
@@ -459,7 +475,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 					},
 				},
 				},
 				Selector: v1alpha1.PushSecretSelector{
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 						Name: SecretName,
 					},
 					},
 				},
 				},
@@ -515,7 +531,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 					},
 				},
 				},
 				Selector: v1alpha1.PushSecretSelector{
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 						Name: SecretName,
 					},
 					},
 				},
 				},
@@ -570,7 +586,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 					},
 				},
 				},
 				Selector: v1alpha1.PushSecretSelector{
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 						Name: SecretName,
 					},
 					},
 				},
 				},
@@ -716,7 +732,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 					},
 				},
 				},
 				Selector: v1alpha1.PushSecretSelector{
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 						Name: SecretName,
 					},
 					},
 				},
 				},
@@ -782,7 +798,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 					},
 				},
 				},
 				Selector: v1alpha1.PushSecretSelector{
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 						Name: SecretName,
 					},
 					},
 				},
 				},
@@ -861,6 +877,28 @@ var _ = Describe("PushSecret controller", func() {
 			return bytes.Equal(secretValue, providerValue) && checkCondition(ps.Status, expected)
 			return bytes.Equal(secretValue, providerValue) && checkCondition(ps.Status, expected)
 		}
 		}
 	}
 	}
+
+	syncWithGenerator := func(tc *testCase) {
+		fakeProvider.SetSecretFn = func() error {
+			return nil
+		}
+		tc.pushsecret.Spec.Selector.Secret = nil
+		tc.pushsecret.Spec.Selector.GeneratorRef = &v1beta1.GeneratorRef{
+			APIVersion: "generators.external-secrets.io/v1alpha1",
+			Kind:       "Fake",
+			Name:       "test",
+		}
+		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
+			providerValue := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey].Value
+			expected := v1alpha1.PushSecretStatusCondition{
+				Type:    v1alpha1.PushSecretReady,
+				Status:  v1.ConditionTrue,
+				Reason:  v1alpha1.ReasonSynced,
+				Message: "PushSecret synced successfully",
+			}
+			return bytes.Equal([]byte("foo-bar-from-generator"), providerValue) && checkCondition(ps.Status, expected)
+		}
+	}
 	// if target Secret name is not specified it should use the ExternalSecret name.
 	// if target Secret name is not specified it should use the ExternalSecret name.
 	syncWithClusterStoreMatchingLabels := func(tc *testCase) {
 	syncWithClusterStoreMatchingLabels := func(tc *testCase) {
 		fakeProvider.SetSecretFn = func() error {
 		fakeProvider.SetSecretFn = func() error {
@@ -884,7 +922,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 					},
 				},
 				},
 				Selector: v1alpha1.PushSecretSelector{
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 						Name: SecretName,
 					},
 					},
 				},
 				},
@@ -1069,6 +1107,7 @@ var _ = Describe("PushSecret controller", func() {
 		Entry("should sync to stores matching labels", syncMatchingLabels),
 		Entry("should sync to stores matching labels", syncMatchingLabels),
 		Entry("should sync with ClusterStore", syncWithClusterStore),
 		Entry("should sync with ClusterStore", syncWithClusterStore),
 		Entry("should sync with ClusterStore matching labels", syncWithClusterStoreMatchingLabels),
 		Entry("should sync with ClusterStore matching labels", syncWithClusterStoreMatchingLabels),
+		Entry("should sync with Generator", syncWithGenerator),
 		Entry("should fail if Secret is not created", failNoSecret),
 		Entry("should fail if Secret is not created", failNoSecret),
 		Entry("should fail if Secret Key does not exist", failNoSecretKey),
 		Entry("should fail if Secret Key does not exist", failNoSecretKey),
 		Entry("should fail if SetSecret fails", setSecretFail),
 		Entry("should fail if SetSecret fails", setSecretFail),
@@ -1168,7 +1207,7 @@ var _ = Describe("PushSecret Controller Un/Managed Stores", func() {
 						},
 						},
 					},
 					},
 					Selector: v1alpha1.PushSecretSelector{
 					Selector: v1alpha1.PushSecretSelector{
-						Secret: v1alpha1.PushSecretSecret{
+						Secret: &v1alpha1.PushSecretSecret{
 							Name: SecretName,
 							Name: SecretName,
 						},
 						},
 					},
 					},

+ 5 - 1
pkg/controllers/pushsecret/suite_test.go

@@ -32,6 +32,7 @@ import (
 
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
 
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	. "github.com/onsi/gomega"
@@ -72,6 +73,8 @@ var _ = BeforeSuite(func() {
 	Expect(err).NotTo(HaveOccurred())
 	Expect(err).NotTo(HaveOccurred())
 	err = esv1alpha1.AddToScheme(scheme.Scheme)
 	err = esv1alpha1.AddToScheme(scheme.Scheme)
 	Expect(err).NotTo(HaveOccurred())
 	Expect(err).NotTo(HaveOccurred())
+	err = genv1alpha1.AddToScheme(scheme.Scheme)
+	Expect(err).NotTo(HaveOccurred())
 
 
 	k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
 	k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
 		Scheme: scheme.Scheme,
 		Scheme: scheme.Scheme,
@@ -90,7 +93,8 @@ var _ = BeforeSuite(func() {
 	err = (&Reconciler{
 	err = (&Reconciler{
 		Client:          k8sClient,
 		Client:          k8sClient,
 		Scheme:          k8sManager.GetScheme(),
 		Scheme:          k8sManager.GetScheme(),
-		Log:             ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
+		Log:             ctrl.Log.WithName("controllers").WithName("PushSecret"),
+		RestConfig:      cfg,
 		RequeueInterval: time.Second,
 		RequeueInterval: time.Second,
 	}).SetupWithManager(k8sManager)
 	}).SetupWithManager(k8sManager)
 	Expect(err).ToNot(HaveOccurred())
 	Expect(err).ToNot(HaveOccurred())

+ 84 - 0
pkg/utils/resolvers/generator.go

@@ -0,0 +1,84 @@
+/*
+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 resolvers
+
+import (
+	"context"
+	"fmt"
+
+	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/restmapper"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+)
+
+// GeneratorRef resolves a generator reference to a generator implementation.
+func GeneratorRef(ctx context.Context, restConfig *rest.Config, namespace string, generatorRef *esv1beta1.GeneratorRef) (genv1alpha1.Generator, *apiextensions.JSON, error) {
+	obj, err := getGeneratorDefinition(ctx, restConfig, namespace, generatorRef)
+	if err != nil {
+		return nil, nil, fmt.Errorf("unable to get generator definition: %w", err)
+	}
+	generator, err := genv1alpha1.GetGenerator(obj)
+	if err != nil {
+		return nil, nil, fmt.Errorf("unable to get generator: %w", err)
+	}
+	return generator, obj, nil
+}
+
+func getGeneratorDefinition(ctx context.Context, restConfig *rest.Config, namespace string, generatorRef *esv1beta1.GeneratorRef) (*apiextensions.JSON, error) {
+	// client-go dynamic client needs a GVR to fetch the resource
+	// But we only have the GVK in our generatorRef.
+	//
+	// TODO: there is no need to discover the GroupVersionResource
+	//       this should be cached.
+	c := discovery.NewDiscoveryClientForConfigOrDie(restConfig)
+	groupResources, err := restmapper.GetAPIGroupResources(c)
+	if err != nil {
+		return nil, err
+	}
+
+	gv, err := schema.ParseGroupVersion(generatorRef.APIVersion)
+	if err != nil {
+		return nil, err
+	}
+	mapper := restmapper.NewDiscoveryRESTMapper(groupResources)
+	mapping, err := mapper.RESTMapping(schema.GroupKind{
+		Group: gv.Group,
+		Kind:  generatorRef.Kind,
+	})
+	if err != nil {
+		return nil, err
+	}
+	d, err := dynamic.NewForConfig(restConfig)
+	if err != nil {
+		return nil, err
+	}
+	res, err := d.Resource(mapping.Resource).
+		Namespace(namespace).
+		Get(ctx, generatorRef.Name, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+	jsonRes, err := res.MarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return &apiextensions.JSON{Raw: jsonRes}, nil
+}