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"`
 }
 
+// +kubebuilder:validation:MinProperties=1
+// +kubebuilder:validation:MaxProperties=1
 type PushSecretSelector struct {
 	// 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 {

+ 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.
 func (in *PushSecretSelector) DeepCopyInto(out *PushSecretSelector) {
 	*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.
@@ -1236,7 +1245,7 @@ func (in *PushSecretSpec) DeepCopyInto(out *PushSecretSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
-	out.Selector = in.Selector
+	in.Selector.DeepCopyInto(&out.Selector)
 	if in.Data != nil {
 		in, out := &in.Data, &out.Data
 		*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"),
 				Scheme:          mgr.GetScheme(),
 				ControllerClass: controllerClass,
+				RestConfig:      mgr.GetConfig(),
 				RequeueInterval: time.Hour,
 			}).SetupWithManager(mgr); err != nil {
 				setupLog.Error(err, errCreateController, "controller", "PushSecret")

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

@@ -165,7 +165,27 @@ spec:
                 type: array
               selector:
                 description: The Secret Selector (k8s source) for the Push Secret
+                maxProperties: 1
+                minProperties: 1
                 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:
                     description: Select a Secret to Push.
                     properties:
@@ -176,8 +196,6 @@ spec:
                     required:
                     - name
                     type: object
-                required:
-                - secret
                 type: object
               template:
                 description: Template defines a blueprint for the created Secret resource.

+ 19 - 2
deploy/crds/bundle.yaml

@@ -6258,7 +6258,26 @@ spec:
                   type: array
                 selector:
                   description: The Secret Selector (k8s source) for the Push Secret
+                  maxProperties: 1
+                  minProperties: 1
                   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:
                       description: Select a Secret to Push.
                       properties:
@@ -6268,8 +6287,6 @@ spec:
                       required:
                         - name
                       type: object
-                  required:
-                    - secret
                   type: object
                 template:
                   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
 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:
     secret:
       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:
     metadata:
       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,
 		}
 		tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
-			Secret: esv1alpha1.PushSecretSecret{
+			Secret: &esv1alpha1.PushSecretSecret{
 				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"
 	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/resolvers"
 
 	// Loading registered generators.
 	_ "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
 		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 {
 				return false, err
 			}
-			skipGenerator, err := shouldSkipGenerator(r, genDef)
+			skipGenerator, err := shouldSkipGenerator(r, obj)
 			if err != nil {
 				return false, err
 			}

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

@@ -22,17 +22,13 @@ import (
 
 	v1 "k8s.io/api/core/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"
 	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
 	// Loading registered providers.
 	"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/resolvers"
 
 	// Loading registered generators.
 	_ "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) {
-	genDef, err := r.getGeneratorDefinition(ctx, namespace, remoteRef.SourceRef.GeneratorRef)
+	gen, obj, err := resolvers.GeneratorRef(ctx, r.RestConfig, namespace, remoteRef.SourceRef.GeneratorRef)
 	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 {
 		return nil, fmt.Errorf(errGenerate, i, err)
 	}
@@ -138,49 +130,6 @@ func (r *Reconciler) handleGenerateSecrets(ctx context.Context, namespace string
 	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) {
 	client, err := cmgr.Get(ctx, externalSecret.Spec.SecretStoreRef, externalSecret.Namespace, remoteRef.SourceRef)
 	if err != nil {

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

@@ -28,6 +28,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/record"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"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/provider/util/locks"
 	"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 (
@@ -59,6 +64,7 @@ type Reconciler struct {
 	Log             logr.Logger
 	Scheme          *runtime.Scheme
 	recorder        record.EventRecorder
+	RestConfig      *rest.Config
 	RequeueInterval time.Duration
 	ControllerClass string
 }
@@ -148,7 +154,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 	default:
 	}
 
-	secret, err := r.GetSecret(ctx, ps)
+	secret, err := r.resolveSecret(ctx, ps)
 	if err != nil {
 		r.markAsFailed(errFailedGetSecret, &ps, nil)
 
@@ -347,14 +353,38 @@ func secretKeyExists(key string, secret *v1.Secret) bool {
 	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 {
-		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) {

+ 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/v1beta1"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
 	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/provider/testing/fake"
@@ -99,6 +100,21 @@ var _ = Describe("PushSecret controller", func() {
 		PushSecretNamespace, err = ctest.CreateNamespace("test-ns", k8sClient)
 		Expect(err).ToNot(HaveOccurred())
 		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() {
@@ -162,7 +178,7 @@ var _ = Describe("PushSecret controller", func() {
 						},
 					},
 					Selector: v1alpha1.PushSecretSelector{
-						Secret: v1alpha1.PushSecretSecret{
+						Secret: &v1alpha1.PushSecretSecret{
 							Name: SecretName,
 						},
 					},
@@ -395,7 +411,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 				},
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 					},
 				},
@@ -459,7 +475,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 				},
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 					},
 				},
@@ -515,7 +531,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 				},
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 					},
 				},
@@ -570,7 +586,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 				},
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 					},
 				},
@@ -716,7 +732,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 				},
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 					},
 				},
@@ -782,7 +798,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 				},
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 					},
 				},
@@ -861,6 +877,28 @@ var _ = Describe("PushSecret controller", func() {
 			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.
 	syncWithClusterStoreMatchingLabels := func(tc *testCase) {
 		fakeProvider.SetSecretFn = func() error {
@@ -884,7 +922,7 @@ var _ = Describe("PushSecret controller", func() {
 					},
 				},
 				Selector: v1alpha1.PushSecretSelector{
-					Secret: v1alpha1.PushSecretSecret{
+					Secret: &v1alpha1.PushSecretSecret{
 						Name: SecretName,
 					},
 				},
@@ -1069,6 +1107,7 @@ var _ = Describe("PushSecret controller", func() {
 		Entry("should sync to stores matching labels", syncMatchingLabels),
 		Entry("should sync with ClusterStore", syncWithClusterStore),
 		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 Key does not exist", failNoSecretKey),
 		Entry("should fail if SetSecret fails", setSecretFail),
@@ -1168,7 +1207,7 @@ var _ = Describe("PushSecret Controller Un/Managed Stores", func() {
 						},
 					},
 					Selector: v1alpha1.PushSecretSelector{
-						Secret: v1alpha1.PushSecretSecret{
+						Secret: &v1alpha1.PushSecretSecret{
 							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"
 	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/gomega"
@@ -72,6 +73,8 @@ var _ = BeforeSuite(func() {
 	Expect(err).NotTo(HaveOccurred())
 	err = esv1alpha1.AddToScheme(scheme.Scheme)
 	Expect(err).NotTo(HaveOccurred())
+	err = genv1alpha1.AddToScheme(scheme.Scheme)
+	Expect(err).NotTo(HaveOccurred())
 
 	k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
 		Scheme: scheme.Scheme,
@@ -90,7 +93,8 @@ var _ = BeforeSuite(func() {
 	err = (&Reconciler{
 		Client:          k8sClient,
 		Scheme:          k8sManager.GetScheme(),
-		Log:             ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
+		Log:             ctrl.Log.WithName("controllers").WithName("PushSecret"),
+		RestConfig:      cfg,
 		RequeueInterval: time.Second,
 	}).SetupWithManager(k8sManager)
 	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
+}