Browse Source

feat: add namespace list selector to ClusterExternalSecrets (#2803)

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Gergely Brautigam 2 years ago
parent
commit
7fbae000d6

+ 7 - 2
apis/externalsecrets/v1beta1/clusterexternalsecret_types.go

@@ -33,9 +33,14 @@ type ClusterExternalSecretSpec struct {
 	ExternalSecretMetadata ExternalSecretMetadata `json:"externalSecretMetadata"`
 
 	// The labels to select by to find the Namespaces to create the ExternalSecrets in.
-	NamespaceSelector metav1.LabelSelector `json:"namespaceSelector"`
+	// +optional
+	NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"`
+
+	// Choose namespaces by name. This field is ORed with anything that NamespaceSelector ends up choosing.
+	// +optional
+	Namespaces []string `json:"namespaces,omitempty"`
 
-	// The time in which the controller should reconcile it's objects and recheck namespaces for labels.
+	// The time in which the controller should reconcile its objects and recheck namespaces for labels.
 	RefreshInterval *metav1.Duration `json:"refreshTime,omitempty"`
 }
 

+ 10 - 1
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -492,7 +492,16 @@ func (in *ClusterExternalSecretSpec) DeepCopyInto(out *ClusterExternalSecretSpec
 	*out = *in
 	in.ExternalSecretSpec.DeepCopyInto(&out.ExternalSecretSpec)
 	in.ExternalSecretMetadata.DeepCopyInto(&out.ExternalSecretMetadata)
-	in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector)
+	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)
+	}
 	if in.RefreshInterval != nil {
 		in, out := &in.RefreshInterval, &out.RefreshInterval
 		*out = new(v1.Duration)

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

@@ -490,13 +490,18 @@ spec:
                     type: object
                 type: object
                 x-kubernetes-map-type: atomic
+              namespaces:
+                description: Choose namespaces by name. This field is ORed with anything
+                  that NamespaceSelector ends up choosing.
+                items:
+                  type: string
+                type: array
               refreshTime:
-                description: The time in which the controller should reconcile it's
+                description: The time in which the controller should reconcile its
                   objects and recheck namespaces for labels.
                 type: string
             required:
             - externalSecretSpec
-            - namespaceSelector
             type: object
           status:
             description: ClusterExternalSecretStatus defines the observed state of

+ 6 - 2
deploy/crds/bundle.yaml

@@ -411,12 +411,16 @@ spec:
                       type: object
                   type: object
                   x-kubernetes-map-type: atomic
+                namespaces:
+                  description: Choose namespaces by name. This field is ORed with anything that NamespaceSelector ends up choosing.
+                  items:
+                    type: string
+                  type: array
                 refreshTime:
-                  description: The time in which the controller should reconcile it's objects and recheck namespaces for labels.
+                  description: The time in which the controller should reconcile its objects and recheck namespaces for labels.
                   type: string
               required:
                 - externalSecretSpec
-                - namespaceSelector
               type: object
             status:
               description: ClusterExternalSecretStatus defines the observed state of ClusterExternalSecret.

+ 28 - 2
docs/api/spec.md

@@ -1183,11 +1183,24 @@ Kubernetes meta/v1.LabelSelector
 </em>
 </td>
 <td>
+<em>(Optional)</em>
 <p>The labels to select by to find the Namespaces to create the ExternalSecrets in.</p>
 </td>
 </tr>
 <tr>
 <td>
+<code>namespaces</code></br>
+<em>
+[]string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Choose namespaces by name. This field is ORed with anything that NamespaceSelector ends up choosing.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>refreshTime</code></br>
 <em>
 <a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
@@ -1196,7 +1209,7 @@ Kubernetes meta/v1.Duration
 </em>
 </td>
 <td>
-<p>The time in which the controller should reconcile it&rsquo;s objects and recheck namespaces for labels.</p>
+<p>The time in which the controller should reconcile its objects and recheck namespaces for labels.</p>
 </td>
 </tr>
 </table>
@@ -1343,11 +1356,24 @@ Kubernetes meta/v1.LabelSelector
 </em>
 </td>
 <td>
+<em>(Optional)</em>
 <p>The labels to select by to find the Namespaces to create the ExternalSecrets in.</p>
 </td>
 </tr>
 <tr>
 <td>
+<code>namespaces</code></br>
+<em>
+[]string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Choose namespaces by name. This field is ORed with anything that NamespaceSelector ends up choosing.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>refreshTime</code></br>
 <em>
 <a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
@@ -1356,7 +1382,7 @@ Kubernetes meta/v1.Duration
 </em>
 </td>
 <td>
-<p>The time in which the controller should reconcile it&rsquo;s objects and recheck namespaces for labels.</p>
+<p>The time in which the controller should reconcile its objects and recheck namespaces for labels.</p>
 </td>
 </tr>
 </tbody>

+ 61 - 20
pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller.go

@@ -18,6 +18,7 @@ import (
 	"context"
 	"fmt"
 	"reflect"
+	"slices"
 	"sort"
 	"time"
 
@@ -96,17 +97,40 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 		refreshInt = clusterExternalSecret.Spec.RefreshInterval.Duration
 	}
 
-	labelSelector, err := metav1.LabelSelectorAsSelector(&clusterExternalSecret.Spec.NamespaceSelector)
-	if err != nil {
-		log.Error(err, errConvertLabelSelector)
-		return ctrl.Result{}, err
+	namespaceList := v1.NamespaceList{}
+
+	if clusterExternalSecret.Spec.NamespaceSelector != nil {
+		labelSelector, err := metav1.LabelSelectorAsSelector(clusterExternalSecret.Spec.NamespaceSelector)
+		if err != nil {
+			log.Error(err, errConvertLabelSelector)
+			return ctrl.Result{}, err
+		}
+
+		err = r.List(ctx, &namespaceList, &client.ListOptions{LabelSelector: labelSelector})
+		if err != nil {
+			log.Error(err, errNamespaces)
+			return ctrl.Result{}, err
+		}
 	}
 
-	namespaceList := v1.NamespaceList{}
-	err = r.List(ctx, &namespaceList, &client.ListOptions{LabelSelector: labelSelector})
-	if err != nil {
-		log.Error(err, errNamespaces)
-		return ctrl.Result{}, err
+	if len(clusterExternalSecret.Spec.Namespaces) > 0 {
+		var additionalNamespace []v1.Namespace
+
+		for _, ns := range clusterExternalSecret.Spec.Namespaces {
+			namespace := &v1.Namespace{}
+			if err = r.Get(ctx, types.NamespacedName{Name: ns}, namespace); err != nil {
+				if apierrors.IsNotFound(err) {
+					continue
+				}
+
+				log.Error(err, errNamespaces)
+				return ctrl.Result{}, err
+			}
+
+			additionalNamespace = append(additionalNamespace, *namespace)
+		}
+
+		namespaceList.Items = append(namespaceList.Items, additionalNamespace...)
 	}
 
 	esName := clusterExternalSecret.Spec.ExternalSecretName
@@ -298,19 +322,36 @@ func (r *Reconciler) findObjectsForNamespace(ctx context.Context, namespace clie
 	var requests []reconcile.Request
 	for i := range clusterExternalSecrets.Items {
 		clusterExternalSecret := &clusterExternalSecrets.Items[i]
-		labelSelector, err := metav1.LabelSelectorAsSelector(&clusterExternalSecret.Spec.NamespaceSelector)
-		if err != nil {
-			r.Log.Error(err, errConvertLabelSelector)
-			return []reconcile.Request{}
+		if clusterExternalSecret.Spec.NamespaceSelector != nil {
+			labelSelector, err := metav1.LabelSelectorAsSelector(clusterExternalSecret.Spec.NamespaceSelector)
+			if err != nil {
+				r.Log.Error(err, errConvertLabelSelector)
+				return []reconcile.Request{}
+			}
+
+			if labelSelector.Matches(labels.Set(namespace.GetLabels())) {
+				requests = append(requests, reconcile.Request{
+					NamespacedName: types.NamespacedName{
+						Name:      clusterExternalSecret.GetName(),
+						Namespace: clusterExternalSecret.GetNamespace(),
+					},
+				})
+
+				// Prevent the object from being added twice if it happens to be listed
+				// by Namespaces selector as well.
+				continue
+			}
 		}
 
-		if labelSelector.Matches(labels.Set(namespace.GetLabels())) {
-			requests = append(requests, reconcile.Request{
-				NamespacedName: types.NamespacedName{
-					Name:      clusterExternalSecret.GetName(),
-					Namespace: clusterExternalSecret.GetNamespace(),
-				},
-			})
+		if len(clusterExternalSecret.Spec.Namespaces) > 0 {
+			if slices.Contains(clusterExternalSecret.Spec.Namespaces, namespace.GetName()) {
+				requests = append(requests, reconcile.Request{
+					NamespacedName: types.NamespacedName{
+						Name:      clusterExternalSecret.GetName(),
+						Namespace: clusterExternalSecret.GetNamespace(),
+					},
+				})
+			}
 		}
 	}
 

+ 78 - 13
pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller_test.go

@@ -156,7 +156,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
 			},
 			clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
 				ces := defaultClusterExternalSecret()
-				ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
+				ces.Spec.NamespaceSelector = &metav1.LabelSelector{
+					MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
+				}
 				return *ces
 			},
 			expectedClusterExternalSecret: func(namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) esv1beta1.ClusterExternalSecret {
@@ -195,7 +197,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
 			},
 			clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
 				ces := defaultClusterExternalSecret()
-				ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
+				ces.Spec.NamespaceSelector = &metav1.LabelSelector{
+					MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
+				}
 				ces.Spec.ExternalSecretName = "test-es"
 				ces.Spec.ExternalSecretMetadata = esv1beta1.ExternalSecretMetadata{
 					Labels:      map[string]string{"test-label-key1": "test-label-value1", "test-label-key2": "test-label-value2"},
@@ -241,7 +245,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
 			},
 			clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
 				ces := defaultClusterExternalSecret()
-				ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
+				ces.Spec.NamespaceSelector = &metav1.LabelSelector{
+					MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
+				}
 				ces.Spec.ExternalSecretName = "old-es-name"
 				return *ces
 			},
@@ -296,7 +302,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
 			},
 			clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
 				ces := defaultClusterExternalSecret()
-				ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
+				ces.Spec.NamespaceSelector = &metav1.LabelSelector{
+					MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
+				}
 				return *ces
 			},
 			beforeCheck: func(ctx context.Context, namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) {
@@ -366,7 +374,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
 			},
 			clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
 				ces := defaultClusterExternalSecret()
-				ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
+				ces.Spec.NamespaceSelector = &metav1.LabelSelector{
+					MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
+				}
 
 				es := &esv1beta1.ExternalSecret{
 					ObjectMeta: metav1.ObjectMeta{
@@ -426,7 +436,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
 			},
 			clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
 				ces := defaultClusterExternalSecret()
-				ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name}
+				ces.Spec.NamespaceSelector = &metav1.LabelSelector{
+					MatchLabels: map[string]string{"kubernetes.io/metadata.name": namespaces[0].Name},
+				}
 
 				es := &esv1beta1.ExternalSecret{
 					ObjectMeta: metav1.ObjectMeta{
@@ -501,7 +513,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
 			clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
 				ces := defaultClusterExternalSecret()
 				ces.Spec.RefreshInterval = &metav1.Duration{Duration: 100 * time.Millisecond}
-				ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"no-longer-match-label-key": "no-longer-match-label-value"}
+				ces.Spec.NamespaceSelector = &metav1.LabelSelector{
+					MatchLabels: map[string]string{"no-longer-match-label-key": "no-longer-match-label-value"},
+				}
 				return *ces
 			},
 			beforeCheck: func(ctx context.Context, namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) {
@@ -570,11 +584,13 @@ var _ = Describe("ClusterExternalSecret controller", func() {
 			clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
 				ces := defaultClusterExternalSecret()
 				ces.Spec.RefreshInterval = &metav1.Duration{Duration: 100 * time.Millisecond}
-				ces.Spec.NamespaceSelector.MatchExpressions = []metav1.LabelSelectorRequirement{
-					{
-						Key:      "prefix",
-						Operator: metav1.LabelSelectorOpIn,
-						Values:   []string{"foo", "bar"}, // "baz" is excluded
+				ces.Spec.NamespaceSelector = &metav1.LabelSelector{
+					MatchExpressions: []metav1.LabelSelectorRequirement{
+						{
+							Key:      "prefix",
+							Operator: metav1.LabelSelectorOpIn,
+							Values:   []string{"foo", "bar"}, // "baz" is excluded
+						},
 					},
 				}
 				return *ces
@@ -628,7 +644,9 @@ var _ = Describe("ClusterExternalSecret controller", func() {
 			},
 			clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
 				ces := defaultClusterExternalSecret()
-				ces.Spec.NamespaceSelector.MatchLabels = map[string]string{"kubernetes.io/metadata.name": "no-namespace-matches"}
+				ces.Spec.NamespaceSelector = &metav1.LabelSelector{
+					MatchLabels: map[string]string{"kubernetes.io/metadata.name": "no-namespace-matches"},
+				}
 				return *ces
 			},
 			expectedClusterExternalSecret: func(namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) esv1beta1.ClusterExternalSecret {
@@ -652,6 +670,53 @@ var _ = Describe("ClusterExternalSecret controller", func() {
 			expectedExternalSecrets: func(namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) []esv1beta1.ExternalSecret {
 				return []esv1beta1.ExternalSecret{}
 			},
+		}),
+		Entry("Should be ready if namespace is selected via the namespace selector", testCase{
+			namespaces: []v1.Namespace{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "not-matching-namespace",
+					},
+				},
+			},
+			clusterExternalSecret: func(namespaces []v1.Namespace) esv1beta1.ClusterExternalSecret {
+				ces := defaultClusterExternalSecret()
+				// does-not-exists tests that we would continue on to the next and not stop if the
+				// namespace hasn't been created yet.
+				ces.Spec.Namespaces = []string{"does-not-exist", "not-matching-namespace"}
+				return *ces
+			},
+			expectedClusterExternalSecret: func(namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) esv1beta1.ClusterExternalSecret {
+				return esv1beta1.ClusterExternalSecret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: created.Name,
+					},
+					Spec: created.Spec,
+					Status: esv1beta1.ClusterExternalSecretStatus{
+						ExternalSecretName: created.Name,
+						ProvisionedNamespaces: []string{
+							"not-matching-namespace",
+						},
+						Conditions: []esv1beta1.ClusterExternalSecretStatusCondition{
+							{
+								Type:   esv1beta1.ClusterExternalSecretReady,
+								Status: v1.ConditionTrue,
+							},
+						},
+					},
+				}
+			},
+			expectedExternalSecrets: func(namespaces []v1.Namespace, created esv1beta1.ClusterExternalSecret) []esv1beta1.ExternalSecret {
+				return []esv1beta1.ExternalSecret{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Namespace: "not-matching-namespace",
+							Name:      created.Name,
+						},
+						Spec: created.Spec.ExternalSecretSpec,
+					},
+				}
+			},
 		}))
 })