Browse Source

Added namespace condition to ClusterSecretStore (#1635)

* Added namespace condition to ClusterSecretStore

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Added the new conditions field to the docs

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Added tests to ClusterSecretStore namespace conditions

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Added some comments to explain tests better

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Fixed a testcase

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Increased golangci timeout to 10m

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Fixed test to use fakeProvider correctly

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Removed hardcoded timeout from make lint

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Improved error message on non matching namespace

Co-authored-by: Moritz Johner <moolen@users.noreply.github.com>
Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Modified testCase to use GenericStore interface

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Attempt at generalizing the testcase and reducing code duplication

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* Reduced some diff

Signed-off-by: Yannay Hammer <yannayha@gmail.com>

* fix: tidy e2e mod

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

Signed-off-by: Yannay Hammer <yannayha@gmail.com>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Docs <docs@external-secrets.io>
Co-authored-by: Moritz Johner <moolen@users.noreply.github.com>
Co-authored-by: Moritz Johner <beller.moritz@googlemail.com>
Yannay Hammer 3 years ago
parent
commit
14f5ddf198

+ 2 - 3
.golangci.yaml

@@ -1,5 +1,5 @@
 run:
-  timeout: 5m
+  timeout: 10m
 
 linters-settings:
   gci:
@@ -79,7 +79,6 @@ linters:
 service:
   golangci-lint-version: 1.33.x
 
-
 issues:
   # Excluding configuration per-path and per-linter
   exclude-rules:
@@ -104,7 +103,7 @@ issues:
     # positives in Kubernetes land where a Secret is an object type.
     - text: "G101:"
       linters:
-      - gosec
+        - gosec
 
     # The header check doesn't correctly parse the header as a code comment and is
     # triggered by the perceived diff. The header check still correctly detects missing

+ 3 - 1
Makefile

@@ -75,6 +75,7 @@ FAIL	= (echo ${TIME} ${RED}[FAIL]${CNone} && false)
 
 reviewable: generate helm.generate helm.docs lint ## Ensure a PR is ready for review.
 	@go mod tidy
+	@cd e2e/ && go mod tidy
 
 golicenses.check: ## Check install of go-licenses
 	@if ! go-licenses >> /dev/null 2>&1; then \
@@ -138,7 +139,7 @@ lint.install: ## Install golangci-lint to the go bin dir
 	fi
 
 lint: lint.check ## Run golangci-lint
-	@if ! golangci-lint run --timeout 5m; then \
+	@if ! golangci-lint run; then \
 		echo -e "\033[0;33mgolangci-lint failed: some checks can be fixed with \`\033[0;32mmake fmt\033[0m\033[0;33m\`\033[0m"; \
 		exit 1; \
 	fi
@@ -146,6 +147,7 @@ lint: lint.check ## Run golangci-lint
 
 fmt: lint.check ## Ensure consistent code style
 	@go mod tidy
+	@cd e2e/ && go mod tidy
 	@go fmt ./...
 	@golangci-lint run --fix > /dev/null 2>&1 || true
 	@$(OK) Ensured consistent code style

+ 9 - 0
apis/externalsecrets/v1beta1/generic_store.go

@@ -34,6 +34,7 @@ type GenericStore interface {
 
 	GetObjectMeta() *metav1.ObjectMeta
 	GetTypeMeta() *metav1.TypeMeta
+	GetKind() string
 
 	GetSpec() *SecretStoreSpec
 	GetNamespacedName() string
@@ -70,6 +71,10 @@ func (c *SecretStore) GetNamespacedName() string {
 	return fmt.Sprintf("%s/%s", c.Namespace, c.Name)
 }
 
+func (c *SecretStore) GetKind() string {
+	return SecretStoreKind
+}
+
 func (c *SecretStore) Copy() GenericStore {
 	return c.DeepCopy()
 }
@@ -105,3 +110,7 @@ func (c *ClusterSecretStore) SetStatus(status SecretStoreStatus) {
 func (c *ClusterSecretStore) GetNamespacedName() string {
 	return fmt.Sprintf("%s/%s", c.Namespace, c.Name)
 }
+
+func (c *ClusterSecretStore) GetKind() string {
+	return ClusterSecretStoreKind
+}

+ 15 - 0
apis/externalsecrets/v1beta1/secretstore_types.go

@@ -36,6 +36,21 @@ type SecretStoreSpec struct {
 	// Used to configure store refresh interval in seconds. Empty or 0 will default to the controller config.
 	// +optional
 	RefreshInterval int `json:"refreshInterval"`
+
+	// Used to constraint a ClusterSecretStore to specific namespaces. Relevant only to ClusterSecretStore
+	// +optional
+	Conditions []ClusterSecretStoreCondition `json:"conditions,omitempty"`
+}
+
+// ClusterSecretStoreCondition describes a condition by which to choose namespaces to process ExternalSecrets in
+// for a ClusterSecretStore instance.
+type ClusterSecretStoreCondition struct {
+	// Choose namespace using a labelSelector
+	// +optional
+	NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
+
+	// Choose namespaces by name
+	Namespaces []string `json:"namespaces,omitempty"`
 }
 
 // SecretStoreProvider contains the provider-specific configuration.

+ 32 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -521,6 +521,31 @@ func (in *ClusterSecretStore) DeepCopyObject() runtime.Object {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClusterSecretStoreCondition) DeepCopyInto(out *ClusterSecretStoreCondition) {
+	*out = *in
+	if in.NamespaceSelector != nil {
+		in, out := &in.NamespaceSelector, &out.NamespaceSelector
+		*out = new(v1.LabelSelector)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Namespaces != nil {
+		in, out := &in.Namespaces, &out.Namespaces
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSecretStoreCondition.
+func (in *ClusterSecretStoreCondition) DeepCopy() *ClusterSecretStoreCondition {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterSecretStoreCondition)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterSecretStoreList) DeepCopyInto(out *ClusterSecretStoreList) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
@@ -1639,6 +1664,13 @@ func (in *SecretStoreSpec) DeepCopyInto(out *SecretStoreSpec) {
 		*out = new(SecretStoreRetrySettings)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]ClusterSecretStoreCondition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreSpec.

+ 60 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -1536,6 +1536,66 @@ spec:
           spec:
             description: SecretStoreSpec defines the desired state of SecretStore.
             properties:
+              conditions:
+                description: Used to constraint a ClusterSecretStore to specific namespaces.
+                  Relevant only to ClusterSecretStore
+                items:
+                  description: ClusterSecretStoreCondition describes a condition by
+                    which to choose namespaces to process ExternalSecrets in for a
+                    ClusterSecretStore instance.
+                  properties:
+                    namespaceSelector:
+                      description: Choose namespace using a labelSelector
+                      properties:
+                        matchExpressions:
+                          description: matchExpressions is a list of label selector
+                            requirements. The requirements are ANDed.
+                          items:
+                            description: A label selector requirement is a selector
+                              that contains values, a key, and an operator that relates
+                              the key and values.
+                            properties:
+                              key:
+                                description: key is the label key that the selector
+                                  applies to.
+                                type: string
+                              operator:
+                                description: operator represents a key's relationship
+                                  to a set of values. Valid operators are In, NotIn,
+                                  Exists and DoesNotExist.
+                                type: string
+                              values:
+                                description: values is an array of string values.
+                                  If the operator is In or NotIn, the values array
+                                  must be non-empty. If the operator is Exists or
+                                  DoesNotExist, the values array must be empty. This
+                                  array is replaced during a strategic merge patch.
+                                items:
+                                  type: string
+                                type: array
+                            required:
+                            - key
+                            - operator
+                            type: object
+                          type: array
+                        matchLabels:
+                          additionalProperties:
+                            type: string
+                          description: matchLabels is a map of {key,value} pairs.
+                            A single {key,value} in the matchLabels map is equivalent
+                            to an element of matchExpressions, whose key field is
+                            "key", the operator is "In", and the values array contains
+                            only "value". The requirements are ANDed.
+                          type: object
+                      type: object
+                      x-kubernetes-map-type: atomic
+                    namespaces:
+                      description: Choose namespaces by name
+                      items:
+                        type: string
+                      type: array
+                  type: object
+                type: array
               controller:
                 description: 'Used to select the correct KES controller (think: ingress.ingressClassName)
                   The KES controller is instantiated with a specific controller name

+ 60 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -1536,6 +1536,66 @@ spec:
           spec:
             description: SecretStoreSpec defines the desired state of SecretStore.
             properties:
+              conditions:
+                description: Used to constraint a ClusterSecretStore to specific namespaces.
+                  Relevant only to ClusterSecretStore
+                items:
+                  description: ClusterSecretStoreCondition describes a condition by
+                    which to choose namespaces to process ExternalSecrets in for a
+                    ClusterSecretStore instance.
+                  properties:
+                    namespaceSelector:
+                      description: Choose namespace using a labelSelector
+                      properties:
+                        matchExpressions:
+                          description: matchExpressions is a list of label selector
+                            requirements. The requirements are ANDed.
+                          items:
+                            description: A label selector requirement is a selector
+                              that contains values, a key, and an operator that relates
+                              the key and values.
+                            properties:
+                              key:
+                                description: key is the label key that the selector
+                                  applies to.
+                                type: string
+                              operator:
+                                description: operator represents a key's relationship
+                                  to a set of values. Valid operators are In, NotIn,
+                                  Exists and DoesNotExist.
+                                type: string
+                              values:
+                                description: values is an array of string values.
+                                  If the operator is In or NotIn, the values array
+                                  must be non-empty. If the operator is Exists or
+                                  DoesNotExist, the values array must be empty. This
+                                  array is replaced during a strategic merge patch.
+                                items:
+                                  type: string
+                                type: array
+                            required:
+                            - key
+                            - operator
+                            type: object
+                          type: array
+                        matchLabels:
+                          additionalProperties:
+                            type: string
+                          description: matchLabels is a map of {key,value} pairs.
+                            A single {key,value} in the matchLabels map is equivalent
+                            to an element of matchExpressions, whose key field is
+                            "key", the operator is "In", and the values array contains
+                            only "value". The requirements are ANDed.
+                          type: object
+                      type: object
+                      x-kubernetes-map-type: atomic
+                    namespaces:
+                      description: Choose namespaces by name
+                      items:
+                        type: string
+                      type: array
+                  type: object
+                type: array
               controller:
                 description: 'Used to select the correct KES controller (think: ingress.ingressClassName)
                   The KES controller is instantiated with a specific controller name

+ 86 - 0
deploy/crds/bundle.yaml

@@ -1497,6 +1497,49 @@ spec:
             spec:
               description: SecretStoreSpec defines the desired state of SecretStore.
               properties:
+                conditions:
+                  description: Used to constraint a ClusterSecretStore to specific namespaces. Relevant only to ClusterSecretStore
+                  items:
+                    description: ClusterSecretStoreCondition describes a condition by which to choose namespaces to process ExternalSecrets in for a ClusterSecretStore instance.
+                    properties:
+                      namespaceSelector:
+                        description: Choose namespace using a labelSelector
+                        properties:
+                          matchExpressions:
+                            description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
+                            items:
+                              description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
+                              properties:
+                                key:
+                                  description: key is the label key that the selector applies to.
+                                  type: string
+                                operator:
+                                  description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
+                                  type: string
+                                values:
+                                  description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
+                                  items:
+                                    type: string
+                                  type: array
+                              required:
+                                - key
+                                - operator
+                              type: object
+                            type: array
+                          matchLabels:
+                            additionalProperties:
+                              type: string
+                            description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
+                            type: object
+                        type: object
+                        x-kubernetes-map-type: atomic
+                      namespaces:
+                        description: Choose namespaces by name
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                  type: array
                 controller:
                   description: 'Used to select the correct KES controller (think: ingress.ingressClassName) The KES controller is instantiated with a specific controller name and filters ES based on this property'
                   type: string
@@ -4425,6 +4468,49 @@ spec:
             spec:
               description: SecretStoreSpec defines the desired state of SecretStore.
               properties:
+                conditions:
+                  description: Used to constraint a ClusterSecretStore to specific namespaces. Relevant only to ClusterSecretStore
+                  items:
+                    description: ClusterSecretStoreCondition describes a condition by which to choose namespaces to process ExternalSecrets in for a ClusterSecretStore instance.
+                    properties:
+                      namespaceSelector:
+                        description: Choose namespace using a labelSelector
+                        properties:
+                          matchExpressions:
+                            description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
+                            items:
+                              description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
+                              properties:
+                                key:
+                                  description: key is the label key that the selector applies to.
+                                  type: string
+                                operator:
+                                  description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
+                                  type: string
+                                values:
+                                  description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
+                                  items:
+                                    type: string
+                                  type: array
+                              required:
+                                - key
+                                - operator
+                              type: object
+                            type: array
+                          matchLabels:
+                            additionalProperties:
+                              type: string
+                            description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
+                            type: object
+                        type: object
+                        x-kubernetes-map-type: atomic
+                      namespaces:
+                        description: Choose namespaces by name
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                  type: array
                 controller:
                   description: 'Used to select the correct KES controller (think: ingress.ingressClassName) The KES controller is instantiated with a specific controller name and filters ES based on this property'
                   type: string

+ 28 - 17
docs/snippets/full-cluster-secret-store.yaml

@@ -3,7 +3,6 @@ kind: ClusterSecretStore
 metadata:
   name: example
 spec:
-
   # Used to select the correct ESO controller (think: ingress.ingressClassName)
   # The ESO controller is instantiated with a specific controller name
   # and filters ES based on this property
@@ -13,7 +12,6 @@ spec:
   # provider field contains the configuration to access the provider
   # which contains the secret exactly one provider must be configured.
   provider:
-
     # (1): AWS Secrets Manager
     # aws configures this store to sync secrets using AWS Secret Manager provider
     aws:
@@ -104,27 +102,40 @@ spec:
     # (3): Kubernetes provider
     kubernetes:
       server:
-        url:  "https://myapiserver.tld"
-        caProvider: 
-            type: Secret
-            name : my-cluster-secrets
-            namespace: example
-            key: ca.crt
+        url: "https://myapiserver.tld"
+        caProvider:
+          type: Secret
+          name: my-cluster-secrets
+          namespace: example
+          key: ca.crt
       auth:
         serviceAccount:
           name: "example-sa"
           namespace: "example"
     # (TODO): add more provider examples here
 
+  # Conditions about namespaces in which the ClusterSecretStore is usable for ExternalSecrets
+  conditions:
+    # Options are namespaceSelector, or namespaces
+    - namespaceSelector:
+        matchLabels:
+          my.namespace.io/some-label: "value" # Only namespaces with that label will work
+
+    - namespaces:
+        - "namespace-a"
+        - "namespace-b"
+
+    # conditions needs only one of the conditions to meet for the CSS to be usable in the namespace.
+
 status:
   # Standard condition schema
   conditions:
-  # SecretStore ready condition indicates the given store is in ready
-  # state and able to referenced by ExternalSecrets
-  # If the `status` of this condition is `False`, ExternalSecret controllers
-  # should prevent attempts to fetch secrets
-  - type: Ready
-    status: "False"
-    reason: "ConfigError"
-    message: "SecretStore validation failed"
-    lastTransitionTime: "2019-08-12T12:33:02Z"
+    # SecretStore ready condition indicates the given store is in ready
+    # state and able to referenced by ExternalSecrets
+    # If the `status` of this condition is `False`, ExternalSecret controllers
+    # should prevent attempts to fetch secrets
+    - type: Ready
+      status: "False"
+      reason: "ConfigError"
+      message: "SecretStore validation failed"
+      lastTransitionTime: "2019-08-12T12:33:02Z"

+ 1 - 1
e2e/go.mod

@@ -214,7 +214,7 @@ require (
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
 	golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
-	golang.org/x/exp v0.0.0-20210901193431-a062eea981d2 // indirect
+	golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 // indirect
 	golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect
 	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
 	golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect

+ 2 - 1
e2e/go.sum

@@ -1147,8 +1147,9 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/exp v0.0.0-20210220032938-85be41e4509f/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4=
-golang.org/x/exp v0.0.0-20210901193431-a062eea981d2 h1:Or4Ra3AAlhUlNn8WmIzw2Yq2vUHSkrP6E2e/FIESpF8=
 golang.org/x/exp v0.0.0-20210901193431-a062eea981d2/go.mod h1:a3o/VtDNHN+dCVLEpzjjUHOzR+Ln3DHX056ZPzoZGGA=
+golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg=
+golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=

+ 1 - 0
go.mod

@@ -208,6 +208,7 @@ require (
 	go.opencensus.io v0.23.0 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
 	go.uber.org/multierr v1.8.0 // indirect
+	golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
 	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
 	golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect
 	golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect

+ 2 - 0
go.sum

@@ -926,6 +926,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg=
+golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=

+ 56 - 0
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -24,6 +24,7 @@ import (
 
 	"github.com/go-logr/logr"
 	"github.com/prometheus/client_golang/prometheus"
+	"golang.org/x/exp/slices"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/equality"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -62,6 +63,7 @@ const (
 	errStoreProvider         = "could not get store provider"
 	errStoreClient           = "could not get provider client"
 	errGetExistingSecret     = "could not get existing secret: %w"
+	errClusterStoreMismatch  = "using cluster store %q is not allowed from namespace %q: denied by spec.condition"
 	errCloseStoreClient      = "could not close provider client"
 	errSetCtrlReference      = "could not set ExternalSecret controller reference: %w"
 	errFetchTplFrom          = "error fetching templateFrom data: %w"
@@ -156,6 +158,21 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 		return ctrl.Result{}, nil
 	}
 
+	// when using ClusterSecretStore, validate the ClusterSecretStore namespace conditions
+	shouldProcess, err := r.ShouldProcessSecret(ctx, store, externalSecret.Namespace)
+	if err != nil || !shouldProcess {
+		if err == nil && !shouldProcess {
+			err = fmt.Errorf(errClusterStoreMismatch, store.GetName(), externalSecret.Namespace)
+		}
+
+		log.Error(err, err.Error())
+		r.recorder.Event(&externalSecret, v1.EventTypeWarning, esv1beta1.ReasonInvalidStoreRef, err.Error())
+		conditionSynced := NewExternalSecretCondition(esv1beta1.ExternalSecretReady, v1.ConditionFalse, esv1beta1.ConditionReasonSecretSyncedError, errStoreUsability)
+		SetExternalSecretCondition(&externalSecret, *conditionSynced)
+		syncCallsError.With(syncCallsMetricLabels).Inc()
+		return ctrl.Result{}, err
+	}
+
 	if r.EnableFloodGate {
 		if err = assertStoreIsUsable(store); err != nil {
 			log.Error(err, errStoreUsability)
@@ -525,6 +542,45 @@ func (r *Reconciler) getStore(ctx context.Context, externalSecret *esv1beta1.Ext
 	return &store, nil
 }
 
+func (r *Reconciler) ShouldProcessSecret(ctx context.Context, store esv1beta1.GenericStore, ns string) (bool, error) {
+	if store.GetKind() != esv1beta1.ClusterSecretStoreKind {
+		return true, nil
+	}
+
+	if len(store.GetSpec().Conditions) == 0 {
+		return true, nil
+	}
+
+	namespaceList := &v1.NamespaceList{}
+
+	for _, condition := range store.GetSpec().Conditions {
+		if condition.NamespaceSelector != nil {
+			namespaceSelector, err := metav1.LabelSelectorAsSelector(condition.NamespaceSelector)
+			if err != nil {
+				return false, err
+			}
+
+			if err := r.Client.List(context.Background(), namespaceList, client.MatchingLabelsSelector{Selector: namespaceSelector}); err != nil {
+				return false, err
+			}
+
+			for _, namespace := range namespaceList.Items {
+				if namespace.GetName() == ns {
+					return true, nil // namespace matches the labelselector
+				}
+			}
+		}
+
+		if condition.Namespaces != nil {
+			if slices.Contains(condition.Namespaces, ns) {
+				return true, nil // namespace in the namespaces list
+			}
+		}
+	}
+
+	return false, nil
+}
+
 // getProviderSecretData returns the provider's secret data with the provided ExternalSecret.
 func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient esv1beta1.SecretsClient, externalSecret *esv1beta1.ExternalSecret) (map[string][]byte, error) {
 	providerData := make(map[string][]byte)

+ 248 - 14
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -44,7 +44,7 @@ var (
 )
 
 type testCase struct {
-	secretStore    *esv1beta1.SecretStore
+	secretStore    esv1beta1.GenericStore
 	externalSecret *esv1beta1.ExternalSecret
 
 	// checkCondition should return true if the externalSecret
@@ -130,6 +130,7 @@ var _ = Describe("Kind=secret existence logic", func() {
 	}
 })
 var _ = Describe("ExternalSecret controller", func() {
+
 	const (
 		ExternalSecretName             = "test-es"
 		ExternalSecretFQDN             = "externalsecrets.external-secrets.io/test-es"
@@ -140,6 +141,8 @@ var _ = Describe("ExternalSecret controller", func() {
 		targetPropObj                  = "{{ .targetProperty | toString | upper }} was templated"
 		FooValue                       = "map-foo-value"
 		BarValue                       = "map-bar-value"
+		NamespaceLabelKey              = "css-test-label-key"
+		NamespaceLabelValue            = "css-test-label-value"
 	)
 
 	var ExternalSecretNamespace string
@@ -153,7 +156,7 @@ var _ = Describe("ExternalSecret controller", func() {
 
 	BeforeEach(func() {
 		var err error
-		ExternalSecretNamespace, err = ctest.CreateNamespace("test-ns", k8sClient)
+		ExternalSecretNamespace, err = ctest.CreateNamespaceWithLabels("test-ns", k8sClient, map[string]string{NamespaceLabelKey: NamespaceLabelValue})
 		Expect(err).ToNot(HaveOccurred())
 		metric.Reset()
 		syncCallsTotal.Reset()
@@ -163,20 +166,36 @@ var _ = Describe("ExternalSecret controller", func() {
 		fakeProvider.Reset()
 	})
 
-	AfterEach(func() {
-		Expect(k8sClient.Delete(context.Background(), &v1.Namespace{
-			ObjectMeta: metav1.ObjectMeta{
-				Name: ExternalSecretNamespace,
-			},
-		})).To(Succeed())
-		Expect(k8sClient.Delete(context.Background(), &esv1beta1.SecretStore{
-			ObjectMeta: metav1.ObjectMeta{
+	AfterEach(
+		func() {
+			secretStore := &esv1beta1.SecretStore{}
+			secretStoreLookupKey := types.NamespacedName{
 				Name:      ExternalSecretStore,
 				Namespace: ExternalSecretNamespace,
-			},
-		})).To(Succeed())
-	})
+			}
+
+			if err := k8sClient.Get(context.Background(), secretStoreLookupKey, secretStore); err == nil {
+				Expect(k8sClient.Delete(context.Background(), secretStore)).To(Succeed())
+			}
 
+			clusterSecretStore := &esv1beta1.ClusterSecretStore{}
+			clusterSecretStoreLookupKey := types.NamespacedName{
+				Name: ExternalSecretStore,
+			}
+
+			if err := k8sClient.Get(context.Background(), clusterSecretStoreLookupKey, clusterSecretStore); err == nil {
+				Expect(k8sClient.Delete(context.Background(), clusterSecretStore)).To(Succeed())
+			}
+
+			Expect(k8sClient.Delete(context.Background(), &v1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: ExternalSecretNamespace,
+				},
+			})).To(Succeed())
+		},
+	)
+
+	const secretVal = "some-value"
 	const targetProp = "targetProperty"
 	const remoteKey = "barz"
 	const remoteProperty = "bang"
@@ -1282,7 +1301,7 @@ var _ = Describe("ExternalSecret controller", func() {
 	// when a SecretStore has a controller field set which we don't care about
 	// the externalSecret must not be touched
 	ignoreMismatchController := func(tc *testCase) {
-		tc.secretStore.Spec.Controller = "nop"
+		tc.secretStore.GetSpec().Controller = "nop"
 		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
 			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
 			return cond == nil
@@ -1394,6 +1413,209 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 	}
 
+	useClusterSecretStore := func(tc *testCase) {
+		tc.secretStore = &esv1beta1.ClusterSecretStore{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: ExternalSecretStore,
+			},
+			Spec: esv1beta1.SecretStoreSpec{
+				Provider: &esv1beta1.SecretStoreProvider{
+					AWS: &esv1beta1.AWSProvider{
+						Service: esv1beta1.AWSServiceSecretsManager,
+					},
+				},
+			},
+		}
+		tc.externalSecret.Spec.SecretStoreRef.Kind = esv1beta1.ClusterSecretStoreKind
+		fakeProvider.WithGetSecret([]byte(secretVal), nil)
+	}
+
+	// Secret is created when ClusterSecretStore has no conditions
+	noConditionsSecretCreated := func(tc *testCase) {
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
+		}
+	}
+
+	// Secret is not created when ClusterSecretStore has a single non-matching string condition
+	noSecretCreatedWhenNamespaceDoesntMatchStringCondition := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				Namespaces: []string{"some-other-ns"},
+			},
+		}
+
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+	}
+
+	// Secret is not created when ClusterSecretStore has a single non-matching string condition with multiple names
+	noSecretCreatedWhenNamespaceDoesntMatchStringConditionWithMultipleNames := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				Namespaces: []string{"some-other-ns", "another-ns"},
+			},
+		}
+
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+	}
+
+	// Secret is not created when ClusterSecretStore has a multiple non-matching string condition
+	noSecretCreatedWhenNamespaceDoesntMatchMultipleStringCondition := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				Namespaces: []string{"some-other-ns"},
+			},
+			{
+				Namespaces: []string{"another-ns"},
+			},
+		}
+
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+	}
+
+	// Secret is created when ClusterSecretStore has a single matching string condition
+	secretCreatedWhenNamespaceMatchesSingleStringCondition := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				Namespaces: []string{ExternalSecretNamespace},
+			},
+		}
+
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
+		}
+	}
+
+	// Secret is created when ClusterSecretStore has a multiple string conditions, one matching
+	secretCreatedWhenNamespaceMatchesMultipleStringConditions := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				Namespaces: []string{ExternalSecretNamespace, "some-other-ns"},
+			},
+		}
+
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
+		}
+	}
+
+	// Secret is not created when ClusterSecretStore has a single non-matching label condition
+	noSecretCreatedWhenNamespaceDoesntMatchLabelCondition := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"some-label-key": "some-label-value"}},
+			},
+		}
+
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+	}
+
+	// Secret is created when ClusterSecretStore has a single matching label condition
+	secretCreatedWhenNamespaceMatchOnlyLabelCondition := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{NamespaceLabelKey: NamespaceLabelValue}},
+			},
+		}
+
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
+		}
+	}
+
+	// Secret is not created when ClusterSecretStore has a partially matching label condition
+	noSecretCreatedWhenNamespacePartiallyMatchLabelCondition := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{NamespaceLabelKey: NamespaceLabelValue, "some-label-key": "some-label-value"}},
+			},
+		}
+
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+	}
+
+	// Secret is created when ClusterSecretStore has at least one matching label condition
+	secretCreatedWhenNamespaceMatchOneLabelCondition := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{NamespaceLabelKey: NamespaceLabelValue}},
+			},
+			{
+				NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"some-label-key": "some-label-value"}},
+			},
+		}
+
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
+		}
+	}
+
+	// Secret is created when ClusterSecretStore has multiple matching conditions
+	secretCreatedWhenNamespaceMatchMultipleConditions := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{NamespaceLabelKey: NamespaceLabelValue}},
+			},
+			{
+				Namespaces: []string{ExternalSecretNamespace},
+			},
+		}
+
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			Expect(string(secret.Data[targetProp])).To(Equal(secretVal))
+		}
+	}
+
+	// Secret is not created when ClusterSecretStore has multiple non-matching conditions
+	noSecretCreatedWhenNamespaceMatchMultipleNonMatchingConditions := func(tc *testCase) {
+		tc.secretStore.GetSpec().Conditions = []esv1beta1.ClusterSecretStoreCondition{
+			{
+				NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"some-label-key": "some-label-value"}},
+			},
+			{
+				Namespaces: []string{"some-other-ns"},
+			},
+		}
+
+		tc.checkCondition = func(es *esv1beta1.ExternalSecret) bool {
+			cond := GetExternalSecretCondition(es.Status, esv1beta1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionFalse || cond.Reason != esv1beta1.ConditionReasonSecretSyncedError {
+				return false
+			}
+			return true
+		}
+	}
+
 	DescribeTable("When reconciling an ExternalSecret",
 		func(tweaks ...testTweaks) {
 			tc := makeDefaultTestcase()
@@ -1471,6 +1693,18 @@ var _ = Describe("ExternalSecret controller", func() {
 		Entry("should eventually delete target secret with deletionPolicy=Delete", deleteSecretPolicy),
 		Entry("should not delete target secret with deletionPolicy=Retain", deleteSecretPolicyRetain),
 		Entry("should not delete pre-existing secret with deletionPolicy=Merge", deleteSecretPolicyMerge),
+		Entry("secret is created when there are no conditions for the cluster secret store", useClusterSecretStore, noConditionsSecretCreated),
+		Entry("secret is not created when the condition for the cluster secret store states a different namespace single string condition", useClusterSecretStore, noSecretCreatedWhenNamespaceDoesntMatchStringCondition),
+		Entry("secret is not created when the condition for the cluster secret store states a different namespace single string condition with multiple names", useClusterSecretStore, noSecretCreatedWhenNamespaceDoesntMatchStringConditionWithMultipleNames),
+		Entry("secret is not created when the condition for the cluster secret store states a different namespace multiple string conditions", useClusterSecretStore, noSecretCreatedWhenNamespaceDoesntMatchMultipleStringCondition),
+		Entry("secret is created when the condition for the cluster secret store has only one matching namespace by string condition", useClusterSecretStore, secretCreatedWhenNamespaceMatchesSingleStringCondition),
+		Entry("secret is created when the condition for the cluster secret store has one matching namespace of multiple namespaces by string condition", useClusterSecretStore, secretCreatedWhenNamespaceMatchesMultipleStringConditions),
+		Entry("secret is not created when the condition for the cluster secret store states a non-matching label condition", useClusterSecretStore, noSecretCreatedWhenNamespaceDoesntMatchLabelCondition),
+		Entry("secret is created when the condition for the cluster secret store states a single matching label condition", useClusterSecretStore, secretCreatedWhenNamespaceMatchOnlyLabelCondition),
+		Entry("secret is not created when the condition for the cluster secret store states a partially-matching label condition", useClusterSecretStore, noSecretCreatedWhenNamespacePartiallyMatchLabelCondition),
+		Entry("secret is created when one of the label conditions for the cluster secret store matches", useClusterSecretStore, secretCreatedWhenNamespaceMatchOneLabelCondition),
+		Entry("secret is created when the namespaces matches multiple cluster secret store conditions", useClusterSecretStore, secretCreatedWhenNamespaceMatchMultipleConditions),
+		Entry("secret is not created when the namespaces doesn't match any of multiple cluster secret store conditions", useClusterSecretStore, noSecretCreatedWhenNamespaceMatchMultipleNonMatchingConditions),
 	)
 })
 

+ 5 - 4
pkg/controllers/externalsecret/suite_test.go

@@ -82,10 +82,11 @@ var _ = BeforeSuite(func() {
 	Expect(err).ToNot(HaveOccurred())
 
 	err = (&Reconciler{
-		Client:          k8sClient,
-		Scheme:          k8sManager.GetScheme(),
-		Log:             ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
-		RequeueInterval: time.Second,
+		Client:                    k8sClient,
+		Scheme:                    k8sManager.GetScheme(),
+		Log:                       ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
+		RequeueInterval:           time.Second,
+		ClusterSecretStoreEnabled: true,
 	}).SetupWithManager(k8sManager, controller.Options{
 		MaxConcurrentReconciles: 1,
 	})