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, + }, + } + }, })) })