Skip to content

Commit db7d7e2

Browse files
committed
bootstrap failures and use reason constants
1 parent 4b8f1ca commit db7d7e2

4 files changed

Lines changed: 173 additions & 12 deletions

File tree

internal/controller/node_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func (r *RuleReadinessController) processNodeAgainstAllRules(ctx context.Context
156156
log.Error(err, "Failed to evaluate rule for node",
157157
"node", node.Name, "rule", rule.Name)
158158
// Continue with other rules even if one fails
159-
r.recordNodeFailure(rule, node.Name, "EvaluationError", err.Error())
159+
r.recordNodeFailure(rule, node.Name, string(metrics.FailureReasonEvaluationError), err.Error())
160160
errs = append(errs, err)
161161
metrics.Failures.WithLabelValues(rule.Name, string(metrics.FailureReasonEvaluationError)).Inc()
162162
}
@@ -393,6 +393,7 @@ func (r *RuleReadinessController) markBootstrapCompleted(ctx context.Context, no
393393
switch {
394394
case err != nil:
395395
log.Error(err, "Failed to mark bootstrap completed", "node", nodeName, "rule", ruleName, "uid", ruleUID)
396+
metrics.Failures.WithLabelValues(ruleName, string(metrics.FailureReasonAnnotationPatchFailed)).Inc()
396397
case marked:
397398
log.Info("Marked bootstrap completed", "node", nodeName, "rule", ruleName, "uid", ruleUID)
398399
metrics.BootstrapCompleted.WithLabelValues(ruleName).Inc()

internal/controller/nodereadinessrule_controller.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,19 @@ func (r *RuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
138138
// Update rule cache (after cleanup)
139139
r.Controller.updateRuleCache(ctx, rule)
140140

141+
// Filter nodes once and update node_readiness_rule_matched_nodes.
142+
var matchedNodes []corev1.Node
143+
for i := range nodeList.Items {
144+
if r.Controller.ruleAppliesTo(ctx, rule, &nodeList.Items[i]) {
145+
matchedNodes = append(matchedNodes, nodeList.Items[i])
146+
}
147+
}
148+
metrics.RuleMatchedNodes.WithLabelValues(rule.Name).Set(float64(len(matchedNodes)))
149+
filteredList := &corev1.NodeList{Items: matchedNodes}
150+
141151
// Handle dry run
142152
if rule.Spec.DryRun {
143-
if err := r.Controller.processDryRun(ctx, rule, nodeList); err != nil {
153+
if err := r.Controller.processDryRun(ctx, rule, filteredList); err != nil {
144154
log.Error(err, "Failed to process dry run", "rule", rule.Name)
145155
return ctrl.Result{RequeueAfter: time.Minute}, err
146156
}
@@ -149,7 +159,7 @@ func (r *RuleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
149159
rule.Status.DryRunResults = readinessv1alpha1.DryRunResults{}
150160

151161
// Process all applicable nodes for this rule
152-
if err := r.Controller.processAllNodesForRule(ctx, rule, nodeList); err != nil {
162+
if err := r.Controller.processAllNodesForRule(ctx, rule, filteredList); err != nil {
153163
log.Error(err, "Failed to process nodes for rule", "rule", rule.Name)
154164
return ctrl.Result{RequeueAfter: time.Minute}, err
155165
}
@@ -213,6 +223,7 @@ func (r *RuleReconciler) reconcileDelete(ctx context.Context, rule *readinessv1a
213223
metrics.BootstrapCompleted.DeleteLabelValues(rule.Name)
214224
metrics.BootstrapDuration.DeleteLabelValues(rule.Name)
215225
metrics.EvaluationDuration.DeleteLabelValues(rule.Name)
226+
metrics.RuleMatchedNodes.DeleteLabelValues(rule.Name)
216227

217228
// For multi-label metrics, use DeletePartialMatch to wipe all combinations
218229
metrics.NodesByState.DeletePartialMatch(ruleLabel)
@@ -281,24 +292,26 @@ func (r *RuleReadinessController) cleanupDeletedNodes(ctx context.Context, rule
281292
func (r *RuleReadinessController) processAllNodesForRule(ctx context.Context, rule *readinessv1alpha1.NodeReadinessRule, nodeList *corev1.NodeList) error {
282293
log := ctrl.LoggerFrom(ctx)
283294

284-
log.Info("Processing all nodes for rule", "rule", rule.Name, "totalNodes", len(nodeList.Items))
295+
log.Info("Processing all nodes for rule", "rule", rule.Name, "matchedNodes", len(nodeList.Items))
285296

286297
var appliedNodes []string
287298
for _, node := range nodeList.Items {
288299
if r.ruleAppliesTo(ctx, rule, &node) {
289300
log.Info("Processing node for rule", "rule", rule.Name, "node", node.Name)
290301
if err := r.evaluateRuleForNode(ctx, rule, &node); err != nil {
291302
log.Error(err, "Failed to evaluate node for rule", "rule", rule.Name, "node", node.Name)
292-
r.recordNodeFailure(rule, node.Name, "EvaluationError", err.Error())
303+
r.recordNodeFailure(rule, node.Name, string(metrics.FailureReasonEvaluationError), err.Error())
293304
metrics.Failures.WithLabelValues(rule.Name, string(metrics.FailureReasonEvaluationError)).Inc()
294305
} else {
295306
appliedNodes = append(appliedNodes, node.Name)
307+
296308
var updatedFailedNodes []readinessv1alpha1.NodeFailure
297309
for _, f := range rule.Status.FailedNodes {
298310
if f.NodeName != node.Name {
299311
updatedFailedNodes = append(updatedFailedNodes, f)
300312
}
301313
}
314+
302315
rule.Status.FailedNodes = updatedFailedNodes
303316
}
304317
}
@@ -600,10 +613,6 @@ func (r *RuleReadinessController) processDryRun(ctx context.Context, rule *readi
600613
var summaryParts []string
601614

602615
for _, node := range nodeList.Items {
603-
if !r.ruleAppliesTo(ctx, rule, &node) {
604-
continue
605-
}
606-
607616
affectedNodes++
608617

609618
// Simulate rule evaluation

internal/controller/nodereadinessrule_controller_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ func histogramSampleCount(histogram interface{ Write(*dto.Metric) error }) uint6
5555
return metric.GetHistogram().GetSampleCount()
5656
}
5757

58+
func gaugeValue(gauge interface{ Write(*dto.Metric) error }) float64 {
59+
metric := &dto.Metric{}
60+
Expect(gauge.Write(metric)).To(Succeed())
61+
return metric.GetGauge().GetValue()
62+
}
63+
5864
// errorInjectingClient forces Patch to fail for selected nodes.
5965
type errorInjectingClient struct {
6066
client.Client
@@ -2235,4 +2241,138 @@ var _ = Describe("NodeReadinessRule Controller", func() {
22352241
Expect(failedNames).NotTo(ContainElement("stale-recovery-node"))
22362242
})
22372243
})
2244+
2245+
Context("Metric: node_readiness_rule_matched_nodes", func() {
2246+
It("should set gauge to matched node count after reconcile", func() {
2247+
ruleName := "sel-gauge-match-rule"
2248+
matchLabel := "sel-gauge-match"
2249+
2250+
nodeA := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "sel-gauge-node-a", Labels: map[string]string{matchLabel: "true"}}}
2251+
nodeB := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "sel-gauge-node-b", Labels: map[string]string{matchLabel: "true"}}}
2252+
nodeC := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "sel-gauge-node-c", Labels: map[string]string{"other": "label"}}}
2253+
for _, n := range []*corev1.Node{nodeA, nodeB, nodeC} {
2254+
Expect(k8sClient.Create(ctx, n)).To(Succeed())
2255+
}
2256+
defer func() {
2257+
for _, n := range []*corev1.Node{nodeA, nodeB, nodeC} {
2258+
_ = k8sClient.Delete(ctx, n)
2259+
}
2260+
}()
2261+
2262+
rule := &nodereadinessiov1alpha1.NodeReadinessRule{
2263+
ObjectMeta: metav1.ObjectMeta{Name: ruleName, Finalizers: []string{finalizerName}},
2264+
Spec: nodereadinessiov1alpha1.NodeReadinessRuleSpec{
2265+
Conditions: []nodereadinessiov1alpha1.ConditionRequirement{{Type: "Ready", RequiredStatus: corev1.ConditionTrue}},
2266+
Taint: corev1.Taint{Key: "readiness.k8s.io/sel-gauge-taint", Effect: corev1.TaintEffectNoSchedule},
2267+
NodeSelector: metav1.LabelSelector{MatchLabels: map[string]string{matchLabel: "true"}},
2268+
EnforcementMode: nodereadinessiov1alpha1.EnforcementModeContinuous,
2269+
},
2270+
}
2271+
Expect(k8sClient.Create(ctx, rule)).To(Succeed())
2272+
defer func() { _ = k8sClient.Delete(ctx, rule) }()
2273+
2274+
_, err := ruleReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: ruleName}})
2275+
Expect(err).NotTo(HaveOccurred())
2276+
2277+
gauge, err := metrics.RuleMatchedNodes.GetMetricWith(prometheus.Labels{"rule": ruleName})
2278+
Expect(err).NotTo(HaveOccurred())
2279+
Expect(gaugeValue(gauge)).To(Equal(2.0))
2280+
})
2281+
2282+
It("should set gauge to 0 when no nodes match the selector", func() {
2283+
ruleName := "sel-gauge-nomatch-rule"
2284+
2285+
rule := &nodereadinessiov1alpha1.NodeReadinessRule{
2286+
ObjectMeta: metav1.ObjectMeta{Name: ruleName, Finalizers: []string{finalizerName}},
2287+
Spec: nodereadinessiov1alpha1.NodeReadinessRuleSpec{
2288+
Conditions: []nodereadinessiov1alpha1.ConditionRequirement{{Type: "Ready", RequiredStatus: corev1.ConditionTrue}},
2289+
Taint: corev1.Taint{Key: "readiness.k8s.io/sel-nomatch-taint", Effect: corev1.TaintEffectNoSchedule},
2290+
NodeSelector: metav1.LabelSelector{MatchLabels: map[string]string{"definitely-no-such-label": "true"}},
2291+
EnforcementMode: nodereadinessiov1alpha1.EnforcementModeContinuous,
2292+
},
2293+
}
2294+
Expect(k8sClient.Create(ctx, rule)).To(Succeed())
2295+
defer func() { _ = k8sClient.Delete(ctx, rule) }()
2296+
2297+
_, err := ruleReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: ruleName}})
2298+
Expect(err).NotTo(HaveOccurred())
2299+
2300+
gauge, err := metrics.RuleMatchedNodes.GetMetricWith(prometheus.Labels{"rule": ruleName})
2301+
Expect(err).NotTo(HaveOccurred())
2302+
Expect(gaugeValue(gauge)).To(Equal(0.0))
2303+
})
2304+
2305+
It("should clean up label values on rule deletion", func() {
2306+
ruleName := "sel-gauge-del-rule"
2307+
matchLabel := "sel-gauge-del"
2308+
2309+
node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "sel-gauge-del-node", Labels: map[string]string{matchLabel: "true"}}}
2310+
Expect(k8sClient.Create(ctx, node)).To(Succeed())
2311+
defer func() { _ = k8sClient.Delete(ctx, node) }()
2312+
2313+
rule := &nodereadinessiov1alpha1.NodeReadinessRule{
2314+
ObjectMeta: metav1.ObjectMeta{Name: ruleName, Finalizers: []string{finalizerName}},
2315+
Spec: nodereadinessiov1alpha1.NodeReadinessRuleSpec{
2316+
Conditions: []nodereadinessiov1alpha1.ConditionRequirement{{Type: "Ready", RequiredStatus: corev1.ConditionTrue}},
2317+
Taint: corev1.Taint{Key: "readiness.k8s.io/sel-del-taint", Effect: corev1.TaintEffectNoSchedule},
2318+
NodeSelector: metav1.LabelSelector{MatchLabels: map[string]string{matchLabel: "true"}},
2319+
EnforcementMode: nodereadinessiov1alpha1.EnforcementModeContinuous,
2320+
},
2321+
}
2322+
Expect(k8sClient.Create(ctx, rule)).To(Succeed())
2323+
2324+
_, err := ruleReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: ruleName}})
2325+
Expect(err).NotTo(HaveOccurred())
2326+
2327+
gauge, err := metrics.RuleMatchedNodes.GetMetricWith(prometheus.Labels{"rule": ruleName})
2328+
Expect(err).NotTo(HaveOccurred())
2329+
Expect(gaugeValue(gauge)).To(Equal(1.0), "gauge should be 1 before deletion")
2330+
2331+
Expect(k8sClient.Delete(ctx, rule)).To(Succeed())
2332+
2333+
// Deletion reconcile triggers reconcileDelete, which calls DeleteLabelValues.
2334+
Eventually(func() bool {
2335+
_, err := ruleReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: types.NamespacedName{Name: ruleName}})
2336+
Expect(err).NotTo(HaveOccurred())
2337+
readinessController.ruleCacheMutex.RLock()
2338+
_, exists := readinessController.ruleCache[ruleName]
2339+
readinessController.ruleCacheMutex.RUnlock()
2340+
return !exists
2341+
}).Should(BeTrue())
2342+
2343+
// After DeleteLabelValues, GetMetricWith allocates a fresh gauge at 0 (old value removed).
2344+
freshGauge, err := metrics.RuleMatchedNodes.GetMetricWith(prometheus.Labels{"rule": ruleName})
2345+
Expect(err).NotTo(HaveOccurred())
2346+
Expect(gaugeValue(freshGauge)).To(Equal(0.0))
2347+
})
2348+
})
2349+
2350+
Context("Metric: failures_total (reason=AnnotationPatchFailed)", func() {
2351+
It("should increment counter when annotation write fails", func() {
2352+
ruleName := "bce-error-rule"
2353+
ruleUID := types.UID("33333333-3333-3333-3333-333333333333")
2354+
2355+
before := counterValue(metrics.Failures.WithLabelValues(ruleName, string(metrics.FailureReasonAnnotationPatchFailed)))
2356+
2357+
readinessController.markBootstrapCompleted(ctx, "nonexistent-node-for-bce-test", ruleName, ruleUID)
2358+
2359+
Expect(counterValue(metrics.Failures.WithLabelValues(ruleName, string(metrics.FailureReasonAnnotationPatchFailed)))).To(Equal(before + 1))
2360+
})
2361+
2362+
It("should not increment counter on successful annotation write", func() {
2363+
nodeName := "bce-success-node"
2364+
ruleName := "bce-success-rule"
2365+
ruleUID := types.UID("44444444-4444-4444-4444-444444444444")
2366+
2367+
node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeName}}
2368+
Expect(k8sClient.Create(ctx, node)).To(Succeed())
2369+
defer func() { _ = k8sClient.Delete(ctx, node) }()
2370+
2371+
before := counterValue(metrics.Failures.WithLabelValues(ruleName, string(metrics.FailureReasonAnnotationPatchFailed)))
2372+
2373+
readinessController.markBootstrapCompleted(ctx, nodeName, ruleName, ruleUID)
2374+
2375+
Expect(counterValue(metrics.Failures.WithLabelValues(ruleName, string(metrics.FailureReasonAnnotationPatchFailed)))).To(Equal(before))
2376+
})
2377+
})
22382378
})

internal/metrics/metrics.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ import (
2525
type FailureReason string
2626

2727
const (
28-
FailureReasonEvaluationError FailureReason = "EvaluationError"
29-
FailureReasonAddTaintError FailureReason = "AddTaintError"
30-
FailureReasonRemoveTaintError FailureReason = "RemoveTaintError"
28+
FailureReasonEvaluationError FailureReason = "EvaluationError"
29+
FailureReasonAddTaintError FailureReason = "AddTaintError"
30+
FailureReasonRemoveTaintError FailureReason = "RemoveTaintError"
31+
FailureReasonAnnotationPatchFailed FailureReason = "AnnotationPatchFailed"
3132
)
3233

3334
// TaintOperation represents a taint operation.
@@ -152,6 +153,15 @@ var (
152153
},
153154
[]string{"rule"},
154155
)
156+
157+
// RuleMatchedNodes tracks how many nodes match each rule's selector.
158+
RuleMatchedNodes = prometheus.NewGaugeVec(
159+
prometheus.GaugeOpts{
160+
Name: "node_readiness_rule_matched_nodes",
161+
Help: "Number of nodes matched by a rule's NodeSelector",
162+
},
163+
[]string{"rule"},
164+
)
155165
)
156166

157167
func init() {
@@ -166,4 +176,5 @@ func init() {
166176
metrics.Registry.MustRegister(NodesByState)
167177
metrics.Registry.MustRegister(ConditionEvaluationFailures)
168178
metrics.Registry.MustRegister(RuleLastReconciliationTime)
179+
metrics.Registry.MustRegister(RuleMatchedNodes)
169180
}

0 commit comments

Comments
 (0)