From edb85326a7130ce1f30c55813fa68d4f0e61f5c8 Mon Sep 17 00:00:00 2001 From: Pratik Langde Date: Mon, 7 Sep 2026 21:09:35 +0530 Subject: [PATCH 1/4] Fix GitopsService controller wiping admin NodePlacement on default ArgoCD CR When GitopsService does not configure nodeSelector or tolerations, the reconcile loop was clearing spec.nodePlacement on the default ArgoCD CR. Admins who set nodePlacement directly on the ArgoCD CR (documented pattern for dedicated infra nodes) saw scheduling config removed every reconcile. Only sync NodePlacement when GitopsService explicitly configures placement fields. Track GitopsService-managed placement with an annotation so clearing GitopsService fields still removes operator-applied placement. Fixes redhat-developer/gitops-operator#572 Signed-off-by: Pratik Langde --- common/common.go | 4 + controllers/gitopsservice_controller.go | 30 ++++- controllers/gitopsservice_controller_test.go | 125 +++++++++++++++++++ 3 files changed, 153 insertions(+), 6 deletions(-) diff --git a/common/common.go b/common/common.go index 65e7347e82c..ada9bc7f3b3 100644 --- a/common/common.go +++ b/common/common.go @@ -48,6 +48,10 @@ const ( InfraNodeSelectorAnnotation = "openshift.io/node-selector" // InfraNodeSelectorAnnotationValue is the value for the infra node selector annotation InfraNodeSelectorAnnotationValue = "node-role.kubernetes.io/infra=" + // NodePlacementManagedByGitopsServiceAnnotation marks NodePlacement on the default ArgoCD CR + // that was applied by the GitopsService controller. Used to distinguish GitopsService-managed + // placement from admin edits on the ArgoCD CR directly. + NodePlacementManagedByGitopsServiceAnnotation = "gitops.openshift.io/node-placement-managed-by-gitopsservice" ) // InfraNodeSelector returns openshift label for infrastructure nodes diff --git a/controllers/gitopsservice_controller.go b/controllers/gitopsservice_controller.go index 37bc08080dc..2b5c619b0db 100644 --- a/controllers/gitopsservice_controller.go +++ b/controllers/gitopsservice_controller.go @@ -620,15 +620,33 @@ func (r *ReconcileGitopsService) reconcileDefaultArgoCDInstance(instance *pipeli changed = true } - // if user is patching nodePlacement through GitopsService CR, then existingArgoCD NodePlacement is updated. - if defaultArgoCDInstance.Spec.NodePlacement != nil { - if !reflect.DeepEqual(existingArgoCD.Spec.NodePlacement, defaultArgoCDInstance.Spec.NodePlacement) { + // Sync NodePlacement when GitopsService configures placement fields. Do not wipe NodePlacement + // that admins set directly on the ArgoCD CR when GitopsService placement fields are empty. + gitopsServiceConfiguresNodePlacement := len(instance.Spec.NodeSelector) > 0 || len(instance.Spec.Tolerations) > 0 + if gitopsServiceConfiguresNodePlacement { + if defaultArgoCDInstance.Spec.NodePlacement != nil { + if !reflect.DeepEqual(existingArgoCD.Spec.NodePlacement, defaultArgoCDInstance.Spec.NodePlacement) { + existingArgoCD.Spec.NodePlacement = defaultArgoCDInstance.Spec.NodePlacement + changed = true + } + } else if existingArgoCD.Spec.NodePlacement != nil { existingArgoCD.Spec.NodePlacement = defaultArgoCDInstance.Spec.NodePlacement changed = true } - // Handle the case where NodePlacement should be removed - } else if existingArgoCD.Spec.NodePlacement != nil { - existingArgoCD.Spec.NodePlacement = defaultArgoCDInstance.Spec.NodePlacement + if existingArgoCD.Annotations == nil { + existingArgoCD.Annotations = map[string]string{} + } + if existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] != "true" { + existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] = "true" + changed = true + } + } else if existingArgoCD.Annotations != nil && + existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] == "true" { + if existingArgoCD.Spec.NodePlacement != nil { + existingArgoCD.Spec.NodePlacement = nil + changed = true + } + delete(existingArgoCD.Annotations, common.NodePlacementManagedByGitopsServiceAnnotation) changed = true } diff --git a/controllers/gitopsservice_controller_test.go b/controllers/gitopsservice_controller_test.go index 1d938d62ed8..1be5387a06a 100644 --- a/controllers/gitopsservice_controller_test.go +++ b/controllers/gitopsservice_controller_test.go @@ -268,6 +268,131 @@ func TestReconcileDefaultForArgoCDNodeplacement(t *testing.T) { assertNoError(t, err) assert.Check(t, existingArgoCD.Spec.NodePlacement != nil) assert.DeepEqual(t, existingArgoCD.Spec.NodePlacement.NodeSelector, gitopsService.Spec.NodeSelector) + assert.Equal(t, existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation], "true") +} + +func TestReconcileDefaultArgoCDNodePlacementPreservesDirectEdit(t *testing.T) { + logf.SetLogger(argocd.ZapLogger(true)) + s := scheme.Scheme + addKnownTypesToScheme(s) + + gitopsService := &pipelinesv1alpha1.GitopsService{ + ObjectMeta: v1.ObjectMeta{ + Name: serviceName, + }, + } + + directNodePlacement := &argoapp.ArgoCDNodePlacementSpec{ + NodeSelector: map[string]string{ + "machine.openshift.io/cluster-api-machineset": "cl01-infra-0", + }, + Tolerations: []corev1.Toleration{{ + Effect: corev1.TaintEffectNoSchedule, + Key: "infra", + Value: "reserved", + }}, + } + + fakeClient := fake.NewFakeClient(gitopsService) + reconciler := newReconcileGitOpsService(fakeClient, s) + + existingArgoCD := &argoapp.ArgoCD{ + ObjectMeta: v1.ObjectMeta{ + Name: serviceNamespace, + Namespace: serviceNamespace, + }, + Spec: argoapp.ArgoCDSpec{ + NodePlacement: directNodePlacement, + Server: argoapp.ArgoCDServerSpec{ + Route: argoapp.ArgoCDRouteSpec{ + Enabled: true, + }, + }, + ApplicationSet: &argoapp.ArgoCDApplicationSet{}, + SSO: &argoapp.ArgoCDSSOSpec{ + Provider: "dex", + Dex: &argoapp.ArgoCDDexSpec{ + Config: "test-config", + }, + }, + }, + } + + err := fakeClient.Create(context.TODO(), existingArgoCD) + assertNoError(t, err) + + _, err = reconciler.Reconcile(context.TODO(), newRequest("test", "test")) + assertNoError(t, err) + + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: common.ArgoCDInstanceName, Namespace: serviceNamespace}, + existingArgoCD) + assertNoError(t, err) + assert.DeepEqual(t, existingArgoCD.Spec.NodePlacement, directNodePlacement) + _, hasManagedAnnotation := existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] + assert.Assert(t, !hasManagedAnnotation) +} + +func TestReconcileDefaultArgoCDNodePlacementClearsWhenGitopsServiceCleared(t *testing.T) { + logf.SetLogger(argocd.ZapLogger(true)) + s := scheme.Scheme + addKnownTypesToScheme(s) + + gitopsService := &pipelinesv1alpha1.GitopsService{ + ObjectMeta: v1.ObjectMeta{ + Name: serviceName, + }, + Spec: pipelinesv1alpha1.GitopsServiceSpec{ + NodeSelector: map[string]string{ + "key1": "value1", + }, + }, + } + + fakeClient := fake.NewFakeClient(gitopsService) + reconciler := newReconcileGitOpsService(fakeClient, s) + + existingArgoCD := &argoapp.ArgoCD{ + ObjectMeta: v1.ObjectMeta{ + Name: serviceNamespace, + Namespace: serviceNamespace, + }, + Spec: argoapp.ArgoCDSpec{ + Server: argoapp.ArgoCDServerSpec{ + Route: argoapp.ArgoCDRouteSpec{ + Enabled: true, + }, + }, + ApplicationSet: &argoapp.ArgoCDApplicationSet{}, + SSO: &argoapp.ArgoCDSSOSpec{ + Provider: "dex", + Dex: &argoapp.ArgoCDDexSpec{ + Config: "test-config", + }, + }, + }, + } + + err := fakeClient.Create(context.TODO(), existingArgoCD) + assertNoError(t, err) + + _, err = reconciler.Reconcile(context.TODO(), newRequest("test", "test")) + assertNoError(t, err) + + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: serviceName}, gitopsService) + assertNoError(t, err) + gitopsService.Spec.NodeSelector = nil + err = fakeClient.Update(context.TODO(), gitopsService) + assertNoError(t, err) + + _, err = reconciler.Reconcile(context.TODO(), newRequest("test", "test")) + assertNoError(t, err) + + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: common.ArgoCDInstanceName, Namespace: serviceNamespace}, + existingArgoCD) + assertNoError(t, err) + assert.Assert(t, existingArgoCD.Spec.NodePlacement == nil) + _, hasManagedAnnotation := existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] + assert.Assert(t, !hasManagedAnnotation) } // If the DISABLE_DEFAULT_ARGOCD_INSTANCE is set, ensure that the default ArgoCD instance is not created. From 10b4739db96f30f58ba6a8dcf2d02f06b450a68f Mon Sep 17 00:00:00 2001 From: Pratik Langde Date: Mon, 7 Sep 2026 21:09:35 +0530 Subject: [PATCH 2/4] Mark GitopsService-managed NodePlacement on ArgoCD create path Set the managed-by annotation on defaultArgoCDInstance before create so clearing GitopsService placement later removes operator-applied placement for newly created instances too. Signed-off-by: Pratik Langde --- controllers/gitopsservice_controller.go | 9 +++- controllers/gitopsservice_controller_test.go | 46 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/controllers/gitopsservice_controller.go b/controllers/gitopsservice_controller.go index 2b5c619b0db..401d7ee9a62 100644 --- a/controllers/gitopsservice_controller.go +++ b/controllers/gitopsservice_controller.go @@ -555,6 +555,14 @@ func (r *ReconcileGitopsService) reconcileDefaultArgoCDInstance(instance *pipeli } } + gitopsServiceConfiguresNodePlacement := len(instance.Spec.NodeSelector) > 0 || len(instance.Spec.Tolerations) > 0 + if gitopsServiceConfiguresNodePlacement { + if defaultArgoCDInstance.Annotations == nil { + defaultArgoCDInstance.Annotations = map[string]string{} + } + defaultArgoCDInstance.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] = "true" + } + // Get or create ArgoCD instance in default namespace existingArgoCD := &argoapp.ArgoCD{} err = r.Client.Get(context.TODO(), types.NamespacedName{Name: defaultArgoCDInstance.Name, Namespace: defaultArgoCDInstance.Namespace}, existingArgoCD) @@ -622,7 +630,6 @@ func (r *ReconcileGitopsService) reconcileDefaultArgoCDInstance(instance *pipeli // Sync NodePlacement when GitopsService configures placement fields. Do not wipe NodePlacement // that admins set directly on the ArgoCD CR when GitopsService placement fields are empty. - gitopsServiceConfiguresNodePlacement := len(instance.Spec.NodeSelector) > 0 || len(instance.Spec.Tolerations) > 0 if gitopsServiceConfiguresNodePlacement { if defaultArgoCDInstance.Spec.NodePlacement != nil { if !reflect.DeepEqual(existingArgoCD.Spec.NodePlacement, defaultArgoCDInstance.Spec.NodePlacement) { diff --git a/controllers/gitopsservice_controller_test.go b/controllers/gitopsservice_controller_test.go index 1be5387a06a..a378433160b 100644 --- a/controllers/gitopsservice_controller_test.go +++ b/controllers/gitopsservice_controller_test.go @@ -395,6 +395,52 @@ func TestReconcileDefaultArgoCDNodePlacementClearsWhenGitopsServiceCleared(t *te assert.Assert(t, !hasManagedAnnotation) } +func TestReconcileDefaultArgoCDNodePlacementClearsWhenCreatedByReconcile(t *testing.T) { + logf.SetLogger(argocd.ZapLogger(true)) + s := scheme.Scheme + addKnownTypesToScheme(s) + + gitopsService := &pipelinesv1alpha1.GitopsService{ + ObjectMeta: v1.ObjectMeta{ + Name: serviceName, + }, + Spec: pipelinesv1alpha1.GitopsServiceSpec{ + NodeSelector: map[string]string{ + "key1": "value1", + }, + }, + } + + fakeClient := fake.NewFakeClient(gitopsService) + reconciler := newReconcileGitOpsService(fakeClient, s) + + _, err := reconciler.Reconcile(context.TODO(), newRequest("test", "test")) + assertNoError(t, err) + + existingArgoCD := &argoapp.ArgoCD{} + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: common.ArgoCDInstanceName, Namespace: serviceNamespace}, + existingArgoCD) + assertNoError(t, err) + assert.Check(t, existingArgoCD.Spec.NodePlacement != nil) + assert.Equal(t, existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation], "true") + + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: serviceName}, gitopsService) + assertNoError(t, err) + gitopsService.Spec.NodeSelector = nil + err = fakeClient.Update(context.TODO(), gitopsService) + assertNoError(t, err) + + _, err = reconciler.Reconcile(context.TODO(), newRequest("test", "test")) + assertNoError(t, err) + + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: common.ArgoCDInstanceName, Namespace: serviceNamespace}, + existingArgoCD) + assertNoError(t, err) + assert.Assert(t, existingArgoCD.Spec.NodePlacement == nil) + _, hasManagedAnnotation := existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] + assert.Assert(t, !hasManagedAnnotation) +} + // If the DISABLE_DEFAULT_ARGOCD_INSTANCE is set, ensure that the default ArgoCD instance is not created. func TestReconcileDisableDefault(t *testing.T) { From 2e595eebef5aeea4ff57f58abe2dde02956b42a2 Mon Sep 17 00:00:00 2001 From: Pratik Langde Date: Tue, 13 Oct 2026 23:54:37 +0530 Subject: [PATCH 3/4] OCPBUGS-120842: apply default-instance node selectors on the namespace GitOps Service owns the default Argo CD instance. Node selection for that instance uses the openshift-gitops openshift.io/node-selector annotation (runOnInfra and GitopsService.nodeSelector), not spec.nodePlacement.NodeSelector on the ArgoCD CR. Tolerations still sync through GitopsService onto the default instance. Signed-off-by: Pratik Langde --- common/common.go | 33 ++++- common/common_test.go | 36 +++++ controllers/gitopsservice_controller.go | 85 ++++-------- controllers/gitopsservice_controller_test.go | 128 +++--------------- .../1-071_validate_node_selectors_test.go | 41 ++++-- 5 files changed, 141 insertions(+), 182 deletions(-) create mode 100644 common/common_test.go diff --git a/common/common.go b/common/common.go index ada9bc7f3b3..1eb9372df4a 100644 --- a/common/common.go +++ b/common/common.go @@ -17,7 +17,11 @@ limitations under the License. package common import ( + "fmt" + "maps" "os" + "slices" + "strings" ) const ( @@ -48,10 +52,6 @@ const ( InfraNodeSelectorAnnotation = "openshift.io/node-selector" // InfraNodeSelectorAnnotationValue is the value for the infra node selector annotation InfraNodeSelectorAnnotationValue = "node-role.kubernetes.io/infra=" - // NodePlacementManagedByGitopsServiceAnnotation marks NodePlacement on the default ArgoCD CR - // that was applied by the GitopsService controller. Used to distinguish GitopsService-managed - // placement from admin edits on the ArgoCD CR directly. - NodePlacementManagedByGitopsServiceAnnotation = "gitops.openshift.io/node-placement-managed-by-gitopsservice" ) // InfraNodeSelector returns openshift label for infrastructure nodes @@ -61,6 +61,31 @@ func InfraNodeSelector() map[string]string { } } +// NamespaceNodeSelectorAnnotation builds the openshift.io/node-selector value for the +// default GitOps namespace. runOnInfra uses the infra role; GitopsService nodeSelector +// entries are merged. An empty result means the annotation should be removed. +func NamespaceNodeSelectorAnnotation(runOnInfra bool, nodeSelector map[string]string) string { + if !runOnInfra && len(nodeSelector) == 0 { + return "" + } + if runOnInfra && len(nodeSelector) == 0 { + return InfraNodeSelectorAnnotationValue + } + sel := maps.Clone(nodeSelector) + if sel == nil { + sel = map[string]string{} + } + if runOnInfra { + sel[InfraNodeLabelSelector] = "" + } + keys := slices.Sorted(maps.Keys(sel)) + parts := make([]string, 0, len(keys)) + for _, k := range keys { + parts = append(parts, fmt.Sprintf("%s=%s", k, sel[k])) + } + return strings.Join(parts, ",") +} + func StringFromEnv(env string, defaultValue string) string { if str := os.Getenv(env); str != "" { return str diff --git a/common/common_test.go b/common/common_test.go new file mode 100644 index 00000000000..0d6b8252d14 --- /dev/null +++ b/common/common_test.go @@ -0,0 +1,36 @@ +package common + +import "testing" + +func TestNamespaceNodeSelectorAnnotation(t *testing.T) { + tests := map[string]struct { + runOnInfra bool + nodeSelector map[string]string + want string + }{ + "neither": { + want: "", + }, + "runOnInfra only keeps exact infra value": { + runOnInfra: true, + want: InfraNodeSelectorAnnotationValue, + }, + "custom selector": { + nodeSelector: map[string]string{"foo": "bar"}, + want: "foo=bar", + }, + "runOnInfra plus custom merges": { + runOnInfra: true, + nodeSelector: map[string]string{"foo": "bar"}, + want: "foo=bar,node-role.kubernetes.io/infra=", + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got := NamespaceNodeSelectorAnnotation(tt.runOnInfra, tt.nodeSelector) + if got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + }) + } +} diff --git a/controllers/gitopsservice_controller.go b/controllers/gitopsservice_controller.go index 401d7ee9a62..a7f4971de8e 100644 --- a/controllers/gitopsservice_controller.go +++ b/controllers/gitopsservice_controller.go @@ -276,7 +276,7 @@ func (r *ReconcileGitopsService) Reconcile(ctx context.Context, request reconcil if err != nil { if errors.IsNotFound(err) { reqLogger.Info("Creating a new Namespace", "Name", namespace) - ensureInfraNodeSelectorAnnotation(namespaceRef, instance.Spec.RunOnInfra) + ensureInfraNodeSelectorAnnotation(namespaceRef, instance.Spec.RunOnInfra, instance.Spec.NodeSelector) err = r.Client.Create(ctx, namespaceRef) if err != nil { return reconcile.Result{}, err @@ -285,7 +285,7 @@ func (r *ReconcileGitopsService) Reconcile(ctx context.Context, request reconcil return reconcile.Result{}, err } } else { - if ensureNamespaceMetadata(namespaceRef, instance.Spec.RunOnInfra) { + if ensureNamespaceMetadata(namespaceRef, instance.Spec.RunOnInfra, instance.Spec.NodeSelector) { err = r.Client.Update(context.TODO(), namespaceRef) if err != nil { return reconcile.Result{}, err @@ -492,7 +492,7 @@ func (r *ReconcileGitopsService) reconcileDefaultArgoCDInstance(instance *pipeli if err != nil { if errors.IsNotFound(err) { reqLogger.Info("Creating a new Namespace", "Name", argocdNS.Name) - ensureInfraNodeSelectorAnnotation(argocdNS, instance.Spec.RunOnInfra) + ensureInfraNodeSelectorAnnotation(argocdNS, instance.Spec.RunOnInfra, instance.Spec.NodeSelector) err = r.Client.Create(context.TODO(), argocdNS) if err != nil { return reconcile.Result{}, err @@ -522,7 +522,7 @@ func (r *ReconcileGitopsService) reconcileDefaultArgoCDInstance(instance *pipeli } } - if ensureNamespaceMetadata(argocdNS, instance.Spec.RunOnInfra) { + if ensureNamespaceMetadata(argocdNS, instance.Spec.RunOnInfra, instance.Spec.NodeSelector) { err = r.Client.Update(context.TODO(), argocdNS) if err != nil { return reconcile.Result{}, err @@ -536,15 +536,9 @@ func (r *ReconcileGitopsService) reconcileDefaultArgoCDInstance(instance *pipeli return reconcile.Result{}, err } - if len(instance.Spec.NodeSelector) > 0 { - if defaultArgoCDInstance.Spec.NodePlacement == nil { - defaultArgoCDInstance.Spec.NodePlacement = &argoapp.ArgoCDNodePlacementSpec{ - NodeSelector: instance.Spec.NodeSelector, - } - } else { - defaultArgoCDInstance.Spec.NodePlacement.NodeSelector = argocdutil.AppendStringMap(defaultArgoCDInstance.Spec.NodePlacement.NodeSelector, instance.Spec.NodeSelector) - } - } + // Node selectors for the default instance are applied on the openshift-gitops + // namespace (openshift.io/node-selector), not on ArgoCD spec.nodePlacement. + // GitopsService still owns NodePlacement.Tolerations for taints. if len(instance.Spec.Tolerations) > 0 { if defaultArgoCDInstance.Spec.NodePlacement == nil { defaultArgoCDInstance.Spec.NodePlacement = &argoapp.ArgoCDNodePlacementSpec{ @@ -555,14 +549,6 @@ func (r *ReconcileGitopsService) reconcileDefaultArgoCDInstance(instance *pipeli } } - gitopsServiceConfiguresNodePlacement := len(instance.Spec.NodeSelector) > 0 || len(instance.Spec.Tolerations) > 0 - if gitopsServiceConfiguresNodePlacement { - if defaultArgoCDInstance.Annotations == nil { - defaultArgoCDInstance.Annotations = map[string]string{} - } - defaultArgoCDInstance.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] = "true" - } - // Get or create ArgoCD instance in default namespace existingArgoCD := &argoapp.ArgoCD{} err = r.Client.Get(context.TODO(), types.NamespacedName{Name: defaultArgoCDInstance.Name, Namespace: defaultArgoCDInstance.Namespace}, existingArgoCD) @@ -628,32 +614,15 @@ func (r *ReconcileGitopsService) reconcileDefaultArgoCDInstance(instance *pipeli changed = true } - // Sync NodePlacement when GitopsService configures placement fields. Do not wipe NodePlacement - // that admins set directly on the ArgoCD CR when GitopsService placement fields are empty. - if gitopsServiceConfiguresNodePlacement { - if defaultArgoCDInstance.Spec.NodePlacement != nil { - if !reflect.DeepEqual(existingArgoCD.Spec.NodePlacement, defaultArgoCDInstance.Spec.NodePlacement) { - existingArgoCD.Spec.NodePlacement = defaultArgoCDInstance.Spec.NodePlacement - changed = true - } - } else if existingArgoCD.Spec.NodePlacement != nil { + // GitopsService owns the default ArgoCD instance. Sync NodePlacement from + // GitopsService (tolerations only; node selectors use the namespace annotation). + if defaultArgoCDInstance.Spec.NodePlacement != nil { + if !reflect.DeepEqual(existingArgoCD.Spec.NodePlacement, defaultArgoCDInstance.Spec.NodePlacement) { existingArgoCD.Spec.NodePlacement = defaultArgoCDInstance.Spec.NodePlacement changed = true } - if existingArgoCD.Annotations == nil { - existingArgoCD.Annotations = map[string]string{} - } - if existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] != "true" { - existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] = "true" - changed = true - } - } else if existingArgoCD.Annotations != nil && - existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] == "true" { - if existingArgoCD.Spec.NodePlacement != nil { - existingArgoCD.Spec.NodePlacement = nil - changed = true - } - delete(existingArgoCD.Annotations, common.NodePlacementManagedByGitopsServiceAnnotation) + } else if existingArgoCD.Spec.NodePlacement != nil { + existingArgoCD.Spec.NodePlacement = defaultArgoCDInstance.Spec.NodePlacement changed = true } @@ -1158,11 +1127,11 @@ func policyRuleForBackendServiceClusterRole() []rbacv1.PolicyRule { } } -func ensureNamespaceMetadata(namespace *corev1.Namespace, runOnInfra bool) bool { +func ensureNamespaceMetadata(namespace *corev1.Namespace, runOnInfra bool, nodeSelector map[string]string) bool { changed := false labelUpdate, _ := ensurePodSecurityLabels(namespace) changed = changed || labelUpdate - annotationUpdate, _ := ensureInfraNodeSelectorAnnotation(namespace, runOnInfra) + annotationUpdate, _ := ensureInfraNodeSelectorAnnotation(namespace, runOnInfra, nodeSelector) changed = changed || annotationUpdate return changed } @@ -1178,20 +1147,22 @@ func ensurePodSecurityLabels(namespace *corev1.Namespace) (bool, *corev1.Namespa return false, namespace } -func ensureInfraNodeSelectorAnnotation(namespace *corev1.Namespace, runOnInfra bool) (bool, *corev1.Namespace) { +func ensureInfraNodeSelectorAnnotation(namespace *corev1.Namespace, runOnInfra bool, nodeSelector map[string]string) (bool, *corev1.Namespace) { if namespace.Annotations == nil { namespace.Annotations = make(map[string]string) } - if runOnInfra { - if namespace.Annotations[common.InfraNodeSelectorAnnotation] != common.InfraNodeSelectorAnnotationValue { - namespace.Annotations[common.InfraNodeSelectorAnnotation] = common.InfraNodeSelectorAnnotationValue - return true, namespace - } - } else { - if namespace.Annotations[common.InfraNodeSelectorAnnotation] == common.InfraNodeSelectorAnnotationValue { - delete(namespace.Annotations, common.InfraNodeSelectorAnnotation) - return true, namespace + want := common.NamespaceNodeSelectorAnnotation(runOnInfra, nodeSelector) + got := namespace.Annotations[common.InfraNodeSelectorAnnotation] + if want == "" { + if got == "" { + return false, namespace } + delete(namespace.Annotations, common.InfraNodeSelectorAnnotation) + return true, namespace } - return false, namespace + if got == want { + return false, namespace + } + namespace.Annotations[common.InfraNodeSelectorAnnotation] = want + return true, namespace } diff --git a/controllers/gitopsservice_controller_test.go b/controllers/gitopsservice_controller_test.go index a378433160b..3d46bdfaa4f 100644 --- a/controllers/gitopsservice_controller_test.go +++ b/controllers/gitopsservice_controller_test.go @@ -219,8 +219,6 @@ func TestReconcileDefaultForArgoCDNodeplacement(t *testing.T) { s := scheme.Scheme addKnownTypesToScheme(s) - var err error - gitopsService := &pipelinesv1alpha1.GitopsService{ ObjectMeta: v1.ObjectMeta{ Name: serviceName, @@ -256,80 +254,22 @@ func TestReconcileDefaultForArgoCDNodeplacement(t *testing.T) { }, } - err = fakeClient.Create(context.TODO(), existingArgoCD) + err := fakeClient.Create(context.TODO(), existingArgoCD) assertNoError(t, err) _, err = reconciler.Reconcile(context.TODO(), newRequest("test", "test")) assertNoError(t, err) - // verify whether existingArgoCD NodePlacement is updated when user is patching nodePlacement through GitopsService CR err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: common.ArgoCDInstanceName, Namespace: serviceNamespace}, existingArgoCD) assertNoError(t, err) - assert.Check(t, existingArgoCD.Spec.NodePlacement != nil) - assert.DeepEqual(t, existingArgoCD.Spec.NodePlacement.NodeSelector, gitopsService.Spec.NodeSelector) - assert.Equal(t, existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation], "true") -} - -func TestReconcileDefaultArgoCDNodePlacementPreservesDirectEdit(t *testing.T) { - logf.SetLogger(argocd.ZapLogger(true)) - s := scheme.Scheme - addKnownTypesToScheme(s) - - gitopsService := &pipelinesv1alpha1.GitopsService{ - ObjectMeta: v1.ObjectMeta{ - Name: serviceName, - }, - } - - directNodePlacement := &argoapp.ArgoCDNodePlacementSpec{ - NodeSelector: map[string]string{ - "machine.openshift.io/cluster-api-machineset": "cl01-infra-0", - }, - Tolerations: []corev1.Toleration{{ - Effect: corev1.TaintEffectNoSchedule, - Key: "infra", - Value: "reserved", - }}, - } - - fakeClient := fake.NewFakeClient(gitopsService) - reconciler := newReconcileGitOpsService(fakeClient, s) - - existingArgoCD := &argoapp.ArgoCD{ - ObjectMeta: v1.ObjectMeta{ - Name: serviceNamespace, - Namespace: serviceNamespace, - }, - Spec: argoapp.ArgoCDSpec{ - NodePlacement: directNodePlacement, - Server: argoapp.ArgoCDServerSpec{ - Route: argoapp.ArgoCDRouteSpec{ - Enabled: true, - }, - }, - ApplicationSet: &argoapp.ArgoCDApplicationSet{}, - SSO: &argoapp.ArgoCDSSOSpec{ - Provider: "dex", - Dex: &argoapp.ArgoCDDexSpec{ - Config: "test-config", - }, - }, - }, - } - - err := fakeClient.Create(context.TODO(), existingArgoCD) - assertNoError(t, err) - - _, err = reconciler.Reconcile(context.TODO(), newRequest("test", "test")) - assertNoError(t, err) + // Node selectors are applied on the namespace, not on the default ArgoCD CR. + assert.Assert(t, existingArgoCD.Spec.NodePlacement == nil) - err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: common.ArgoCDInstanceName, Namespace: serviceNamespace}, - existingArgoCD) + ns := &corev1.Namespace{} + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: serviceNamespace}, ns) assertNoError(t, err) - assert.DeepEqual(t, existingArgoCD.Spec.NodePlacement, directNodePlacement) - _, hasManagedAnnotation := existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] - assert.Assert(t, !hasManagedAnnotation) + assert.Equal(t, ns.Annotations[common.InfraNodeSelectorAnnotation], "foo=bar") } func TestReconcileDefaultArgoCDNodePlacementClearsWhenGitopsServiceCleared(t *testing.T) { @@ -345,6 +285,7 @@ func TestReconcileDefaultArgoCDNodePlacementClearsWhenGitopsServiceCleared(t *te NodeSelector: map[string]string{ "key1": "value1", }, + Tolerations: deploymentDefaultTolerations(), }, } @@ -378,55 +319,22 @@ func TestReconcileDefaultArgoCDNodePlacementClearsWhenGitopsServiceCleared(t *te _, err = reconciler.Reconcile(context.TODO(), newRequest("test", "test")) assertNoError(t, err) - err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: serviceName}, gitopsService) - assertNoError(t, err) - gitopsService.Spec.NodeSelector = nil - err = fakeClient.Update(context.TODO(), gitopsService) - assertNoError(t, err) - - _, err = reconciler.Reconcile(context.TODO(), newRequest("test", "test")) - assertNoError(t, err) - err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: common.ArgoCDInstanceName, Namespace: serviceNamespace}, existingArgoCD) assertNoError(t, err) - assert.Assert(t, existingArgoCD.Spec.NodePlacement == nil) - _, hasManagedAnnotation := existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] - assert.Assert(t, !hasManagedAnnotation) -} - -func TestReconcileDefaultArgoCDNodePlacementClearsWhenCreatedByReconcile(t *testing.T) { - logf.SetLogger(argocd.ZapLogger(true)) - s := scheme.Scheme - addKnownTypesToScheme(s) - - gitopsService := &pipelinesv1alpha1.GitopsService{ - ObjectMeta: v1.ObjectMeta{ - Name: serviceName, - }, - Spec: pipelinesv1alpha1.GitopsServiceSpec{ - NodeSelector: map[string]string{ - "key1": "value1", - }, - }, - } - - fakeClient := fake.NewFakeClient(gitopsService) - reconciler := newReconcileGitOpsService(fakeClient, s) - - _, err := reconciler.Reconcile(context.TODO(), newRequest("test", "test")) - assertNoError(t, err) + assert.Check(t, existingArgoCD.Spec.NodePlacement != nil) + assert.Assert(t, existingArgoCD.Spec.NodePlacement.NodeSelector == nil) + assert.DeepEqual(t, existingArgoCD.Spec.NodePlacement.Tolerations, deploymentDefaultTolerations()) - existingArgoCD := &argoapp.ArgoCD{} - err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: common.ArgoCDInstanceName, Namespace: serviceNamespace}, - existingArgoCD) + ns := &corev1.Namespace{} + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: serviceNamespace}, ns) assertNoError(t, err) - assert.Check(t, existingArgoCD.Spec.NodePlacement != nil) - assert.Equal(t, existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation], "true") + assert.Equal(t, ns.Annotations[common.InfraNodeSelectorAnnotation], "key1=value1") err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: serviceName}, gitopsService) assertNoError(t, err) gitopsService.Spec.NodeSelector = nil + gitopsService.Spec.Tolerations = nil err = fakeClient.Update(context.TODO(), gitopsService) assertNoError(t, err) @@ -437,8 +345,12 @@ func TestReconcileDefaultArgoCDNodePlacementClearsWhenCreatedByReconcile(t *test existingArgoCD) assertNoError(t, err) assert.Assert(t, existingArgoCD.Spec.NodePlacement == nil) - _, hasManagedAnnotation := existingArgoCD.Annotations[common.NodePlacementManagedByGitopsServiceAnnotation] - assert.Assert(t, !hasManagedAnnotation) + + ns = &corev1.Namespace{} + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: serviceNamespace}, ns) + assertNoError(t, err) + _, hasAnnotation := ns.Annotations[common.InfraNodeSelectorAnnotation] + assert.Assert(t, !hasAnnotation) } // If the DISABLE_DEFAULT_ARGOCD_INSTANCE is set, ensure that the default ArgoCD instance is not created. diff --git a/test/openshift/e2e/ginkgo/sequential/1-071_validate_node_selectors_test.go b/test/openshift/e2e/ginkgo/sequential/1-071_validate_node_selectors_test.go index 7e7556aa921..c2655091863 100644 --- a/test/openshift/e2e/ginkgo/sequential/1-071_validate_node_selectors_test.go +++ b/test/openshift/e2e/ginkgo/sequential/1-071_validate_node_selectors_test.go @@ -68,19 +68,27 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() { gs.Spec.NodeSelector["key1"] = "value1" }) - By("ensuring Deployments and StatefulSets pick up the change we made to nodeSelector in GitOpsService CR") + By("ensuring the openshift-gitops namespace annotation picks up GitOpsService nodeSelector") + openshiftGitopsNS := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "openshift-gitops"}} + Eventually(openshiftGitopsNS).Should(namespaceFixture.HaveAnnotation(common.InfraNodeSelectorAnnotation, "key1=value1")) - for _, deploymentName := range deploymentNameList { + By("ensuring the GitOps Service backend Deployment still has nodeSelector on the pod template") + clusterDepl := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "cluster", Namespace: "openshift-gitops"}} + Eventually(clusterDepl).Should(deploymentFixture.HaveTemplateSpecNodeSelector(map[string]string{ + "kubernetes.io/os": "linux", + "key1": "value1", + })) + + By("ensuring Argo CD workload templates keep the default nodeSelector (namespace annotation schedules them)") + argoCDDeploymentNames := []string{"openshift-gitops-server", "openshift-gitops-repo-server", "openshift-gitops-dex-server", "openshift-gitops-redis"} + for _, deploymentName := range argoCDDeploymentNames { depl := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: deploymentName, Namespace: "openshift-gitops"}} Eventually(depl).Should(deploymentFixture.HaveTemplateSpecNodeSelector(map[string]string{ "kubernetes.io/os": "linux", - "key1": "value1", })) } - Eventually(appControllerSS).Should(statefulsetFixture.HaveTemplateSpecNodeSelector(map[string]string{ "kubernetes.io/os": "linux", - "key1": "value1", })) By("enabling runOnInfra and setting various tolerations on GitOpsService") @@ -93,17 +101,26 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() { Value: "reserved"}} }) - By("ensuring the openshift-gitops namespace has the infra node-selector annotation") - openshiftGitopsNS := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "openshift-gitops"}} - Eventually(openshiftGitopsNS).Should(namespaceFixture.HaveAnnotation(common.InfraNodeSelectorAnnotation, common.InfraNodeSelectorAnnotationValue)) + By("ensuring the openshift-gitops namespace annotation merges infra and custom selectors") + Eventually(openshiftGitopsNS).Should(namespaceFixture.HaveAnnotation(common.InfraNodeSelectorAnnotation, + common.NamespaceNodeSelectorAnnotation(true, map[string]string{"key1": "value1"}))) - By("ensuring Deployments and StatefulSets still have the custom nodeSelector and tolerations") + By("ensuring the GitOps Service backend Deployment still has the custom nodeSelector and tolerations") + Eventually(clusterDepl).Should(deploymentFixture.HaveTemplateSpecNodeSelector(map[string]string{ + "kubernetes.io/os": "linux", + "key1": "value1", + })) + Eventually(clusterDepl).Should(deploymentFixture.HaveTolerations([]corev1.Toleration{{ + Effect: "NoSchedule", + Key: "infra", + Value: "reserved", + }})) - for _, deploymentName := range deploymentNameList { + By("ensuring Argo CD workloads get GitopsService tolerations, with node selection via the namespace annotation") + for _, deploymentName := range argoCDDeploymentNames { depl := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: deploymentName, Namespace: "openshift-gitops"}} Eventually(depl).Should(deploymentFixture.HaveTemplateSpecNodeSelector(map[string]string{ "kubernetes.io/os": "linux", - "key1": "value1", })) Eventually(depl).Should(deploymentFixture.HaveTolerations([]corev1.Toleration{{ Effect: "NoSchedule", @@ -111,10 +128,8 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() { Value: "reserved", }})) } - Eventually(appControllerSS).Should(statefulsetFixture.HaveTemplateSpecNodeSelector(map[string]string{ "kubernetes.io/os": "linux", - "key1": "value1", })) Eventually(appControllerSS).Should(statefulsetFixture.HaveTolerations([]corev1.Toleration{{ Effect: "NoSchedule", From c7356ed212844719973d380bbd9210cd986478c8 Mon Sep 17 00:00:00 2001 From: Pratik Langde Date: Mon, 19 Oct 2026 14:42:58 +0530 Subject: [PATCH 4/4] OCPBUGS-120842: roll default-instance workloads on node-selector change Namespace openshift.io/node-selector only affects newly admitted pods. When GitopsService runOnInfra or nodeSelector changes that annotation, restart existing Deployments and StatefulSets so running pods move. Signed-off-by: Pratik Langde --- controllers/gitopsservice_controller.go | 71 ++++++++++++++++++-- controllers/gitopsservice_controller_test.go | 53 +++++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/controllers/gitopsservice_controller.go b/controllers/gitopsservice_controller.go index a7f4971de8e..714a1f24290 100644 --- a/controllers/gitopsservice_controller.go +++ b/controllers/gitopsservice_controller.go @@ -23,6 +23,7 @@ import ( "os" "reflect" "strings" + "time" "github.com/go-logr/logr" version "github.com/hashicorp/go-version" @@ -79,6 +80,10 @@ const ( PodSecurityLabelSyncLabel = "security.openshift.io/scc.podSecurityLabelSync" PodSecurityLabelSyncLabelValue = "true" kamResourceName = "kam" + // nodeSelectorRestartedAtAnnotation is bumped on default-instance workloads when + // openshift.io/node-selector on the namespace changes, so existing pods are replaced + // and pick up the new selector at admission. + nodeSelectorRestartedAtAnnotation = "gitops.openshift.io/node-selector-restartedAt" ) // SetupWithManager sets up the controller with the Manager. @@ -285,12 +290,18 @@ func (r *ReconcileGitopsService) Reconcile(ctx context.Context, request reconcil return reconcile.Result{}, err } } else { - if ensureNamespaceMetadata(namespaceRef, instance.Spec.RunOnInfra, instance.Spec.NodeSelector) { + changed, selectorChanged := ensureNamespaceMetadata(namespaceRef, instance.Spec.RunOnInfra, instance.Spec.NodeSelector) + if changed { err = r.Client.Update(context.TODO(), namespaceRef) if err != nil { return reconcile.Result{}, err } } + if selectorChanged { + if err := r.restartWorkloadsForNodeSelector(namespaceRef.Name, reqLogger); err != nil { + return reconcile.Result{}, err + } + } } // Create/reconcile the default Argo CD instance @@ -522,12 +533,18 @@ func (r *ReconcileGitopsService) reconcileDefaultArgoCDInstance(instance *pipeli } } - if ensureNamespaceMetadata(argocdNS, instance.Spec.RunOnInfra, instance.Spec.NodeSelector) { + changed, selectorChanged := ensureNamespaceMetadata(argocdNS, instance.Spec.RunOnInfra, instance.Spec.NodeSelector) + if changed { err = r.Client.Update(context.TODO(), argocdNS) if err != nil { return reconcile.Result{}, err } } + if selectorChanged { + if err := r.restartWorkloadsForNodeSelector(argocdNS.Name, reqLogger); err != nil { + return reconcile.Result{}, err + } + } } @@ -1127,13 +1144,53 @@ func policyRuleForBackendServiceClusterRole() []rbacv1.PolicyRule { } } -func ensureNamespaceMetadata(namespace *corev1.Namespace, runOnInfra bool, nodeSelector map[string]string) bool { - changed := false +func ensureNamespaceMetadata(namespace *corev1.Namespace, runOnInfra bool, nodeSelector map[string]string) (changed bool, nodeSelectorChanged bool) { labelUpdate, _ := ensurePodSecurityLabels(namespace) - changed = changed || labelUpdate annotationUpdate, _ := ensureInfraNodeSelectorAnnotation(namespace, runOnInfra, nodeSelector) - changed = changed || annotationUpdate - return changed + return labelUpdate || annotationUpdate, annotationUpdate +} + +func (r *ReconcileGitopsService) restartWorkloadsForNodeSelector(namespace string, reqLogger logr.Logger) error { + now := time.Now().UTC().Format(time.RFC3339) + reqLogger.Info("Restarting workloads so pods pick up namespace node-selector changes", "Namespace", namespace) + + deploys := &appsv1.DeploymentList{} + if err := r.Client.List(context.TODO(), deploys, client.InNamespace(namespace)); err != nil { + return err + } + for i := range deploys.Items { + d := &deploys.Items[i] + // Backend deployment already rolls via its own pod-template nodeSelector sync. + if d.Name == serviceName { + continue + } + if err := r.patchPodTemplateRestartedAt(d, &d.Spec.Template, now); err != nil { + return err + } + } + + statefuls := &appsv1.StatefulSetList{} + if err := r.Client.List(context.TODO(), statefuls, client.InNamespace(namespace)); err != nil { + return err + } + for i := range statefuls.Items { + ss := &statefuls.Items[i] + if err := r.patchPodTemplateRestartedAt(ss, &ss.Spec.Template, now); err != nil { + return err + } + } + return nil +} + +func (r *ReconcileGitopsService) patchPodTemplateRestartedAt(obj client.Object, template *corev1.PodTemplateSpec, now string) error { + if template.Annotations == nil { + template.Annotations = map[string]string{} + } + if template.Annotations[nodeSelectorRestartedAtAnnotation] == now { + return nil + } + template.Annotations[nodeSelectorRestartedAtAnnotation] = now + return r.Client.Update(context.TODO(), obj) } func ensurePodSecurityLabels(namespace *corev1.Namespace) (bool, *corev1.Namespace) { diff --git a/controllers/gitopsservice_controller_test.go b/controllers/gitopsservice_controller_test.go index 3d46bdfaa4f..f1f02c74501 100644 --- a/controllers/gitopsservice_controller_test.go +++ b/controllers/gitopsservice_controller_test.go @@ -985,6 +985,59 @@ func TestReconcile_InfrastructureNode_AnnotationRemoved(t *testing.T) { assert.Assert(t, !hasAnnotation) } +func TestReconcile_InfrastructureNode_RestartsExistingWorkloads(t *testing.T) { + logf.SetLogger(argocd.ZapLogger(true)) + s := scheme.Scheme + addKnownTypesToScheme(s) + + ns := &corev1.Namespace{ObjectMeta: v1.ObjectMeta{Name: serviceNamespace}} + gitopsService := &pipelinesv1alpha1.GitopsService{ + ObjectMeta: v1.ObjectMeta{Name: serviceName}, + Spec: pipelinesv1alpha1.GitopsServiceSpec{ + RunOnInfra: true, + }, + } + server := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{Name: "openshift-gitops-server", Namespace: serviceNamespace}, + Spec: appsv1.DeploymentSpec{ + Selector: &v1.LabelSelector{MatchLabels: map[string]string{"app": "server"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: v1.ObjectMeta{Labels: map[string]string{"app": "server"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "server", Image: "test"}}}, + }, + }, + } + controller := &appsv1.StatefulSet{ + ObjectMeta: v1.ObjectMeta{Name: "openshift-gitops-application-controller", Namespace: serviceNamespace}, + Spec: appsv1.StatefulSetSpec{ + Selector: &v1.LabelSelector{MatchLabels: map[string]string{"app": "controller"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: v1.ObjectMeta{Labels: map[string]string{"app": "controller"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "controller", Image: "test"}}}, + }, + }, + } + + fakeClient := fake.NewFakeClient(gitopsService, ns, server, controller) + reconciler := newReconcileGitOpsService(fakeClient, s) + + _, err := reconciler.Reconcile(context.TODO(), newRequest("test", "test")) + assertNoError(t, err) + + updatedNS := &corev1.Namespace{} + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: serviceNamespace}, updatedNS) + assertNoError(t, err) + assert.Equal(t, updatedNS.Annotations[common.InfraNodeSelectorAnnotation], common.InfraNodeSelectorAnnotationValue) + + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: server.Name, Namespace: serviceNamespace}, server) + assertNoError(t, err) + assert.Assert(t, server.Spec.Template.Annotations[nodeSelectorRestartedAtAnnotation] != "") + + err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: controller.Name, Namespace: serviceNamespace}, controller) + assertNoError(t, err) + assert.Assert(t, controller.Spec.Template.Annotations[nodeSelectorRestartedAtAnnotation] != "") +} + func TestReconcile_PSSLabels(t *testing.T) { logf.SetLogger(argocd.ZapLogger(true)) s := scheme.Scheme