From 4632d31c0dbe6c811ca03b6ad902141f44d96ed8 Mon Sep 17 00:00:00 2001 From: Vishnu Kothakapu Date: Wed, 5 Aug 2026 01:51:04 +0530 Subject: [PATCH] feat: add suspend field to NodeReadinessRuleSpec to pause rule evaluation --- api/v1alpha1/nodereadinessrule_types.go | 6 +++ ...ness.node.x-k8s.io_nodereadinessrules.yaml | 8 ++++ .../nodereadinessrule_controller.go | 5 +++ .../nodereadinessrule_controller_test.go | 40 +++++++++++++++++++ 4 files changed, 59 insertions(+) diff --git a/api/v1alpha1/nodereadinessrule_types.go b/api/v1alpha1/nodereadinessrule_types.go index 5e86ef44..5c820d8c 100644 --- a/api/v1alpha1/nodereadinessrule_types.go +++ b/api/v1alpha1/nodereadinessrule_types.go @@ -104,6 +104,11 @@ type NodeReadinessRuleSpec struct { // // +optional DryRun bool `json:"dryRun,omitempty"` //nolint:kubeapilinter + + // suspend tells the controller to suspend operations for this NodeReadinessRule. + // + // +optional + Suspend bool `json:"suspend,omitempty"` } // ConditionRequirement defines a specific Node condition and the status value @@ -327,6 +332,7 @@ type DryRunResults struct { // +kubebuilder:printcolumn:name="Effect",type=string,JSONPath=`.spec.taint.effect`,description="The taint effect: NoSchedule, PreferNoSchedule or NoExecute." // +kubebuilder:printcolumn:name="DryRun",type=boolean,JSONPath=`.spec.dryRun`,description="Whether the rule is in dry-run mode and only previews taint changes." // +kubebuilder:selectablefield:JSONPath=`.spec.dryRun` +// +kubebuilder:printcolumn:name="Suspend",type=boolean,JSONPath=`.spec.suspend`,description="Whether operations for this rule are suspended." // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`,description="The age of this resource" // NodeReadinessRule is the Schema for the NodeReadinessRules API. diff --git a/config/crd/bases/readiness.node.x-k8s.io_nodereadinessrules.yaml b/config/crd/bases/readiness.node.x-k8s.io_nodereadinessrules.yaml index f0850bc3..c7393cb9 100644 --- a/config/crd/bases/readiness.node.x-k8s.io_nodereadinessrules.yaml +++ b/config/crd/bases/readiness.node.x-k8s.io_nodereadinessrules.yaml @@ -33,6 +33,10 @@ spec: jsonPath: .spec.dryRun name: DryRun type: boolean + - description: Whether operations for this rule are suspended. + jsonPath: .spec.suspend + name: Suspend + type: boolean - description: The age of this resource jsonPath: .metadata.creationTimestamp name: Age @@ -185,6 +189,10 @@ spec: x-kubernetes-validations: - message: nodeSelector is immutable rule: self == oldSelf + suspend: + description: suspend tells the controller to suspend operations for + this NodeReadinessRule. + type: boolean taint: description: |- taint defines the specific Taint (Key, Value, and Effect) to be managed diff --git a/internal/controller/nodereadinessrule_controller.go b/internal/controller/nodereadinessrule_controller.go index f51d7187..602b27c1 100644 --- a/internal/controller/nodereadinessrule_controller.go +++ b/internal/controller/nodereadinessrule_controller.go @@ -115,6 +115,11 @@ func (r *RuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. log = log.WithValues("ruleName", rule.Name) ctx = ctrl.LoggerInto(ctx, log) + if rule.Spec.Suspend { + log.Info("NodeReadinessRule is suspended, skipping reconciliation") + return ctrl.Result{}, nil + } + // Add finalizer first if not set to avoid the race condition between init and delete. if finalizerAdded, err := r.ensureFinalizer(ctx, rule, finalizerName); err != nil { return ctrl.Result{}, err diff --git a/internal/controller/nodereadinessrule_controller_test.go b/internal/controller/nodereadinessrule_controller_test.go index b551ebb3..3a72232e 100644 --- a/internal/controller/nodereadinessrule_controller_test.go +++ b/internal/controller/nodereadinessrule_controller_test.go @@ -107,6 +107,46 @@ var _ = Describe("NodeReadinessRule Controller", func() { }) Context("Rule Reconciliation", func() { + It("should skip reconciliation when rule is suspended", func() { + rule := &nodereadinessiov1alpha1.NodeReadinessRule{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rule-suspend", + }, + Spec: nodereadinessiov1alpha1.NodeReadinessRuleSpec{ + Suspend: true, + Conditions: []nodereadinessiov1alpha1.ConditionRequirement{ + {Type: "Ready", RequiredStatus: corev1.ConditionTrue}, + }, + NodeSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "node-role.kubernetes.io/worker": "", + }, + }, + Taint: corev1.Taint{ + Key: "readiness.k8s.io/test-taint", + Effect: corev1.TaintEffectNoSchedule, + }, + EnforcementMode: nodereadinessiov1alpha1.EnforcementModeContinuous, + }, + } + + Expect(k8sClient.Create(ctx, rule)).To(Succeed()) + + res, err := ruleReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: types.NamespacedName{Name: "test-rule-suspend"}, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(reconcile.Result{})) + + // Verify finalizer is NOT added to the rule + updatedRule := &nodereadinessiov1alpha1.NodeReadinessRule{} + Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "test-rule-suspend"}, updatedRule)).To(Succeed()) + Expect(updatedRule.Finalizers).To(BeEmpty()) + + // Cleanup + Expect(k8sClient.Delete(ctx, rule)).To(Succeed()) + }) + It("should handle rule creation and add the finalizer to the rule", func() { rule := &nodereadinessiov1alpha1.NodeReadinessRule{ ObjectMeta: metav1.ObjectMeta{