Browse Source

feat: implement a cluster-wide generator (#4140)

* feat: implement a cluster-wide generator

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* remove unneeded function

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* check diff run output

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* alternative implementation of the Generator approach using specs only

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* refactor the extracting code

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* slight modification to the naming of the spec from generatorSpec to simply generator

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* write a unit test for the generator and register it in the scheme

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* add documentation for the cluster generator

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

---------

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Gergely Brautigam 1 year ago
parent
commit
fb9526f38a

+ 1 - 1
apis/externalsecrets/v1beta1/externalsecret_types.go

@@ -393,7 +393,7 @@ type GeneratorRef struct {
 	// Specify the apiVersion of the generator resource
 	// +kubebuilder:default="generators.external-secrets.io/v1alpha1"
 	APIVersion string `json:"apiVersion,omitempty"`
-	// Specify the Kind of the resource, e.g. Password, ACRAccessToken etc.
+	// Specify the Kind of the resource, e.g. Password, ACRAccessToken, ClusterGenerator etc.
 	Kind string `json:"kind"`
 	// Specify the name of the generator resource
 	Name string `json:"name"`

+ 2 - 2
apis/generators/v1alpha1/generator_schema.go

@@ -59,7 +59,7 @@ func GetGeneratorByName(kind string) (Generator, bool) {
 	return f, ok
 }
 
-// GetGenerator returns a implementation from a generator
+// GetGenerator returns an implementation from a generator
 // defined as json.
 func GetGenerator(obj *apiextensions.JSON) (Generator, error) {
 	type unknownGenerator struct {
@@ -75,7 +75,7 @@ func GetGenerator(obj *apiextensions.JSON) (Generator, error) {
 	defer buildlock.RUnlock()
 	gen, ok := builder[res.Kind]
 	if !ok {
-		return nil, fmt.Errorf("failed to find registered generator for: %s", string(obj.Raw))
+		return nil, fmt.Errorf("failed to find registered generator for: %s with kind: %s", string(obj.Raw), res.Kind)
 	}
 	return gen, nil
 }

+ 57 - 0
apis/generators/v1alpha1/generator_types.go

@@ -14,8 +14,65 @@ limitations under the License.
 
 package v1alpha1
 
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// A couple of constants to define the generator's keys for accessing via Resource map values.
+const (
+	GeneratorGeneratorKey = "generator"
+	GeneratorKindKey      = "kind"
+	GeneratorSpecKey      = "spec"
+)
+
 type ControllerClassResource struct {
 	Spec struct {
 		ControllerClass string `json:"controller"`
 	} `json:"spec"`
 }
+
+type GeneratorSpec struct {
+	ACRAccessTokenSpec        *ACRAccessTokenSpec        `json:"acrAccessTokenSpec,omitempty"`
+	ECRAuthorizationTokenSpec *ECRAuthorizationTokenSpec `json:"ecrRAuthorizationTokenSpec,omitempty"`
+	FakeSpec                  *FakeSpec                  `json:"fakeSpec,omitempty"`
+	GCRAccessTokenSpec        *GCRAccessTokenSpec        `json:"gcrAccessTokenSpec,omitempty"`
+	GithubAccessTokenSpec     *GithubAccessTokenSpec     `json:"githubAccessTokenSpec,omitempty"`
+	PasswordSpec              *PasswordSpec              `json:"passwordSpec,omitempty"`
+	STSSessionTokenSpec       *STSSessionTokenSpec       `json:"stsSessionTokenSpec,omitempty"`
+	UUIDSpec                  *UUIDSpec                  `json:"uuidSpec,omitempty"`
+	VaultDynamicSecretSpec    *VaultDynamicSecretSpec    `json:"vaultDynamicSecretSpec,omitempty"`
+	WebhookSpec               *WebhookSpec               `json:"webhookSpec,omitempty"`
+}
+
+type ClusterGeneratorSpec struct {
+	Kind      string        `json:"kind"`
+	Generator GeneratorSpec `json:"generator"`
+}
+
+type ClusterGeneratorStatus struct{}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:storageversion
+
+// ClusterGenerator represents a cluster-wide generator which can be referenced as part of `generatorRef` fields.
+// +kubebuilder:object:root=true
+// +kubebuilder:storageversion
+// +kubebuilder:subresource:status
+// +kubebuilder:metadata:labels="external-secrets.io/component=controller"
+// +kubebuilder:resource:scope=Cluster,categories={external-secrets, external-secrets-generators},shortName=cg
+type ClusterGenerator struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   ClusterGeneratorSpec   `json:"spec,omitempty"`
+	Status ClusterGeneratorStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// ClusterGeneratorList contains a list of ClusterGenerator resources.
+type ClusterGeneratorList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []ClusterGenerator `json:"items"`
+}

+ 9 - 0
apis/generators/v1alpha1/register.go

@@ -116,6 +116,14 @@ var (
 	UUIDGroupVersionKind = SchemeGroupVersion.WithKind(UUIDKind)
 )
 
+// ClusterGenerator type metadata.
+var (
+	ClusterGeneratorKind             = reflect.TypeOf(ClusterGenerator{}).Name()
+	ClusterGeneratorGroupKind        = schema.GroupKind{Group: Group, Kind: ClusterGeneratorKind}.String()
+	ClusterGeneratorKindAPIVersion   = ClusterGeneratorKind + "." + SchemeGroupVersion.String()
+	ClusterGeneratorGroupVersionKind = SchemeGroupVersion.WithKind(ClusterGeneratorKind)
+)
+
 func init() {
 	SchemeBuilder.Register(&ECRAuthorizationToken{}, &ECRAuthorizationToken{})
 	SchemeBuilder.Register(&GCRAccessToken{}, &GCRAccessTokenList{})
@@ -125,4 +133,5 @@ func init() {
 	SchemeBuilder.Register(&VaultDynamicSecret{}, &VaultDynamicSecretList{})
 	SchemeBuilder.Register(&Password{}, &PasswordList{})
 	SchemeBuilder.Register(&Webhook{}, &WebhookList{})
+	SchemeBuilder.Register(&ClusterGenerator{}, &ClusterGeneratorList{})
 }

+ 155 - 0
apis/generators/v1alpha1/zz_generated.deepcopy.go

@@ -266,6 +266,96 @@ func (in *AzureACRWorkloadIdentityAuth) DeepCopy() *AzureACRWorkloadIdentityAuth
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClusterGenerator) DeepCopyInto(out *ClusterGenerator) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGenerator.
+func (in *ClusterGenerator) DeepCopy() *ClusterGenerator {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterGenerator)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ClusterGenerator) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClusterGeneratorList) DeepCopyInto(out *ClusterGeneratorList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]ClusterGenerator, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorList.
+func (in *ClusterGeneratorList) DeepCopy() *ClusterGeneratorList {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterGeneratorList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ClusterGeneratorList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClusterGeneratorSpec) DeepCopyInto(out *ClusterGeneratorSpec) {
+	*out = *in
+	in.Generator.DeepCopyInto(&out.Generator)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorSpec.
+func (in *ClusterGeneratorSpec) DeepCopy() *ClusterGeneratorSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterGeneratorSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClusterGeneratorStatus) DeepCopyInto(out *ClusterGeneratorStatus) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorStatus.
+func (in *ClusterGeneratorStatus) DeepCopy() *ClusterGeneratorStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterGeneratorStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ControllerClassResource) DeepCopyInto(out *ControllerClassResource) {
 	*out = *in
 	out.Spec = in.Spec
@@ -567,6 +657,71 @@ func (in *GCRAccessTokenSpec) DeepCopy() *GCRAccessTokenSpec {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GeneratorSpec) DeepCopyInto(out *GeneratorSpec) {
+	*out = *in
+	if in.ACRAccessTokenSpec != nil {
+		in, out := &in.ACRAccessTokenSpec, &out.ACRAccessTokenSpec
+		*out = new(ACRAccessTokenSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.ECRAuthorizationTokenSpec != nil {
+		in, out := &in.ECRAuthorizationTokenSpec, &out.ECRAuthorizationTokenSpec
+		*out = new(ECRAuthorizationTokenSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.FakeSpec != nil {
+		in, out := &in.FakeSpec, &out.FakeSpec
+		*out = new(FakeSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.GCRAccessTokenSpec != nil {
+		in, out := &in.GCRAccessTokenSpec, &out.GCRAccessTokenSpec
+		*out = new(GCRAccessTokenSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.GithubAccessTokenSpec != nil {
+		in, out := &in.GithubAccessTokenSpec, &out.GithubAccessTokenSpec
+		*out = new(GithubAccessTokenSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.PasswordSpec != nil {
+		in, out := &in.PasswordSpec, &out.PasswordSpec
+		*out = new(PasswordSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.STSSessionTokenSpec != nil {
+		in, out := &in.STSSessionTokenSpec, &out.STSSessionTokenSpec
+		*out = new(STSSessionTokenSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.UUIDSpec != nil {
+		in, out := &in.UUIDSpec, &out.UUIDSpec
+		*out = new(UUIDSpec)
+		**out = **in
+	}
+	if in.VaultDynamicSecretSpec != nil {
+		in, out := &in.VaultDynamicSecretSpec, &out.VaultDynamicSecretSpec
+		*out = new(VaultDynamicSecretSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.WebhookSpec != nil {
+		in, out := &in.WebhookSpec, &out.WebhookSpec
+		*out = new(WebhookSpec)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorSpec.
+func (in *GeneratorSpec) DeepCopy() *GeneratorSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(GeneratorSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GithubAccessToken) DeepCopyInto(out *GithubAccessToken) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta

+ 2 - 2
config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml

@@ -151,7 +151,7 @@ spec:
                                   type: string
                                 kind:
                                   description: Specify the Kind of the resource, e.g.
-                                    Password, ACRAccessToken etc.
+                                    Password, ACRAccessToken, ClusterGenerator etc.
                                   type: string
                                 name:
                                   description: Specify the name of the generator resource
@@ -327,7 +327,7 @@ spec:
                                   type: string
                                 kind:
                                   description: Specify the Kind of the resource, e.g.
-                                    Password, ACRAccessToken etc.
+                                    Password, ACRAccessToken, ClusterGenerator etc.
                                   type: string
                                 name:
                                   description: Specify the name of the generator resource

+ 2 - 2
config/crds/bases/external-secrets.io_externalsecrets.yaml

@@ -416,7 +416,7 @@ spec:
                               type: string
                             kind:
                               description: Specify the Kind of the resource, e.g.
-                                Password, ACRAccessToken etc.
+                                Password, ACRAccessToken, ClusterGenerator etc.
                               type: string
                             name:
                               description: Specify the name of the generator resource
@@ -591,7 +591,7 @@ spec:
                               type: string
                             kind:
                               description: Specify the Kind of the resource, e.g.
-                                Password, ACRAccessToken etc.
+                                Password, ACRAccessToken, ClusterGenerator etc.
                               type: string
                             name:
                               description: Specify the name of the generator resource

+ 1 - 1
config/crds/bases/external-secrets.io_pushsecrets.yaml

@@ -177,7 +177,7 @@ spec:
                         type: string
                       kind:
                         description: Specify the Kind of the resource, e.g. Password,
-                          ACRAccessToken etc.
+                          ACRAccessToken, ClusterGenerator etc.
                         type: string
                       name:
                         description: Specify the name of the generator resource

File diff suppressed because it is too large
+ 1408 - 0
config/crds/bases/generators.external-secrets.io_clustergenerators.yaml


+ 1 - 0
config/crds/bases/kustomization.yaml

@@ -8,6 +8,7 @@ resources:
   - external-secrets.io_pushsecrets.yaml
   - external-secrets.io_secretstores.yaml
   - generators.external-secrets.io_acraccesstokens.yaml
+  - generators.external-secrets.io_clustergenerators.yaml
   - generators.external-secrets.io_ecrauthorizationtokens.yaml
   - generators.external-secrets.io_fakes.yaml
   - generators.external-secrets.io_gcraccesstokens.yaml

+ 1 - 0
deploy/charts/external-secrets/README.md

@@ -89,6 +89,7 @@ The command removes all the Kubernetes components associated with the chart and
 | crds.annotations | object | `{}` |  |
 | crds.conversion.enabled | bool | `true` | If webhook is set to false this also needs to be set to false otherwise the kubeapi will be hammered because the conversion is looking for a webhook endpoint. |
 | crds.createClusterExternalSecret | bool | `true` | If true, create CRDs for Cluster External Secret. |
+| crds.createClusterGenerator | bool | `true` | If true, create CRDs for Cluster Generator. |
 | crds.createClusterSecretStore | bool | `true` | If true, create CRDs for Cluster Secret Store. |
 | crds.createPushSecret | bool | `true` | If true, create CRDs for Push Secret. |
 | createOperator | bool | `true` | Specifies whether an external secret operator deployment be created. |

+ 3 - 0
deploy/charts/external-secrets/templates/rbac.yaml

@@ -51,6 +51,7 @@ rules:
     - "generators.external-secrets.io"
     resources:
     - "acraccesstokens"
+    - "clustergenerators"
     - "ecrauthorizationtokens"
     - "fakes"
     - "gcraccesstokens"
@@ -145,6 +146,7 @@ rules:
     - "generators.external-secrets.io"
     resources:
     - "acraccesstokens"
+    - "clustergenerators"
     - "ecrauthorizationtokens"
     - "fakes"
     - "gcraccesstokens"
@@ -190,6 +192,7 @@ rules:
     - "generators.external-secrets.io"
     resources:
     - "acraccesstokens"
+    - "clustergenerators"
     - "ecrauthorizationtokens"
     - "fakes"
     - "gcraccesstokens"

+ 3 - 0
deploy/charts/external-secrets/values.schema.json

@@ -270,6 +270,9 @@
                 "createClusterExternalSecret": {
                     "type": "boolean"
                 },
+                "createClusterGenerator": {
+                    "type": "boolean"
+                },
                 "createClusterSecretStore": {
                     "type": "boolean"
                 },

+ 2 - 0
deploy/charts/external-secrets/values.yaml

@@ -39,6 +39,8 @@ crds:
   createClusterExternalSecret: true
   # -- If true, create CRDs for Cluster Secret Store.
   createClusterSecretStore: true
+  # -- If true, create CRDs for Cluster Generator.
+  createClusterGenerator: true
   # -- If true, create CRDs for Push Secret.
   createPushSecret: true
   annotations: {}

File diff suppressed because it is too large
+ 1346 - 5
deploy/crds/bundle.yaml


+ 20 - 0
docs/api/generator/cluster.md

@@ -0,0 +1,20 @@
+`ClusterGenerator` is a generator wrapper that is available to configure a generator
+cluster-wide. The purpose of this generator is that the user doesn't have to redefine
+the generator in every namespace. They could define it once in the cluster and then reference that
+in the consuming `ExternalSecret`.
+
+## Limitations
+
+With this, the generator will still create objects in the namespace in which the referencing ES lives.
+That has not changed as of now. It will change in future modifications.
+
+## Example Manifest
+
+```yaml
+{% include 'generator-cluster.yaml' %}
+```
+
+Example `ExternalSecret` that references the Cluster generator:
+```yaml
+{% include 'generator-cluster-example.yaml' %}
+```

+ 1 - 1
docs/api/spec.md

@@ -4569,7 +4569,7 @@ string
 </em>
 </td>
 <td>
-<p>Specify the Kind of the resource, e.g. Password, ACRAccessToken etc.</p>
+<p>Specify the Kind of the resource, e.g. Password, ACRAccessToken, ClusterGenerator etc.</p>
 </td>
 </tr>
 <tr>

+ 44 - 2
docs/guides/generator.md

@@ -1,4 +1,3 @@
-
 Generators allow you to generate values. They are used through a ExternalSecret `spec.DataFrom`. They are referenced from a custom resource using `sourceRef.generatorRef`.
 
 If the External Secret should be refreshed via `spec.refreshInterval` the generator produces a map of values with the `generator.spec` as input. The generator does not keep track of the produced values. Every invocation produces a new set of values.
@@ -24,4 +23,47 @@ spec:
         apiVersion: generators.external-secrets.io/v1alpha1
         kind: ECRAuthorizationToken
         name: "my-ecr"
-```
+```
+
+## Cluster Generate Resource
+
+It's possible to use a `Cluster` scoped generator. At the moment of this writing, this Generator
+will only help in locating the Generator cluster-wide. It doesn't mean that the generator can create resources in all
+namespaces. It will still only create a resource in the given namespace where the referencing `ExternalSecret` lives.
+
+To define a `ClusterGenerator` use the following config:
+
+```yaml
+apiVersion: generators.external-secrets.io/v1alpha1
+kind: ClusterGenerator
+metadata:
+  name: my-generator
+spec:
+  kind: Password
+  generator:
+    passwordSpec:
+      length: 42
+      digits: 5
+      symbols: 5
+      symbolCharacters: "-_$@"
+      noUpper: false
+      allowRepeat: true
+```
+
+All the generators are available as a ClusterGenerator spec. The `kind` field MUST match the kind of the Generator
+exactly. The following Spec fields are available:
+
+```go
+type GeneratorSpec struct {
+	ACRAccessTokenSpec        *ACRAccessTokenSpec        `json:"acrAccessTokenSpec,omitempty"`
+	ECRAuthorizationTokenSpec *ECRAuthorizationTokenSpec `json:"ecrRAuthorizationTokenSpec,omitempty"`
+	FakeSpec                  *FakeSpec                  `json:"fakeSpec,omitempty"`
+	GCRAccessTokenSpec        *GCRAccessTokenSpec        `json:"gcrAccessTokenSpec,omitempty"`
+	GithubAccessTokenSpec     *GithubAccessTokenSpec     `json:"githubAccessTokenSpec,omitempty"`
+	PasswordSpec              *PasswordSpec              `json:"passwordSpec,omitempty"`
+	STSSessionTokenSpec       *STSSessionTokenSpec       `json:"stsSessionTokenSpec,omitempty"`
+	UUIDSpec                  *UUIDSpec                  `json:"uuidSpec,omitempty"`
+	VaultDynamicSecretSpec    *VaultDynamicSecretSpec    `json:"vaultDynamicSecretSpec,omitempty"`
+	WebhookSpec               *WebhookSpec               `json:"webhookSpec,omitempty"`
+}
+```

+ 14 - 0
docs/snippets/generator-cluster-example.yaml

@@ -0,0 +1,14 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: "cluster-secret"
+spec:
+  refreshInterval: "1h"
+  target:
+    name: cluster-secret
+  dataFrom:
+  - sourceRef:
+      generatorRef:
+        apiVersion: generators.external-secrets.io/v1alpha1
+        kind: ClusterGenerator
+        name: "cluster-gen"

+ 24 - 0
docs/snippets/generator-cluster.yaml

@@ -0,0 +1,24 @@
+apiVersion: generators.external-secrets.io/v1alpha1
+kind: ClusterGenerator
+metadata:
+  name: cluster-gen
+spec:
+  kind: Password
+  generator:
+#    Further specs are available:
+#    acrAccessTokenSpec:
+#    ecrRAuthorizationTokenSpec:
+#    fakeSpec:
+#    gcrAccessTokenSpec:
+#    githubAccessTokenSpec:
+#    stsSessionTokenSpec:
+#    uuidSpec:
+#    vaultDynamicSecretSpec:
+#    webhookSpec:
+    passwordSpec:
+      length: 42
+      digits: 5
+      symbols: 5
+      symbolCharacters: "-_$@"
+      noUpper: false
+      allowRepeat: true

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

@@ -69,6 +69,7 @@ nav:
       - Azure Container Registry: api/generator/acr.md
       - AWS Elastic Container Registry: api/generator/ecr.md
       - AWS STS Session Token: api/generator/sts.md
+      - Cluster Generator: api/generator/cluster.md
       - Google Container Registry: api/generator/gcr.md
       - Vault Dynamic Secret: api/generator/vault.md
       - Password: api/generator/password.md

+ 2 - 1
pkg/controllers/externalsecret/externalsecret_controller_secret.go

@@ -25,7 +25,6 @@ import (
 
 	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"
@@ -116,6 +115,8 @@ func (r *Reconciler) handleGenerateSecrets(ctx context.Context, namespace string
 	if err != nil {
 		return nil, fmt.Errorf("unable to resolve generator: %w", err)
 	}
+	// We still pass the namespace to the generate function because it needs to create
+	// namespace based objects.
 	secretMap, err := gen.Generate(ctx, obj, r.Client, namespace)
 	if err != nil {
 		return nil, fmt.Errorf(errGenerate, i, err)

+ 40 - 0
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -650,6 +650,45 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 			Expect(string(secret.Data[secretKey])).To(Equal(secretVal))
 		}
 	}
+	syncWithClusterGeneratorRef := func(tc *testCase) {
+		const secretKey = "somekey2"
+		const secretVal = "someValue2"
+		Expect(k8sClient.Create(context.Background(), &genv1alpha1.ClusterGenerator{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "mytestfake",
+			},
+			Spec: genv1alpha1.ClusterGeneratorSpec{
+				Kind: "Fake",
+				Generator: genv1alpha1.GeneratorSpec{
+					FakeSpec: &genv1alpha1.FakeSpec{
+						Data: map[string]string{
+							secretKey: secretVal,
+						},
+					},
+				},
+			},
+		})).To(Succeed())
+
+		// reset secretStoreRef
+		tc.externalSecret.Spec.SecretStoreRef = esv1beta1.SecretStoreRef{}
+		tc.externalSecret.Spec.Data = nil
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				SourceRef: &esv1beta1.StoreGeneratorSourceRef{
+					GeneratorRef: &esv1beta1.GeneratorRef{
+						APIVersion: genv1alpha1.Group + "/" + genv1alpha1.Version,
+						Kind:       "ClusterGenerator",
+						Name:       "mytestfake",
+					},
+				},
+			},
+		}
+
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// check values
+			Expect(string(secret.Data[secretKey])).To(Equal(secretVal))
+		}
+	}
 
 	deleteOrphanedSecrets := func(tc *testCase) {
 		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
@@ -2280,6 +2319,7 @@ var _ = Describe("ExternalSecret controller", Serial, func() {
 		Entry("should not resolve conflicts with creationPolicy=Merge", mergeWithConflict),
 		Entry("should not update unchanged secret using creationPolicy=Merge", mergeWithSecretNoChange),
 		Entry("should not delete pre-existing secret with creationPolicy=Orphan", createSecretPolicyOrphan),
+		Entry("should sync cluster generator ref", syncWithClusterGeneratorRef),
 		Entry("should sync with generatorRef", syncWithGeneratorRef),
 		Entry("should not process generatorRef with mismatching controller field", ignoreMismatchControllerForGeneratorRef),
 		Entry("should sync with multiple secret stores via sourceRef", syncWithMultipleSecretStores),

+ 81 - 3
pkg/utils/resolvers/generator.go

@@ -11,6 +11,7 @@ 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 (
@@ -18,8 +19,10 @@ import (
 	"fmt"
 
 	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/util/json"
 	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/rest"
@@ -70,15 +73,90 @@ func getGeneratorDefinition(ctx context.Context, restConfig *rest.Config, namesp
 	if err != nil {
 		return nil, err
 	}
-	res, err := d.Resource(mapping.Resource).
-		Namespace(namespace).
-		Get(ctx, generatorRef.Name, metav1.GetOptions{})
+
+	if generatorRef.Kind == "ClusterGenerator" {
+		return extractGeneratorFromClusterGenerator(ctx, d, mapping, generatorRef)
+	}
+
+	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 extractGeneratorFromClusterGenerator(
+	ctx context.Context,
+	d *dynamic.DynamicClient,
+	mapping *meta.RESTMapping,
+	generatorRef *esv1beta1.GeneratorRef,
+) (*apiextensions.JSON, error) {
+	res, err := d.Resource(mapping.Resource).Get(ctx, generatorRef.Name, metav1.GetOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	spec, err := extractValue[map[string]any](res.Object, genv1alpha1.GeneratorSpecKey)
+	if err != nil {
+		return nil, err
+	}
+
+	generator, err := extractValue[map[string]any](spec, genv1alpha1.GeneratorGeneratorKey)
+	if err != nil {
+		return nil, err
+	}
+
+	kind, err := extractValue[string](spec, genv1alpha1.GeneratorKindKey)
+	if err != nil {
+		return nil, err
+	}
+
+	// find the first value and that's what we are going to take
+	// this will be the generator that has been set by the user
+	var result []byte
+	for _, v := range generator {
+		vMap, ok := v.(map[string]interface{})
+		if !ok {
+			return nil, fmt.Errorf("kind was not of object type for cluster generator %T", v)
+		}
+
+		// Construct our generator object so it can be later unmarshalled into a valid Generator Spec.
+		object := map[string]interface{}{}
+		object["kind"] = kind
+		object["spec"] = vMap
+		result, err = json.Marshal(object)
+		if err != nil {
+			return nil, err
+		}
+
+		return &apiextensions.JSON{Raw: result}, nil
+	}
+
+	return nil, fmt.Errorf("no defined generators found for cluster generator spec: %v", spec)
+}
+
+// extractValue fetches a specific key value that we are looking for in a map.
+func extractValue[T any](m any, k string) (T, error) {
+	var result T
+	v, ok := m.(map[string]any)
+	if !ok {
+		return result, fmt.Errorf("value was not of type map[string]any but: %T", m)
+	}
+
+	vv, ok := v[k]
+	if !ok {
+		return result, fmt.Errorf("key %s was not found in map", k)
+	}
+
+	vvv, ok := vv.(T)
+	if !ok {
+		return result, fmt.Errorf("value was not of type T but: %T", vvv)
+	}
+
+	return vvv, nil
+}