From 8e9b8adac9a79d811e4376b2563ae1ed60d5a5d0 Mon Sep 17 00:00:00 2001 From: Deofex <28751252+Deofex@users.noreply.github.com> Date: Thu, 8 May 2025 08:45:05 +0200 Subject: [PATCH] feat: Add additionalMetadataList Support for Conditional Metadata Assignment (#1339) * feat: Add support for additionalMetadataList Signed-off-by: Deofex <28751252+Deofex@users.noreply.github.com> * docs: change description Signed-off-by: Deofex <28751252+Deofex@users.noreply.github.com> * fix: missing bracket Signed-off-by: Deofex <28751252+Deofex@users.noreply.github.com> * fix: removed duplicated if statement Signed-off-by: Deofex <28751252+Deofex@users.noreply.github.com> * chore: adjustments after review Signed-off-by: Deofex <28751252+Deofex@users.noreply.github.com> * chore: Sync `syncNamespaceMetadata` method Signed-off-by: Deofex <28751252+Deofex@users.noreply.github.com> --------- Signed-off-by: Deofex <28751252+Deofex@users.noreply.github.com> Signed-off-by: Deofex 28751252+Deofex@users.noreply.github.com --- api/v1beta2/namespace_options.go | 2 + api/v1beta2/zz_generated.deepcopy.go | 7 + .../crds/capsule.clastix.io_tenants.yaml | 65 +++++++ controllers/tenant/namespaces.go | 159 +++++++++------- e2e/namespace_additional_metadata_test.go | 179 +++++++++++++++++- pkg/api/additional_metadata.go | 10 + pkg/api/zz_generated.deepcopy.go | 39 +++- pkg/utils/namespace_selector.go | 20 ++ 8 files changed, 405 insertions(+), 76 deletions(-) create mode 100644 pkg/utils/namespace_selector.go diff --git a/api/v1beta2/namespace_options.go b/api/v1beta2/namespace_options.go index ddaed491..e0a872c5 100644 --- a/api/v1beta2/namespace_options.go +++ b/api/v1beta2/namespace_options.go @@ -13,6 +13,8 @@ type NamespaceOptions struct { Quota *int32 `json:"quota,omitempty"` // Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant. Optional. AdditionalMetadata *api.AdditionalMetadataSpec `json:"additionalMetadata,omitempty"` + // Specifies additional labels and annotations the Capsule operator places on any Namespace resource in the Tenant via a list. Optional. + AdditionalMetadataList []api.AdditionalMetadataSelectorSpec `json:"additionalMetadataList,omitempty"` // Define the labels that a Tenant Owner cannot set for their Namespace resources. ForbiddenLabels api.ForbiddenListSpec `json:"forbiddenLabels,omitempty"` // Define the annotations that a Tenant Owner cannot set for their Namespace resources. diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 79ab4aba..5f8f12d2 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -293,6 +293,13 @@ func (in *NamespaceOptions) DeepCopyInto(out *NamespaceOptions) { *out = new(api.AdditionalMetadataSpec) (*in).DeepCopyInto(*out) } + if in.AdditionalMetadataList != nil { + in, out := &in.AdditionalMetadataList, &out.AdditionalMetadataList + *out = make([]api.AdditionalMetadataSelectorSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } in.ForbiddenLabels.DeepCopyInto(&out.ForbiddenLabels) in.ForbiddenAnnotations.DeepCopyInto(&out.ForbiddenAnnotations) } diff --git a/charts/capsule/crds/capsule.clastix.io_tenants.yaml b/charts/capsule/crds/capsule.clastix.io_tenants.yaml index 1b6cf975..a6035ed0 100644 --- a/charts/capsule/crds/capsule.clastix.io_tenants.yaml +++ b/charts/capsule/crds/capsule.clastix.io_tenants.yaml @@ -1373,6 +1373,71 @@ spec: type: string type: object type: object + additionalMetadataList: + description: Specifies additional labels and annotations the Capsule + operator places on any Namespace resource in the Tenant via + a list. Optional. + items: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + namespaceSelector: + description: |- + A label selector is a label query over a set of resources. The result of matchLabels and + matchExpressions are ANDed. An empty label selector matches all objects. A null + label selector matches no objects. + 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 + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + 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 + type: object + type: array forbiddenAnnotations: description: Define the annotations that a Tenant Owner cannot set for their Namespace resources. diff --git a/controllers/tenant/namespaces.go b/controllers/tenant/namespaces.go index cee31121..02ee680e 100644 --- a/controllers/tenant/namespaces.go +++ b/controllers/tenant/namespaces.go @@ -6,6 +6,7 @@ package tenant import ( "context" "fmt" + "maps" "strings" "golang.org/x/sync/errgroup" @@ -42,36 +43,39 @@ func (r *Manager) syncNamespaces(ctx context.Context, tenant *capsulev1beta2.Ten return } -//nolint:gocognit,nakedret func (r *Manager) syncNamespaceMetadata(ctx context.Context, namespace string, tnt *capsulev1beta2.Tenant) (err error) { var res controllerutil.OperationResult err = retry.RetryOnConflict(retry.DefaultBackoff, func() (conflictErr error) { ns := &corev1.Namespace{} if conflictErr = r.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil { - return + return conflictErr } capsuleLabel, _ := utils.GetTypeLabel(&capsulev1beta2.Tenant{}) res, conflictErr = controllerutil.CreateOrUpdate(ctx, r.Client, ns, func() error { - annotations := make(map[string]string) - labels := map[string]string{ - "kubernetes.io/metadata.name": namespace, - capsuleLabel: tnt.GetName(), - } + annotations := buildNamespaceAnnotationsForTenant(tnt) + labels := buildNamespaceLabelsForTenant(tnt) - if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil { - for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Annotations { - annotations[k] = v + if opts := tnt.Spec.NamespaceOptions; opts != nil && len(opts.AdditionalMetadataList) > 0 { + for _, md := range opts.AdditionalMetadataList { + ok, err := utils.IsNamespaceSelectedBySelector(ns, md.NamespaceSelector) + if err != nil { + return err + } + + if !ok { + continue + } + + maps.Copy(labels, md.Labels) + maps.Copy(annotations, md.Annotations) } } - if tnt.Spec.NamespaceOptions != nil && tnt.Spec.NamespaceOptions.AdditionalMetadata != nil { - for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels { - labels[k] = v - } - } + labels["kubernetes.io/metadata.name"] = namespace + labels[capsuleLabel] = tnt.GetName() if tnt.Spec.Cordoned { ns.Labels[utils.CordonedLabel] = "true" @@ -79,76 +83,22 @@ func (r *Manager) syncNamespaceMetadata(ctx context.Context, namespace string, t delete(ns.Labels, utils.CordonedLabel) } - if tnt.Spec.NodeSelector != nil { - annotations = utils.BuildNodeSelector(tnt, annotations) - } - - if tnt.Spec.IngressOptions.AllowedClasses != nil { - if len(tnt.Spec.IngressOptions.AllowedClasses.Exact) > 0 { - annotations[AvailableIngressClassesAnnotation] = strings.Join(tnt.Spec.IngressOptions.AllowedClasses.Exact, ",") - } - - if len(tnt.Spec.IngressOptions.AllowedClasses.Regex) > 0 { - annotations[AvailableIngressClassesRegexpAnnotation] = tnt.Spec.IngressOptions.AllowedClasses.Regex - } - } - - if tnt.Spec.StorageClasses != nil { - if len(tnt.Spec.StorageClasses.Exact) > 0 { - annotations[AvailableStorageClassesAnnotation] = strings.Join(tnt.Spec.StorageClasses.Exact, ",") - } - - if len(tnt.Spec.StorageClasses.Regex) > 0 { - annotations[AvailableStorageClassesRegexpAnnotation] = tnt.Spec.StorageClasses.Regex - } - } - - if tnt.Spec.ContainerRegistries != nil { - if len(tnt.Spec.ContainerRegistries.Exact) > 0 { - annotations[AllowedRegistriesAnnotation] = strings.Join(tnt.Spec.ContainerRegistries.Exact, ",") - } - - if len(tnt.Spec.ContainerRegistries.Regex) > 0 { - annotations[AllowedRegistriesRegexpAnnotation] = tnt.Spec.ContainerRegistries.Regex - } - } - - if value, ok := tnt.Annotations[api.ForbiddenNamespaceLabelsAnnotation]; ok { - annotations[api.ForbiddenNamespaceLabelsAnnotation] = value - } - - if value, ok := tnt.Annotations[api.ForbiddenNamespaceLabelsRegexpAnnotation]; ok { - annotations[api.ForbiddenNamespaceLabelsRegexpAnnotation] = value - } - - if value, ok := tnt.Annotations[api.ForbiddenNamespaceAnnotationsAnnotation]; ok { - annotations[api.ForbiddenNamespaceAnnotationsAnnotation] = value - } - - if value, ok := tnt.Annotations[api.ForbiddenNamespaceAnnotationsRegexpAnnotation]; ok { - annotations[api.ForbiddenNamespaceAnnotationsRegexpAnnotation] = value - } - if ns.Annotations == nil { ns.SetAnnotations(annotations) } else { - for k, v := range annotations { - ns.Annotations[k] = v - } + maps.Copy(ns.Annotations, annotations) } if ns.Labels == nil { ns.SetLabels(labels) } else { - for k, v := range labels { - ns.Labels[k] = v - } + maps.Copy(ns.Labels, labels) } return nil }) - return + return conflictErr }) r.emitEvent(tnt, namespace, res, "Ensuring Namespace metadata", err) @@ -156,6 +106,71 @@ func (r *Manager) syncNamespaceMetadata(ctx context.Context, namespace string, t return err } +func buildNamespaceAnnotationsForTenant(tnt *capsulev1beta2.Tenant) map[string]string { + annotations := make(map[string]string) + + if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil { + maps.Copy(annotations, md.AdditionalMetadata.Annotations) + } + + if tnt.Spec.NodeSelector != nil { + annotations = utils.BuildNodeSelector(tnt, annotations) + } + + if ic := tnt.Spec.IngressOptions.AllowedClasses; ic != nil { + if len(ic.Exact) > 0 { + annotations[AvailableIngressClassesAnnotation] = strings.Join(ic.Exact, ",") + } + + if len(ic.Regex) > 0 { + annotations[AvailableIngressClassesRegexpAnnotation] = ic.Regex + } + } + + if sc := tnt.Spec.StorageClasses; sc != nil { + if len(sc.Exact) > 0 { + annotations[AvailableStorageClassesAnnotation] = strings.Join(sc.Exact, ",") + } + + if len(sc.Regex) > 0 { + annotations[AvailableStorageClassesRegexpAnnotation] = sc.Regex + } + } + + if cr := tnt.Spec.ContainerRegistries; cr != nil { + if len(cr.Exact) > 0 { + annotations[AllowedRegistriesAnnotation] = strings.Join(cr.Exact, ",") + } + + if len(cr.Regex) > 0 { + annotations[AllowedRegistriesRegexpAnnotation] = cr.Regex + } + } + + for _, key := range []string{ + api.ForbiddenNamespaceLabelsAnnotation, + api.ForbiddenNamespaceLabelsRegexpAnnotation, + api.ForbiddenNamespaceAnnotationsAnnotation, + api.ForbiddenNamespaceAnnotationsRegexpAnnotation, + } { + if value, ok := tnt.Annotations[key]; ok { + annotations[key] = value + } + } + + return annotations +} + +func buildNamespaceLabelsForTenant(tnt *capsulev1beta2.Tenant) map[string]string { + labels := make(map[string]string) + + if md := tnt.Spec.NamespaceOptions; md != nil && md.AdditionalMetadata != nil { + maps.Copy(labels, md.AdditionalMetadata.Labels) + } + + return labels +} + func (r *Manager) ensureNamespaceCount(ctx context.Context, tenant *capsulev1beta2.Tenant) error { return retry.RetryOnConflict(retry.DefaultBackoff, func() error { tenant.Status.Size = uint(len(tenant.Status.Namespaces)) diff --git a/e2e/namespace_additional_metadata_test.go b/e2e/namespace_additional_metadata_test.go index d5e936c6..65298ff7 100644 --- a/e2e/namespace_additional_metadata_test.go +++ b/e2e/namespace_additional_metadata_test.go @@ -38,8 +38,10 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", f NamespaceOptions: &capsulev1beta2.NamespaceOptions{ AdditionalMetadata: &api.AdditionalMetadataSpec{ Labels: map[string]string{ - "k8s.io/custom-label": "foo", - "clastix.io/custom-label": "bar", + "k8s.io/custom-label": "foo", + "clastix.io/custom-label": "bar", + "capsule.clastix.io/tenant": "tenan-override", + "kubernetes.io/metadata.name": "namespace-override", }, Annotations: map[string]string{ "k8s.io/custom-annotation": "bizz", @@ -68,6 +70,9 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", f Eventually(func() (ok bool) { Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed()) for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadata.Labels { + if k == "capsule.clastix.io/tenant" || k == "kubernetes.io/metadata.name" { + continue // this label is managed and shouldn't be set by the user + } if ok, _ = HaveKeyWithValue(k, v).Match(ns.Labels); !ok { return } @@ -75,6 +80,18 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", f return }, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue()) }) + By("checking managed labels", func() { + Eventually(func() (ok bool) { + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed()) + if ok, _ = HaveKeyWithValue("capsule.clastix.io/tenant", tnt.GetName()).Match(ns.Labels); !ok { + return + } + if ok, _ = HaveKeyWithValue("kubernetes.io/metadata.name", ns.GetName()).Match(ns.Labels); !ok { + return + } + return + }, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue()) + }) By("checking additional annotations", func() { Eventually(func() (ok bool) { Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed()) @@ -88,3 +105,161 @@ var _ = Describe("creating a Namespace for a Tenant with additional metadata", f }) }) }) + +var _ = Describe("creating a Namespace for a Tenant with additional metadata list", func() { + tnt := &capsulev1beta2.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenant-metadata", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "cap", + Kind: "dummy", + Name: "tenant-metadata", + UID: "tenant-metadata", + }, + }, + }, + Spec: capsulev1beta2.TenantSpec{ + Owners: capsulev1beta2.OwnerListSpec{ + { + Name: "gatsby", + Kind: "User", + }, + }, + NamespaceOptions: &capsulev1beta2.NamespaceOptions{ + AdditionalMetadataList: []api.AdditionalMetadataSelectorSpec{ + { + Labels: map[string]string{ + "k8s.io/custom-label": "foo", + "clastix.io/custom-label": "bar", + }, + Annotations: map[string]string{ + "k8s.io/custom-annotation": "bizz", + "clastix.io/custom-annotation": "buzz", + }, + }, + { + NamespaceSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "matching_namespace_label", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"matching_namespace_label_value"}, + }, + }, + }, + Labels: map[string]string{ + "k8s.io/custom-label_2": "foo", + }, + Annotations: map[string]string{ + "k8s.io/custom-annotation_2": "bizz", + }, + }, + { + NamespaceSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "nonmatching_namespace_label", + Operator: metav1.LabelSelectorOpIn, + Values: []string{"nonmatching_namespace_label_value"}, + }, + }, + }, + Labels: map[string]string{ + "k8s.io/custom-label_3": "foo", + }, + Annotations: map[string]string{ + "k8s.io/custom-annotation_3": "bizz", + }, + }, + }, + }, + }, + } + + JustBeforeEach(func() { + EventuallyCreation(func() error { + return k8sClient.Create(context.TODO(), tnt) + }).Should(Succeed()) + }) + JustAfterEach(func() { + Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed()) + }) + + It("should contain additional Namespace metadata", func() { + labels := map[string]string{ + "matching_namespace_label": "matching_namespace_label_value", + } + ns := NewNamespace("", labels) + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + By("checking additional labels from entry without node selector", func() { + Eventually(func() (ok bool) { + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed()) + for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadataList[0].Labels { + if ok, _ = HaveKeyWithValue(k, v).Match(ns.Labels); !ok { + return + } + } + return + }, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue()) + }) + By("checking additional labels from entry with matching node selector", func() { + Eventually(func() (ok bool) { + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed()) + for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadataList[1].Labels { + if ok, _ = HaveKeyWithValue(k, v).Match(ns.Labels); !ok { + return + } + } + return + }, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue()) + }) + By("checking additional labels from entry with non-matching node selector", func() { + Eventually(func() (ok bool) { + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed()) + for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadataList[2].Labels { + if ok, _ = HaveKeyWithValue(k, v).Match(ns.Labels); !ok { + return + } + } + return + }, defaultTimeoutInterval, defaultPollInterval).Should(BeFalse()) + }) + By("checking additional annotations from entry without node selector", func() { + Eventually(func() (ok bool) { + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed()) + for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadataList[0].Annotations { + if ok, _ = HaveKeyWithValue(k, v).Match(ns.Annotations); !ok { + return + } + } + return + }, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue()) + }) + By("checking additional annotations from entry with matching node selector", func() { + Eventually(func() (ok bool) { + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed()) + for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadataList[1].Annotations { + if ok, _ = HaveKeyWithValue(k, v).Match(ns.Annotations); !ok { + return + } + } + return + }, defaultTimeoutInterval, defaultPollInterval).Should(BeTrue()) + }) + By("checking additional annotations from entry with non-matching node selector", func() { + Eventually(func() (ok bool) { + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: ns.GetName()}, ns)).Should(Succeed()) + for k, v := range tnt.Spec.NamespaceOptions.AdditionalMetadataList[2].Annotations { + if ok, _ = HaveKeyWithValue(k, v).Match(ns.Annotations); !ok { + return + } + } + return + }, defaultTimeoutInterval, defaultPollInterval).Should(BeFalse()) + }) + + }) +}) diff --git a/pkg/api/additional_metadata.go b/pkg/api/additional_metadata.go index 04b347a6..95612e12 100644 --- a/pkg/api/additional_metadata.go +++ b/pkg/api/additional_metadata.go @@ -3,9 +3,19 @@ package api +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + // +kubebuilder:object:generate=true type AdditionalMetadataSpec struct { Labels map[string]string `json:"labels,omitempty"` Annotations map[string]string `json:"annotations,omitempty"` } + +// +kubebuilder:object:generate=true + +type AdditionalMetadataSelectorSpec struct { + NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` +} diff --git a/pkg/api/zz_generated.deepcopy.go b/pkg/api/zz_generated.deepcopy.go index e3ac0fb2..30f032ba 100644 --- a/pkg/api/zz_generated.deepcopy.go +++ b/pkg/api/zz_generated.deepcopy.go @@ -10,9 +10,44 @@ package api import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" - "k8s.io/api/rbac/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdditionalMetadataSelectorSpec) DeepCopyInto(out *AdditionalMetadataSelectorSpec) { + *out = *in + if in.NamespaceSelector != nil { + in, out := &in.NamespaceSelector, &out.NamespaceSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalMetadataSelectorSpec. +func (in *AdditionalMetadataSelectorSpec) DeepCopy() *AdditionalMetadataSelectorSpec { + if in == nil { + return nil + } + out := new(AdditionalMetadataSelectorSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AdditionalMetadataSpec) DeepCopyInto(out *AdditionalMetadataSpec) { *out = *in @@ -47,7 +82,7 @@ func (in *AdditionalRoleBindingsSpec) DeepCopyInto(out *AdditionalRoleBindingsSp *out = *in if in.Subjects != nil { in, out := &in.Subjects, &out.Subjects - *out = make([]v1.Subject, len(*in)) + *out = make([]rbacv1.Subject, len(*in)) copy(*out, *in) } } diff --git a/pkg/utils/namespace_selector.go b/pkg/utils/namespace_selector.go new file mode 100644 index 00000000..46b8aaa1 --- /dev/null +++ b/pkg/utils/namespace_selector.go @@ -0,0 +1,20 @@ +package utils + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +func IsNamespaceSelectedBySelector(ns *corev1.Namespace, selector *metav1.LabelSelector) (bool, error) { + if selector == nil { + return true, nil // If selector is nil, all namespaces match + } + + labelSelector, err := metav1.LabelSelectorAsSelector(selector) + if err != nil { + return false, err + } + + return labelSelector.Matches(labels.Set(ns.Labels)), nil +}