From 7fbae000d6e6249873c686e2ff40717dbbb5022c Mon Sep 17 00:00:00 2001
From: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Date: Wed, 25 Oct 2023 13:58:05 +0200
Subject: [PATCH] feat: add namespace list selector to ClusterExternalSecrets
(#2803)
Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
---
.../v1beta1/clusterexternalsecret_types.go | 9 +-
.../v1beta1/zz_generated.deepcopy.go | 11 ++-
...nal-secrets.io_clusterexternalsecrets.yaml | 9 +-
deploy/crds/bundle.yaml | 8 +-
docs/api/spec.md | 30 +++++-
.../clusterexternalsecret_controller.go | 81 +++++++++++++----
.../clusterexternalsecret_controller_test.go | 91 ++++++++++++++++---
7 files changed, 197 insertions(+), 42 deletions(-)
diff --git a/apis/externalsecrets/v1beta1/clusterexternalsecret_types.go b/apis/externalsecrets/v1beta1/clusterexternalsecret_types.go
index 2e6bac4e2..d824ff32c 100644
--- a/apis/externalsecrets/v1beta1/clusterexternalsecret_types.go
+++ b/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"`
- // The time in which the controller should reconcile it's objects and recheck namespaces for labels.
+ // 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 its objects and recheck namespaces for labels.
RefreshInterval *metav1.Duration `json:"refreshTime,omitempty"`
}
diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
index 1426933e7..0a4979527 100644
--- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
+++ b/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)
diff --git a/config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml b/config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml
index 3963fcc3b..1761688d1 100644
--- a/config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml
+++ b/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
diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml
index 9bd16a6b7..b5539d202 100644
--- a/deploy/crds/bundle.yaml
+++ b/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.
diff --git a/docs/api/spec.md b/docs/api/spec.md
index ead347a08..78e2cb4b0 100644
--- a/docs/api/spec.md
+++ b/docs/api/spec.md
@@ -1183,11 +1183,24 @@ Kubernetes meta/v1.LabelSelector
+(Optional)
The labels to select by to find the Namespaces to create the ExternalSecrets in.
|
+namespaces
+
+[]string
+
+ |
+
+(Optional)
+ Choose namespaces by name. This field is ORed with anything that NamespaceSelector ends up choosing.
+ |
+
+
+
refreshTime
@@ -1196,7 +1209,7 @@ Kubernetes meta/v1.Duration
|
- 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.
|
@@ -1343,11 +1356,24 @@ Kubernetes meta/v1.LabelSelector
+(Optional)
The labels to select by to find the Namespaces to create the ExternalSecrets in.
|
+namespaces
+
+[]string
+
+ |
+
+(Optional)
+ Choose namespaces by name. This field is ORed with anything that NamespaceSelector ends up choosing.
+ |
+
+
+
refreshTime
@@ -1356,7 +1382,7 @@ Kubernetes meta/v1.Duration
|
- 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.
|
diff --git a/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller.go b/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller.go
index a49bf4bf4..3cb62f6a6 100644
--- a/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller.go
+++ b/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(),
+ },
+ })
+ }
}
}
diff --git a/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller_test.go b/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller_test.go
index bd36d29c1..313793880 100644
--- a/pkg/controllers/clusterexternalsecret/clusterexternalsecret_controller_test.go
+++ b/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,
+ },
+ }
+ },
}))
})