Просмотр исходного кода

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 год назад
Родитель
Сommit
fb9526f38a

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

@@ -393,7 +393,7 @@ type GeneratorRef struct {
 	// Specify the apiVersion of the generator resource
 	// Specify the apiVersion of the generator resource
 	// +kubebuilder:default="generators.external-secrets.io/v1alpha1"
 	// +kubebuilder:default="generators.external-secrets.io/v1alpha1"
 	APIVersion string `json:"apiVersion,omitempty"`
 	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"`
 	Kind string `json:"kind"`
 	// Specify the name of the generator resource
 	// Specify the name of the generator resource
 	Name string `json:"name"`
 	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
 	return f, ok
 }
 }
 
 
-// GetGenerator returns a implementation from a generator
+// GetGenerator returns an implementation from a generator
 // defined as json.
 // defined as json.
 func GetGenerator(obj *apiextensions.JSON) (Generator, error) {
 func GetGenerator(obj *apiextensions.JSON) (Generator, error) {
 	type unknownGenerator struct {
 	type unknownGenerator struct {
@@ -75,7 +75,7 @@ func GetGenerator(obj *apiextensions.JSON) (Generator, error) {
 	defer buildlock.RUnlock()
 	defer buildlock.RUnlock()
 	gen, ok := builder[res.Kind]
 	gen, ok := builder[res.Kind]
 	if !ok {
 	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
 	return gen, nil
 }
 }

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

@@ -14,8 +14,65 @@ limitations under the License.
 
 
 package v1alpha1
 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 {
 type ControllerClassResource struct {
 	Spec struct {
 	Spec struct {
 		ControllerClass string `json:"controller"`
 		ControllerClass string `json:"controller"`
 	} `json:"spec"`
 	} `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)
 	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() {
 func init() {
 	SchemeBuilder.Register(&ECRAuthorizationToken{}, &ECRAuthorizationToken{})
 	SchemeBuilder.Register(&ECRAuthorizationToken{}, &ECRAuthorizationToken{})
 	SchemeBuilder.Register(&GCRAccessToken{}, &GCRAccessTokenList{})
 	SchemeBuilder.Register(&GCRAccessToken{}, &GCRAccessTokenList{})
@@ -125,4 +133,5 @@ func init() {
 	SchemeBuilder.Register(&VaultDynamicSecret{}, &VaultDynamicSecretList{})
 	SchemeBuilder.Register(&VaultDynamicSecret{}, &VaultDynamicSecretList{})
 	SchemeBuilder.Register(&Password{}, &PasswordList{})
 	SchemeBuilder.Register(&Password{}, &PasswordList{})
 	SchemeBuilder.Register(&Webhook{}, &WebhookList{})
 	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.
 // 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) {
 func (in *ControllerClassResource) DeepCopyInto(out *ControllerClassResource) {
 	*out = *in
 	*out = *in
 	out.Spec = in.Spec
 	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.
 // 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) {
 func (in *GithubAccessToken) DeepCopyInto(out *GithubAccessToken) {
 	*out = *in
 	*out = *in
 	out.TypeMeta = in.TypeMeta
 	out.TypeMeta = in.TypeMeta

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

@@ -151,7 +151,7 @@ spec:
                                   type: string
                                   type: string
                                 kind:
                                 kind:
                                   description: Specify the Kind of the resource, e.g.
                                   description: Specify the Kind of the resource, e.g.
-                                    Password, ACRAccessToken etc.
+                                    Password, ACRAccessToken, ClusterGenerator etc.
                                   type: string
                                   type: string
                                 name:
                                 name:
                                   description: Specify the name of the generator resource
                                   description: Specify the name of the generator resource
@@ -327,7 +327,7 @@ spec:
                                   type: string
                                   type: string
                                 kind:
                                 kind:
                                   description: Specify the Kind of the resource, e.g.
                                   description: Specify the Kind of the resource, e.g.
-                                    Password, ACRAccessToken etc.
+                                    Password, ACRAccessToken, ClusterGenerator etc.
                                   type: string
                                   type: string
                                 name:
                                 name:
                                   description: Specify the name of the generator resource
                                   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
                               type: string
                             kind:
                             kind:
                               description: Specify the Kind of the resource, e.g.
                               description: Specify the Kind of the resource, e.g.
-                                Password, ACRAccessToken etc.
+                                Password, ACRAccessToken, ClusterGenerator etc.
                               type: string
                               type: string
                             name:
                             name:
                               description: Specify the name of the generator resource
                               description: Specify the name of the generator resource
@@ -591,7 +591,7 @@ spec:
                               type: string
                               type: string
                             kind:
                             kind:
                               description: Specify the Kind of the resource, e.g.
                               description: Specify the Kind of the resource, e.g.
-                                Password, ACRAccessToken etc.
+                                Password, ACRAccessToken, ClusterGenerator etc.
                               type: string
                               type: string
                             name:
                             name:
                               description: Specify the name of the generator resource
                               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
                         type: string
                       kind:
                       kind:
                         description: Specify the Kind of the resource, e.g. Password,
                         description: Specify the Kind of the resource, e.g. Password,
-                          ACRAccessToken etc.
+                          ACRAccessToken, ClusterGenerator etc.
                         type: string
                         type: string
                       name:
                       name:
                         description: Specify the name of the generator resource
                         description: Specify the name of the generator resource

Разница между файлами не показана из-за своего большого размера
+ 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_pushsecrets.yaml
   - external-secrets.io_secretstores.yaml
   - external-secrets.io_secretstores.yaml
   - generators.external-secrets.io_acraccesstokens.yaml
   - generators.external-secrets.io_acraccesstokens.yaml
+  - generators.external-secrets.io_clustergenerators.yaml
   - generators.external-secrets.io_ecrauthorizationtokens.yaml
   - generators.external-secrets.io_ecrauthorizationtokens.yaml
   - generators.external-secrets.io_fakes.yaml
   - generators.external-secrets.io_fakes.yaml
   - generators.external-secrets.io_gcraccesstokens.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.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.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.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.createClusterSecretStore | bool | `true` | If true, create CRDs for Cluster Secret Store. |
 | crds.createPushSecret | bool | `true` | If true, create CRDs for Push Secret. |
 | crds.createPushSecret | bool | `true` | If true, create CRDs for Push Secret. |
 | createOperator | bool | `true` | Specifies whether an external secret operator deployment be created. |
 | 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"
     - "generators.external-secrets.io"
     resources:
     resources:
     - "acraccesstokens"
     - "acraccesstokens"
+    - "clustergenerators"
     - "ecrauthorizationtokens"
     - "ecrauthorizationtokens"
     - "fakes"
     - "fakes"
     - "gcraccesstokens"
     - "gcraccesstokens"
@@ -145,6 +146,7 @@ rules:
     - "generators.external-secrets.io"
     - "generators.external-secrets.io"
     resources:
     resources:
     - "acraccesstokens"
     - "acraccesstokens"
+    - "clustergenerators"
     - "ecrauthorizationtokens"
     - "ecrauthorizationtokens"
     - "fakes"
     - "fakes"
     - "gcraccesstokens"
     - "gcraccesstokens"
@@ -190,6 +192,7 @@ rules:
     - "generators.external-secrets.io"
     - "generators.external-secrets.io"
     resources:
     resources:
     - "acraccesstokens"
     - "acraccesstokens"
+    - "clustergenerators"
     - "ecrauthorizationtokens"
     - "ecrauthorizationtokens"
     - "fakes"
     - "fakes"
     - "gcraccesstokens"
     - "gcraccesstokens"

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

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

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

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

Разница между файлами не показана из-за своего большого размера
+ 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>
 </em>
 </td>
 </td>
 <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>
 </td>
 </tr>
 </tr>
 <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`.
 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.
 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
         apiVersion: generators.external-secrets.io/v1alpha1
         kind: ECRAuthorizationToken
         kind: ECRAuthorizationToken
         name: "my-ecr"
         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
       - Azure Container Registry: api/generator/acr.md
       - AWS Elastic Container Registry: api/generator/ecr.md
       - AWS Elastic Container Registry: api/generator/ecr.md
       - AWS STS Session Token: api/generator/sts.md
       - AWS STS Session Token: api/generator/sts.md
+      - Cluster Generator: api/generator/cluster.md
       - Google Container Registry: api/generator/gcr.md
       - Google Container Registry: api/generator/gcr.md
       - Vault Dynamic Secret: api/generator/vault.md
       - Vault Dynamic Secret: api/generator/vault.md
       - Password: api/generator/password.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"
 	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.
 	"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"
 	"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 {
 	if err != nil {
 		return nil, fmt.Errorf("unable to resolve generator: %w", err)
 		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)
 	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)

+ 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))
 			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) {
 	deleteOrphanedSecrets := func(tc *testCase) {
 		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
 		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 resolve conflicts with creationPolicy=Merge", mergeWithConflict),
 		Entry("should not update unchanged secret using creationPolicy=Merge", mergeWithSecretNoChange),
 		Entry("should not update unchanged secret using creationPolicy=Merge", mergeWithSecretNoChange),
 		Entry("should not delete pre-existing secret with creationPolicy=Orphan", createSecretPolicyOrphan),
 		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 sync with generatorRef", syncWithGeneratorRef),
 		Entry("should not process generatorRef with mismatching controller field", ignoreMismatchControllerForGeneratorRef),
 		Entry("should not process generatorRef with mismatching controller field", ignoreMismatchControllerForGeneratorRef),
 		Entry("should sync with multiple secret stores via sourceRef", syncWithMultipleSecretStores),
 		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
 See the License for the specific language governing permissions and
 limitations under the License.
 limitations under the License.
 */
 */
+
 package resolvers
 package resolvers
 
 
 import (
 import (
@@ -18,8 +19,10 @@ import (
 	"fmt"
 	"fmt"
 
 
 	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/util/json"
 	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/rest"
@@ -70,15 +73,90 @@ func getGeneratorDefinition(ctx context.Context, restConfig *rest.Config, namesp
 	if err != nil {
 	if err != nil {
 		return nil, err
 		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 {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
+
 	jsonRes, err := res.MarshalJSON()
 	jsonRes, err := res.MarshalJSON()
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 	return &apiextensions.JSON{Raw: jsonRes}, nil
 	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
+}