diff --git a/charts/cluster-api-runtime-extensions-nutanix/README.md b/charts/cluster-api-runtime-extensions-nutanix/README.md index 9c6c7998e..dd371bcbc 100644 --- a/charts/cluster-api-runtime-extensions-nutanix/README.md +++ b/charts/cluster-api-runtime-extensions-nutanix/README.md @@ -109,7 +109,7 @@ A Helm chart for cluster-api-runtime-extensions-nutanix | imagePullSecrets | list | `[]` | Optional secrets used for pulling the container image | | namespaceSync.enabled | bool | `true` | | | namespaceSync.sourceNamespace | string | `""` | | -| namespaceSync.targetNamespaceLabelKey | string | `"caren.nutanix.com/namespace-sync"` | | +| namespaceSync.targetNamespaceLabelSelector | string | `"caren.nutanix.com/namespace-sync"` | | | nodeSelector | object | `{}` | | | priorityClassName | string | `"system-cluster-critical"` | Priority class to be used for the pod. | | resources.limits.cpu | string | `"100m"` | | diff --git a/charts/cluster-api-runtime-extensions-nutanix/templates/deployment.yaml b/charts/cluster-api-runtime-extensions-nutanix/templates/deployment.yaml index 647519413..1dbf9e6bd 100644 --- a/charts/cluster-api-runtime-extensions-nutanix/templates/deployment.yaml +++ b/charts/cluster-api-runtime-extensions-nutanix/templates/deployment.yaml @@ -32,7 +32,7 @@ spec: - --defaults-namespace=$(POD_NAMESPACE) - --namespacesync-enabled={{ .Values.namespaceSync.enabled }} - --namespacesync-source-namespace={{ default .Release.Namespace .Values.namespaceSync.sourceNamespace }} - - --namespacesync-target-namespace-label-key={{ .Values.namespaceSync.targetNamespaceLabelKey }} + - --namespacesync-target-namespace-label-selector={{ .Values.namespaceSync.targetNamespaceLabelSelector }} - --enforce-clusterautoscaler-limits-enabled={{ .Values.enforceClusterAutoscalerLimits.enabled }} - --failure-domain-rollout-enabled={{ .Values.failureDomainRollout.enabled }} - --failure-domain-rollout-concurrency={{ .Values.failureDomainRollout.concurrency }} diff --git a/charts/cluster-api-runtime-extensions-nutanix/values.schema.json b/charts/cluster-api-runtime-extensions-nutanix/values.schema.json index 29365dde2..85fefab91 100644 --- a/charts/cluster-api-runtime-extensions-nutanix/values.schema.json +++ b/charts/cluster-api-runtime-extensions-nutanix/values.schema.json @@ -683,7 +683,7 @@ "sourceNamespace": { "type": "string" }, - "targetNamespaceLabelKey": { + "targetNamespaceLabelSelector": { "type": "string" } } diff --git a/charts/cluster-api-runtime-extensions-nutanix/values.yaml b/charts/cluster-api-runtime-extensions-nutanix/values.yaml index b26e175de..2363e3116 100644 --- a/charts/cluster-api-runtime-extensions-nutanix/values.yaml +++ b/charts/cluster-api-runtime-extensions-nutanix/values.yaml @@ -143,7 +143,7 @@ deployDefaultClusterClasses: true # target namespace, i.e., every namespace that has a label with a matching key. namespaceSync: enabled: true - targetNamespaceLabelKey: caren.nutanix.com/namespace-sync + targetNamespaceLabelSelector: caren.nutanix.com/namespace-sync # By default, sourceNamespace is the helm release namespace. sourceNamespace: "" diff --git a/cmd/main.go b/cmd/main.go index a613b2000..ea477de69 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,6 +9,7 @@ import ( "os" "github.com/spf13/pflag" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -191,10 +192,32 @@ func main() { if namespacesyncOptions.Enabled { if namespacesyncOptions.SourceNamespace == "" || - namespacesyncOptions.TargetNamespaceLabelKey == "" { + namespacesyncOptions.TargetNamespaceLabelSelector == "" { setupLog.Error( nil, - "Namespace Sync is enabled, but source namespace and/or target namespace label key are not configured.", + "Namespace Sync is enabled, but source namespace and/or target namespace label selector are not configured.", + ) + os.Exit(1) + } + + targetSelector, err := metav1.ParseToLabelSelector(namespacesyncOptions.TargetNamespaceLabelSelector) + if err != nil { + setupLog.Error( + err, + "unable to parse target namespace label selector", + "selector", + namespacesyncOptions.TargetNamespaceLabelSelector, + ) + os.Exit(1) + } + + targetLabelSelector, err := metav1.LabelSelectorAsSelector(targetSelector) + if err != nil { + setupLog.Error( + err, + "unable to convert label selector", + "selector", + namespacesyncOptions.TargetNamespaceLabelSelector, ) os.Exit(1) } @@ -215,7 +238,7 @@ func main() { Client: mgr.GetClient(), UnstructuredCachingClient: unstructuredCachingClient, SourceClusterClassNamespace: namespacesyncOptions.SourceNamespace, - IsTargetNamespace: namespacesync.NamespaceHasLabelKey(namespacesyncOptions.TargetNamespaceLabelKey), + TargetNamespaceSelector: targetLabelSelector, }).SetupWithManager( mgr, &controller.Options{MaxConcurrentReconciles: namespacesyncOptions.Concurrency}, diff --git a/pkg/controllers/namespacesync/controller.go b/pkg/controllers/namespacesync/controller.go index ccbf11046..2fa117d36 100644 --- a/pkg/controllers/namespacesync/controller.go +++ b/pkg/controllers/namespacesync/controller.go @@ -8,6 +8,7 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -28,16 +29,17 @@ type Reconciler struct { // SourceClusterClassNamespace is the namespace from which ClusterClasses are copied. SourceClusterClassNamespace string - // IsTargetNamespace determines whether ClusterClasses should be copied to a given namespace. - IsTargetNamespace func(ns *corev1.Namespace) bool + // TargetNamespaceSelector is a label selector to determine which namespaces should receive + // copies of ClusterClasses and Templates from the source namespace. + TargetNamespaceSelector labels.Selector } func (r *Reconciler) SetupWithManager( mgr ctrl.Manager, options *controller.Options, ) error { - if r.IsTargetNamespace == nil { - return fmt.Errorf("define IsTargetNamespace function to use controller") + if r.TargetNamespaceSelector == nil { + return fmt.Errorf("TargetNamespaceSelector must be defined to use controller") } err := ctrl.NewControllerManagedBy(mgr). @@ -51,7 +53,7 @@ func (r *Reconciler) SetupWithManager( if !ok { return false } - return r.IsTargetNamespace(ns) + return r.TargetNamespaceSelector.Matches(labels.Set(ns.GetLabels())) }, UpdateFunc: func(e event.UpdateEvent) bool { // Called when an object is already in the cache, and it is either updated, @@ -66,7 +68,9 @@ func (r *Reconciler) SetupWithManager( } // Only reconcile the namespace if the answer to the question "Is this a // target namespace?" changed from no to yes. - return !r.IsTargetNamespace(nsOld) && r.IsTargetNamespace(nsNew) + matchesOld := r.TargetNamespaceSelector.Matches(labels.Set(nsOld.GetLabels())) + matchesNew := r.TargetNamespaceSelector.Matches(labels.Set(nsNew.GetLabels())) + return !matchesOld && matchesNew }, DeleteFunc: func(e event.DeleteEvent) bool { // Ignore deletes. @@ -93,22 +97,23 @@ func (r *Reconciler) SetupWithManager( func (r *Reconciler) clusterClassToNamespaces(ctx context.Context, o client.Object) []ctrl.Request { namespaceList := &corev1.NamespaceList{} - err := r.Client.List(ctx, namespaceList) + err := r.Client.List(ctx, namespaceList, &client.ListOptions{ + LabelSelector: r.TargetNamespaceSelector, + }) if err != nil { // TODO Log the error, and record an Event. return nil } - rs := []ctrl.Request{} + // Pre-allocate slice with exact capacity since we're using label selector + rs := make([]ctrl.Request, 0, len(namespaceList.Items)) for i := range namespaceList.Items { ns := &namespaceList.Items[i] - if r.IsTargetNamespace(ns) { - rs = append(rs, - ctrl.Request{ - NamespacedName: client.ObjectKeyFromObject(ns), - }, - ) - } + rs = append(rs, + ctrl.Request{ + NamespacedName: client.ObjectKeyFromObject(ns), + }, + ) } return rs } @@ -131,6 +136,7 @@ func (r *Reconciler) Reconcile( // TODO Consider running in parallel. for i := range sccs { scc := &sccs[i] + err := copyClusterClassAndTemplates( ctx, r.Client, diff --git a/pkg/controllers/namespacesync/copy.go b/pkg/controllers/namespacesync/copy.go index f13bd21e5..8a770e9bb 100644 --- a/pkg/controllers/namespacesync/copy.go +++ b/pkg/controllers/namespacesync/copy.go @@ -71,12 +71,6 @@ func copyObjectForCreate[T client.Object](src T, name, namespace string) T { dst.SetName(name) dst.SetNamespace(namespace) - // Zero out ManagedFields (clients will set them) - dst.SetManagedFields(nil) - // Zero out OwnerReferences (object is garbage-collected if - // owners are not in the target namespace) - dst.SetOwnerReferences(nil) - // Zero out fields that are ignored by the API server on create dst.SetCreationTimestamp(metav1.Time{}) dst.SetDeletionGracePeriodSeconds(nil) diff --git a/pkg/controllers/namespacesync/flags.go b/pkg/controllers/namespacesync/flags.go index 1b2bd8e00..6b7aa3f31 100644 --- a/pkg/controllers/namespacesync/flags.go +++ b/pkg/controllers/namespacesync/flags.go @@ -8,38 +8,48 @@ import ( ) type Options struct { - Enabled bool - Concurrency int - SourceNamespace string - TargetNamespaceLabelKey string + Enabled bool + Concurrency int + SourceNamespace string + TargetNamespaceLabelSelector string } func (o *Options) AddFlags(flags *pflag.FlagSet) { - pflag.CommandLine.BoolVar( + flags.BoolVar( &o.Enabled, "namespacesync-enabled", false, "Enable copying of ClusterClasses and Templates from a source namespace to one or more target namespaces.", ) - pflag.CommandLine.IntVar( + flags.IntVar( &o.Concurrency, "namespacesync-concurrency", 10, "Number of target namespaces to sync concurrently.", ) - pflag.CommandLine.StringVar( + flags.StringVar( &o.SourceNamespace, "namespacesync-source-namespace", "", "Namespace from which ClusterClasses and Templates are copied.", ) - pflag.CommandLine.StringVar( - &o.TargetNamespaceLabelKey, + flags.StringVar( + &o.TargetNamespaceLabelSelector, "namespacesync-target-namespace-label-key", "", "Label key to determine if a namespace is a target. If a namespace has a label with this key, copy ClusterClasses and Templates to it from the source namespace.", //nolint:lll // Output will be wrapped. ) + _ = flags.MarkDeprecated( + "namespacesync-target-namespace-label-key", + "use namespacesync-target-namespace-label-selector instead", + ) + flags.StringVar( + &o.TargetNamespaceLabelSelector, + "namespacesync-target-namespace-label-selector", + "", + "Label selector to determine target namespaces. Namespaces matching this selector will receive copies of ClusterClasses and Templates from the source namespace. Example: 'environment=production' or 'team in (platform,infrastructure)'.", //nolint:lll // Output will be wrapped. + ) } diff --git a/pkg/controllers/namespacesync/label.go b/pkg/controllers/namespacesync/label.go deleted file mode 100644 index b0bf3aae9..000000000 --- a/pkg/controllers/namespacesync/label.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2024 Nutanix. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -package namespacesync - -import corev1 "k8s.io/api/core/v1" - -var NamespaceHasLabelKey = func(key string) func(ns *corev1.Namespace) bool { - return func(ns *corev1.Namespace) bool { - _, ok := ns.GetLabels()[key] - return ok - } -} diff --git a/pkg/controllers/namespacesync/label_test.go b/pkg/controllers/namespacesync/label_test.go deleted file mode 100644 index d274687ba..000000000 --- a/pkg/controllers/namespacesync/label_test.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2024 Nutanix. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -package namespacesync - -import ( - "testing" - - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestNamespaceHasLabelKey(t *testing.T) { - tests := []struct { - name string - key string - ns *corev1.Namespace - want bool - }{ - { - name: "match a labeled namespace", - key: "testkey", - ns: &corev1.Namespace{ - ObjectMeta: v1.ObjectMeta{ - Name: "test", - Labels: map[string]string{ - "testkey": "", - }, - }, - }, - want: true, - }, - { - name: "do not match if label key is not found", - key: "testkey", - ns: &corev1.Namespace{ - ObjectMeta: v1.ObjectMeta{ - Name: "test", - }, - }, - want: false, - }, - { - name: "do not match if label key is empty string", - key: "", - ns: &corev1.Namespace{ - ObjectMeta: v1.ObjectMeta{ - Name: "test", - }, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fn := NamespaceHasLabelKey(tt.key) - if got := fn(tt.ns); got != tt.want { - t.Fatalf("got %t, want %t", got, tt.want) - } - }) - } -} diff --git a/pkg/controllers/namespacesync/suite_test.go b/pkg/controllers/namespacesync/suite_test.go index 9b2b5c217..ad4174ffb 100644 --- a/pkg/controllers/namespacesync/suite_test.go +++ b/pkg/controllers/namespacesync/suite_test.go @@ -54,11 +54,22 @@ func TestMain(m *testing.M) { if err != nil { panic(fmt.Sprintf("unable to create unstructuredCachineClient: %v", err)) } + + // Create a label selector that matches namespaces with the target label key + targetSelector, err := metav1.ParseToLabelSelector(targetNamespaceLabelKey) + if err != nil { + panic(fmt.Sprintf("unable to parse label selector: %v", err)) + } + targetLabelSelector, err := metav1.LabelSelectorAsSelector(targetSelector) + if err != nil { + panic(fmt.Sprintf("unable to convert label selector: %v", err)) + } + if err := (&Reconciler{ Client: mgr.GetClient(), UnstructuredCachingClient: unstructuredCachingClient, SourceClusterClassNamespace: sourceClusterClassNamespace, - IsTargetNamespace: NamespaceHasLabelKey(targetNamespaceLabelKey), + TargetNamespaceSelector: targetLabelSelector, }).SetupWithManager(mgr, &controller.Options{MaxConcurrentReconciles: 1}); err != nil { panic(fmt.Sprintf("unable to create reconciler: %v", err)) }