Skip to content

Commit 65a81b8

Browse files
Add node events for taint operations (TaintAdded, TaintRemoved, TaintAdopted) (#158)
1 parent 87fe459 commit 65a81b8

File tree

6 files changed

+201
-30
lines changed

6 files changed

+201
-30
lines changed

config/rbac/role.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ kind: ClusterRole
44
metadata:
55
name: manager-role
66
rules:
7+
- apiGroups:
8+
- ""
9+
resources:
10+
- events
11+
verbs:
12+
- create
13+
- patch
714
- apiGroups:
815
- ""
916
resources:

internal/controller/node_controller.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,14 +242,21 @@ func (r *RuleReadinessController) hasTaintBySpec(node *corev1.Node, taintSpec co
242242
}
243243

244244
// addTaintBySpec adds a taint to a node.
245-
func (r *RuleReadinessController) addTaintBySpec(ctx context.Context, node *corev1.Node, taintSpec corev1.Taint) error {
245+
func (r *RuleReadinessController) addTaintBySpec(ctx context.Context, node *corev1.Node, taintSpec corev1.Taint, ruleName string) error {
246246
patch := client.StrategicMergeFrom(node.DeepCopy())
247247
node.Spec.Taints = append(node.Spec.Taints, taintSpec)
248-
return r.Patch(ctx, node, patch)
248+
if err := r.Patch(ctx, node, patch); err != nil {
249+
return err
250+
}
251+
252+
message := fmt.Sprintf("Taint '%s:%s' added by rule '%s'", taintSpec.Key, taintSpec.Effect, ruleName)
253+
r.EventRecorder.Event(node, corev1.EventTypeNormal, "TaintAdded", message)
254+
255+
return nil
249256
}
250257

251258
// removeTaintBySpec removes a taint from a node.
252-
func (r *RuleReadinessController) removeTaintBySpec(ctx context.Context, node *corev1.Node, taintSpec corev1.Taint) error {
259+
func (r *RuleReadinessController) removeTaintBySpec(ctx context.Context, node *corev1.Node, taintSpec corev1.Taint, ruleName string) error {
253260
patch := client.StrategicMergeFrom(node.DeepCopy())
254261
var newTaints []corev1.Taint
255262
for _, taint := range node.Spec.Taints {
@@ -258,7 +265,14 @@ func (r *RuleReadinessController) removeTaintBySpec(ctx context.Context, node *c
258265
}
259266
}
260267
node.Spec.Taints = newTaints
261-
return r.Patch(ctx, node, patch)
268+
if err := r.Patch(ctx, node, patch); err != nil {
269+
return err
270+
}
271+
272+
message := fmt.Sprintf("Taint '%s:%s' removed by rule '%s'", taintSpec.Key, taintSpec.Effect, ruleName)
273+
r.EventRecorder.Event(node, corev1.EventTypeNormal, "TaintRemoved", message)
274+
275+
return nil
262276
}
263277

264278
// Bootstrap completion tracking.

internal/controller/node_controller_test.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2828
"k8s.io/apimachinery/pkg/types"
2929
"k8s.io/client-go/kubernetes/fake"
30+
"k8s.io/client-go/tools/record"
3031
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3132

3233
nodereadinessiov1alpha1 "sigs.k8s.io/node-readiness-controller/api/v1alpha1"
@@ -115,10 +116,11 @@ var _ = Describe("Node Controller", func() {
115116

116117
fakeClientset = fake.NewSimpleClientset()
117118
readinessController = &RuleReadinessController{
118-
Client: k8sClient,
119-
Scheme: k8sClient.Scheme(),
120-
clientset: fakeClientset,
121-
ruleCache: make(map[string]*nodereadinessiov1alpha1.NodeReadinessRule),
119+
Client: k8sClient,
120+
Scheme: k8sClient.Scheme(),
121+
clientset: fakeClientset,
122+
ruleCache: make(map[string]*nodereadinessiov1alpha1.NodeReadinessRule),
123+
EventRecorder: record.NewFakeRecorder(10),
122124
}
123125

124126
nodeReconciler = &NodeReconciler{
@@ -377,10 +379,11 @@ var _ = Describe("Node Controller", func() {
377379

378380
fakeClientset = fake.NewSimpleClientset()
379381
readinessController = &RuleReadinessController{
380-
Client: k8sClient,
381-
Scheme: k8sClient.Scheme(),
382-
clientset: fakeClientset,
383-
ruleCache: make(map[string]*nodereadinessiov1alpha1.NodeReadinessRule),
382+
Client: k8sClient,
383+
Scheme: k8sClient.Scheme(),
384+
clientset: fakeClientset,
385+
ruleCache: make(map[string]*nodereadinessiov1alpha1.NodeReadinessRule),
386+
EventRecorder: record.NewFakeRecorder(10),
384387
}
385388

386389
nodeReconciler = &NodeReconciler{
@@ -526,10 +529,11 @@ var _ = Describe("Node Controller", func() {
526529

527530
fakeClientset = fake.NewSimpleClientset()
528531
readinessController = &RuleReadinessController{
529-
Client: k8sClient,
530-
Scheme: k8sClient.Scheme(),
531-
clientset: fakeClientset,
532-
ruleCache: make(map[string]*nodereadinessiov1alpha1.NodeReadinessRule),
532+
Client: k8sClient,
533+
Scheme: k8sClient.Scheme(),
534+
clientset: fakeClientset,
535+
ruleCache: make(map[string]*nodereadinessiov1alpha1.NodeReadinessRule),
536+
EventRecorder: record.NewFakeRecorder(10),
533537
}
534538

535539
nodeReconciler = &NodeReconciler{

internal/controller/nodereadinessrule_controller.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"k8s.io/apimachinery/pkg/labels"
3131
"k8s.io/apimachinery/pkg/runtime"
3232
"k8s.io/client-go/kubernetes"
33+
"k8s.io/client-go/tools/record"
3334
"k8s.io/client-go/util/retry"
3435
ctrl "sigs.k8s.io/controller-runtime"
3536
"sigs.k8s.io/controller-runtime/pkg/builder"
@@ -50,8 +51,9 @@ const (
5051
// RuleReadinessController manages node taints based on readiness rules.
5152
type RuleReadinessController struct {
5253
client.Client
53-
Scheme *runtime.Scheme
54-
clientset kubernetes.Interface
54+
Scheme *runtime.Scheme
55+
clientset kubernetes.Interface
56+
EventRecorder record.EventRecorder
5557

5658
// Cache for efficient rule lookup
5759
ruleCacheMutex sync.RWMutex
@@ -68,10 +70,11 @@ type RuleReconciler struct {
6870
// NewRuleReadinessController creates a new controller.
6971
func NewRuleReadinessController(mgr ctrl.Manager, clientset kubernetes.Interface) *RuleReadinessController {
7072
return &RuleReadinessController{
71-
Client: mgr.GetClient(),
72-
Scheme: mgr.GetScheme(),
73-
clientset: clientset,
74-
ruleCache: make(map[string]*readinessv1alpha1.NodeReadinessRule),
73+
Client: mgr.GetClient(),
74+
Scheme: mgr.GetScheme(),
75+
clientset: clientset,
76+
EventRecorder: mgr.GetEventRecorderFor("node-readiness-controller"),
77+
ruleCache: make(map[string]*readinessv1alpha1.NodeReadinessRule),
7578
}
7679
}
7780

@@ -87,6 +90,7 @@ func (r *RuleReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager)
8790
// +kubebuilder:rbac:groups=readiness.node.x-k8s.io,resources=nodereadinessrules,verbs=get;list;watch;create;update;patch;delete
8891
// +kubebuilder:rbac:groups=readiness.node.x-k8s.io,resources=nodereadinessrules/status,verbs=get;update;patch
8992
// +kubebuilder:rbac:groups=readiness.node.x-k8s.io,resources=nodereadinessrules/finalizers,verbs=update
93+
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
9094

9195
func (r *RuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
9296
log := ctrl.LoggerFrom(ctx)
@@ -325,13 +329,15 @@ func (r *RuleReadinessController) evaluateRuleForNode(ctx context.Context, rule
325329
log.Info("Evaluation result", "node", node.Name, "rule", rule.Name,
326330
"allConditionsSatisfied", allConditionsSatisfied, "hasTaint", currentlyHasTaint)
327331

332+
isFirstEvaluation := r.getPreviousNodeEvaluation(rule, node.Name) == nil
333+
328334
var err error
329335

330336
switch {
331337
case shouldRemoveTaint && currentlyHasTaint:
332338
log.Info("Removing taint", "node", node.Name, "rule", rule.Name, "taint", rule.Spec.Taint.Key)
333339

334-
if err = r.removeTaintBySpec(ctx, node, rule.Spec.Taint); err != nil {
340+
if err = r.removeTaintBySpec(ctx, node, rule.Spec.Taint, rule.Name); err != nil {
335341
metrics.Failures.WithLabelValues(rule.Name, "RemoveTaintError").Inc()
336342
return fmt.Errorf("failed to remove taint: %w", err)
337343
}
@@ -345,12 +351,20 @@ func (r *RuleReadinessController) evaluateRuleForNode(ctx context.Context, rule
345351
case !shouldRemoveTaint && !currentlyHasTaint:
346352
log.Info("Adding taint", "node", node.Name, "rule", rule.Name, "taint", rule.Spec.Taint.Key)
347353

348-
if err = r.addTaintBySpec(ctx, node, rule.Spec.Taint); err != nil {
354+
if err = r.addTaintBySpec(ctx, node, rule.Spec.Taint, rule.Name); err != nil {
349355
metrics.Failures.WithLabelValues(rule.Name, "AddTaintError").Inc()
350356
return fmt.Errorf("failed to add taint: %w", err)
351357
}
352358
metrics.TaintOperations.WithLabelValues(rule.Name, "add").Inc()
353359

360+
case !shouldRemoveTaint && currentlyHasTaint:
361+
if isFirstEvaluation {
362+
log.Info("Adopting pre-existing taint", "node", node.Name, "rule", rule.Name, "taint", rule.Spec.Taint.Key)
363+
364+
message := fmt.Sprintf("Taint '%s:%s' is now managed by rule '%s'", rule.Spec.Taint.Key, rule.Spec.Taint.Effect, rule.Name)
365+
r.EventRecorder.Event(node, corev1.EventTypeNormal, "TaintAdopted", message)
366+
}
367+
354368
default:
355369
log.Info("No taint action needed", "node", node.Name, "rule", rule.Name,
356370
"shouldRemove", shouldRemoveTaint, "hasTaint", currentlyHasTaint)
@@ -598,7 +612,7 @@ func (r *RuleReadinessController) cleanupTaintsForRule(ctx context.Context, rule
598612
"rule", rule.Name,
599613
"taint", rule.Spec.Taint.Key)
600614

601-
if err := r.removeTaintBySpec(ctx, &node, rule.Spec.Taint); err != nil {
615+
if err := r.removeTaintBySpec(ctx, &node, rule.Spec.Taint, rule.Name); err != nil {
602616
errors = append(errors, fmt.Sprintf("node %s: %v", node.Name, err))
603617
}
604618
}
@@ -650,7 +664,7 @@ func (r *RuleReadinessController) cleanupNodesAfterSelectorChange(ctx context.Co
650664
"rule", newRule.Name,
651665
"taint", newRule.Spec.Taint.Key)
652666

653-
if err := r.removeTaintBySpec(ctx, &node, newRule.Spec.Taint); err != nil {
667+
if err := r.removeTaintBySpec(ctx, &node, newRule.Spec.Taint, newRule.Name); err != nil {
654668
errors = append(errors, fmt.Sprintf("node %s: %v", node.Name, err))
655669
}
656670
}
@@ -681,3 +695,14 @@ func (r *RuleReconciler) ensureFinalizer(ctx context.Context, rule *readinessv1a
681695
}
682696
return true, nil
683697
}
698+
699+
// getPreviousNodeEvaluation retrieves the previous evaluation result for a specific node from the rule status.
700+
// It returns nil (if the node is evaluated for the first time) otherwsie, return the previously evaluated node data.
701+
func (r *RuleReadinessController) getPreviousNodeEvaluation(rule *readinessv1alpha1.NodeReadinessRule, nodeName string) *readinessv1alpha1.NodeEvaluation {
702+
for i := range rule.Status.NodeEvaluations {
703+
if rule.Status.NodeEvaluations[i].NodeName == nodeName {
704+
return &rule.Status.NodeEvaluations[i]
705+
}
706+
}
707+
return nil
708+
}

internal/controller/nodereadinessrule_controller_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"k8s.io/apimachinery/pkg/runtime"
2929
"k8s.io/apimachinery/pkg/types"
3030
"k8s.io/client-go/kubernetes/fake"
31+
"k8s.io/client-go/tools/record"
3132
"sigs.k8s.io/controller-runtime/pkg/client"
3233
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3334

@@ -56,10 +57,11 @@ var _ = Describe("NodeReadinessRule Controller", func() {
5657

5758
fakeClientset = fake.NewSimpleClientset()
5859
readinessController = &RuleReadinessController{
59-
Client: k8sClient,
60-
Scheme: scheme,
61-
clientset: fakeClientset,
62-
ruleCache: make(map[string]*nodereadinessiov1alpha1.NodeReadinessRule),
60+
Client: k8sClient,
61+
Scheme: scheme,
62+
clientset: fakeClientset,
63+
ruleCache: make(map[string]*nodereadinessiov1alpha1.NodeReadinessRule),
64+
EventRecorder: record.NewFakeRecorder(10),
6365
}
6466

6567
ruleReconciler = &RuleReconciler{

test/e2e/e2e_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,125 @@ status:
525525
exec.Command("kubectl", "delete", "node", nodeName).Run()
526526
exec.Command("kubectl", "delete", "nodereadinessrule", "dryrun-test-rule").Run()
527527
})
528+
529+
It("should emit events for taint operations", func() {
530+
nodeName := "event-test-node"
531+
532+
By("creating a test node with pre-existing taint")
533+
cmd := exec.Command("kubectl", "apply", "-f", "-")
534+
cmd.Stdin = strings.NewReader(fmt.Sprintf(`
535+
apiVersion: v1
536+
kind: Node
537+
metadata:
538+
name: %s
539+
labels:
540+
e2e-test: "events"
541+
spec:
542+
taints:
543+
- key: readiness.k8s.io/EventTest
544+
effect: NoSchedule
545+
value: pending
546+
status:
547+
conditions:
548+
- type: EventTest
549+
status: "False"
550+
lastHeartbeatTime: %s
551+
lastTransitionTime: %s
552+
`, nodeName, time.Now().Format(time.RFC3339), time.Now().Format(time.RFC3339)))
553+
_, err := utils.Run(cmd)
554+
Expect(err).NotTo(HaveOccurred())
555+
556+
By("applying the rule to adopt pre-existing taint")
557+
cmd = exec.Command("kubectl", "apply", "-f", "-")
558+
cmd.Stdin = strings.NewReader(`
559+
apiVersion: readiness.node.x-k8s.io/v1alpha1
560+
kind: NodeReadinessRule
561+
metadata:
562+
name: event-test-rule
563+
spec:
564+
conditions:
565+
- type: EventTest
566+
requiredStatus: "True"
567+
taint:
568+
key: readiness.k8s.io/EventTest
569+
effect: NoSchedule
570+
value: pending
571+
enforcementMode: "continuous"
572+
nodeSelector:
573+
matchLabels:
574+
e2e-test: "events"
575+
`)
576+
_, err = utils.Run(cmd)
577+
Expect(err).NotTo(HaveOccurred())
578+
579+
By("verifying TaintAdopted event is emitted")
580+
Eventually(func() bool {
581+
cmd := exec.Command("kubectl", "get", "events", "--field-selector",
582+
fmt.Sprintf("involvedObject.name=%s", nodeName), "-o", "json")
583+
output, err := utils.Run(cmd)
584+
if err != nil {
585+
return false
586+
}
587+
return strings.Contains(output, "TaintAdopted") &&
588+
strings.Contains(output, "event-test-rule")
589+
}, 30*time.Second, 2*time.Second).Should(BeTrue())
590+
591+
By("updating node condition to True to trigger taint removal")
592+
err = patchNodeCondition(nodeName, "EventTest", "True")
593+
Expect(err).NotTo(HaveOccurred())
594+
595+
By("verifying taint is removed")
596+
Eventually(func() bool {
597+
cmd := exec.Command("kubectl", "get", "node", nodeName, "-o", "jsonpath={.spec.taints}")
598+
output, err := utils.Run(cmd)
599+
if err != nil {
600+
return false
601+
}
602+
return !strings.Contains(output, "readiness.k8s.io/EventTest")
603+
}, 30*time.Second, 2*time.Second).Should(BeTrue())
604+
605+
By("verifying TaintRemoved event is emitted")
606+
Eventually(func() bool {
607+
cmd := exec.Command("kubectl", "get", "events", "--field-selector",
608+
fmt.Sprintf("involvedObject.name=%s", nodeName), "-o", "json")
609+
output, err := utils.Run(cmd)
610+
if err != nil {
611+
return false
612+
}
613+
return strings.Contains(output, "TaintRemoved") &&
614+
strings.Contains(output, "event-test-rule")
615+
}, 30*time.Second, 2*time.Second).Should(BeTrue())
616+
617+
By("updating node condition back to False to trigger taint re-addition")
618+
err = patchNodeCondition(nodeName, "EventTest", "False")
619+
Expect(err).NotTo(HaveOccurred())
620+
621+
By("verifying taint is re-added")
622+
Eventually(func() bool {
623+
cmd := exec.Command("kubectl", "get", "node", nodeName, "-o", "jsonpath={.spec.taints}")
624+
output, err := utils.Run(cmd)
625+
if err != nil {
626+
return false
627+
}
628+
return strings.Contains(output, "readiness.k8s.io/EventTest")
629+
}, 30*time.Second, 2*time.Second).Should(BeTrue())
630+
631+
By("verifying TaintAdded event is emitted")
632+
Eventually(func() bool {
633+
cmd := exec.Command("kubectl", "get", "events", "--field-selector",
634+
fmt.Sprintf("involvedObject.name=%s", nodeName), "-o", "json")
635+
output, err := utils.Run(cmd)
636+
if err != nil {
637+
return false
638+
}
639+
return strings.Contains(output, "TaintAdded") &&
640+
strings.Contains(output, "event-test-rule")
641+
}, 30*time.Second, 2*time.Second).Should(BeTrue())
642+
643+
By("cleaning up test resources")
644+
exec.Command("kubectl", "delete", "node", nodeName).Run()
645+
exec.Command("kubectl", "delete", "nodereadinessrule", "event-test-rule").Run()
646+
})
528647
})
529648
})
530649

0 commit comments

Comments
 (0)