Skip to content

Commit 5de4525

Browse files
fix(fault-quarantine): keep applied taints whenever a ValidationRequest is created
Address review feedback: drop the validation.retainTaints setting. When fault-quarantine creates a ValidationRequest it now keeps every taint it applied during the quarantine session, the same way it keeps the cordon, by clearing taintsToBeRemoved next to isUnCordon in triggerValidationOnUnquarantine. lifecycle-manager lifts them through schedulingGate.taints when validation passes. The docs state that every taint a rule-set applies must be listed in schedulingGate.taints with remove set to true. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Harsha Kalalbandi <hkalalbandi@voltagepark.com>
1 parent 6fa177a commit 5de4525

6 files changed

Lines changed: 24 additions & 37 deletions

File tree

‎distros/kubernetes/nvsentinel/charts/fault-quarantine/templates/configmap.yaml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ data:
8282
resource = {{ printf "%ss" (.Values.validation.kind | lower) | quote }}
8383
templateFileName = {{ .Values.validation.templateFileName | quote }}
8484
templateMountPath = "/etc/config"
85-
retainTaints = {{ .Values.validation.retainTaints | default false }}
8685
8786
{{- range .Values.validation.ruleSets }}
8887
[[validation.ruleSets]]

‎distros/kubernetes/nvsentinel/charts/fault-quarantine/values.yaml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,9 @@ ruleSets:
270270

271271
# The validation section configures post-remediation validation from the fault-quarantine
272272
# module. If enabled, fault-quarantine will preserve the scheduling gate on a given node
273-
# when an unquarantine event occurs, and it will create the given validation resource
273+
# (the cordon and the taints it applied) when an unquarantine event occurs, so every taint
274+
# a ruleSet above applies must be listed in lifecycle-manager config.schedulingGate.taints
275+
# with remove set to true. fault-quarantine will also create the given validation resource
274276
# for the requested tests. The list of test is derived from the unhealthy events which
275277
# occurred during the quarantine session and these tests are derived from the ruleSets
276278
# section below.
@@ -280,8 +282,6 @@ validation:
280282
version: "v1alpha1"
281283
kind: "ValidationRequest"
282284
templateFileName: "validation-request-template.yaml"
283-
# Keep quarantine taints during validation; list them in lifecycle-manager schedulingGate.taints.
284-
retainTaints: false
285285
templates:
286286
"validation-request-template.yaml": |
287287
apiVersion: nvsentinel.nvidia.com/v1alpha1

‎docs/configuration/validation.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,8 @@ status:
343343
| templateFileName | string | The filename of the Go text/template, resolved from templates, rendered to build the ValidationRequest |
344344
| templates | map[string]string | Inline template content, keyed by filename |
345345
| ruleSets | []RuleSet | Maps HealthEvents from a quarantine session to the tests they require |
346-
| retainTaints | bool | Keep the session's quarantine taints when a ValidationRequest is created, as the cordon is kept. List each taint in lifecycle-manager schedulingGate.taints with remove set to true so it is lifted when validation passes, and keep the not-under-quarantine readiness criterion so a new quarantine fails the pending validation instead of releasing the node. Use remove set to true only for taints that fault-quarantine alone applies: a taint that was on the node before the quarantine is not fault-quarantine's, and lifecycle-manager would lift it too. Default false |
346+
347+
When fault-quarantine creates a ValidationRequest, it keeps the cordon and every taint it applied during the quarantine session, and it stops tracking them. lifecycle-manager releases them when validation passes. Every taint that a fault-quarantine rule-set applies must therefore be listed in lifecycle-manager schedulingGate.taints with remove set to true. A taint that is not listed is not tolerated by the test pods, so validation cannot run on the node, and it is never removed. Keep the not-under-quarantine readiness criterion so a new quarantine fails the pending validation instead of releasing the node. Use remove set to true only for taints that fault-quarantine alone applies: lifecycle-manager matches on key, value, and effect, so it also lifts a matching taint that was on the node before the quarantine. When no ValidationRequest is created, fault-quarantine removes its taints itself.
347348
348349
### fault-quarantine.validation.ruleSets
349350

‎fault-quarantine/pkg/config/config.go‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@ type ValidationConfig struct {
9090
TemplateMountPath string `toml:"templateMountPath"`
9191
TemplateFileName string `toml:"templateFileName"`
9292
RuleSets []ValidationRuleSet `toml:"ruleSets"`
93-
// RetainTaints leaves quarantine taints for lifecycle-manager schedulingGate.taints to remove.
94-
RetainTaints bool `toml:"retainTaints"`
9593
}
9694

9795
type TomlConfig struct {

‎fault-quarantine/pkg/reconciler/reconciler.go‎

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1945,14 +1945,13 @@ func (r *Reconciler) performUncordon(
19451945
return false, nil
19461946
}
19471947

1948-
annotationsToBeRemoved, isUnCordon, validationRequestCreated, err := r.triggerValidationOnUnquarantine(
1949-
ctx, span, event, annotations, annotationsToBeRemoved, isUnCordon)
1948+
annotationsToBeRemoved, taintsToBeRemoved, isUnCordon, validationRequestCreated, err :=
1949+
r.triggerValidationOnUnquarantine(ctx, span, event, annotations, annotationsToBeRemoved, taintsToBeRemoved,
1950+
isUnCordon)
19501951
if err != nil {
19511952
return true, err
19521953
}
19531954

1954-
taintsToBeRemoved = r.taintsToRemoveOnUncordon(span, taintsToBeRemoved, validationRequestCreated)
1955-
19561955
if !r.breakerActive() {
19571956
slog.InfoContext(ctx, "Circuit breaker is disabled, proceeding with unquarantine action for node",
19581957
"node", event.NodeName)
@@ -1990,19 +1989,6 @@ func (r *Reconciler) performUncordon(
19901989
return false, nil
19911990
}
19921991

1993-
// taintsToRemoveOnUncordon returns no taints when a ValidationRequest was created and
1994-
// validation.retainTaints is set, so the taints stay until validation completes.
1995-
func (r *Reconciler) taintsToRemoveOnUncordon(span trace.Span, taints []config.Taint,
1996-
validationRequestCreated bool) []config.Taint {
1997-
if !validationRequestCreated || !r.config.TomlConfig.Validation.RetainTaints {
1998-
return taints
1999-
}
2000-
2001-
span.SetAttributes(attribute.Int("fault_quarantine.taints.retained", len(taints)))
2002-
2003-
return nil
2004-
}
2005-
20061992
func (r *Reconciler) buildUncordonLabelsToRemove(ruleLabelsToRemove []config.Label,
20071993
validationRequestCreated bool) []string {
20081994
labelsToRemove := []string{statemanager.NVSentinelStateLabelKey}
@@ -2021,15 +2007,16 @@ func (r *Reconciler) buildUncordonLabelsToRemove(ruleLabelsToRemove []config.Lab
20212007
// annotation present when it is being unquarantined. Note that we also require that the node was fully drained
20222008
// as part of its quarantine session so it's possible that ValidationRequest creation is skipped even if the
20232009
// annotation present. If a ValidationRequest is created, we will skip removing the cordon, skip removing the
2024-
// cordon-by labels, and skip adding the uncordon-by labels. Taints are still removed unless validation.retainTaints
2025-
// is set.
2010+
// taints fault-quarantine applied, skip removing the cordon-by labels, and skip adding the uncordon-by labels.
2011+
// lifecycle-manager removes the cordon and the taints listed in schedulingGate.taints when validation succeeds.
20262012
//
20272013
// If a ValidationRequest creation fails, the node will not be uncordoned or untainted and all fault-quarantine labels
20282014
// and annotations will be preserved.
20292015
func (r *Reconciler) triggerValidationOnUnquarantine(ctx context.Context, span trace.Span, event *protos.HealthEvent,
2030-
annotations map[string]string, annotationsToBeRemoved []string, isUnCordon bool) ([]string, bool, bool, error) {
2016+
annotations map[string]string, annotationsToBeRemoved []string, taintsToBeRemoved []config.Taint,
2017+
isUnCordon bool) ([]string, []config.Taint, bool, bool, error) {
20312018
if _, exists := annotations[common.QuarantineValidationHealthEventAnnotationKey]; !exists {
2032-
return annotationsToBeRemoved, isUnCordon, false, nil
2019+
return annotationsToBeRemoved, taintsToBeRemoved, isUnCordon, false, nil
20332020
}
20342021

20352022
validationRequestCreated, err := r.createValidationRequestIfRequested(ctx, event, annotations)
@@ -2043,18 +2030,19 @@ func (r *Reconciler) triggerValidationOnUnquarantine(ctx context.Context, span t
20432030
attribute.String("fault_quarantine.error.message", err.Error()),
20442031
)
20452032

2046-
return annotationsToBeRemoved, isUnCordon, false, err
2033+
return annotationsToBeRemoved, taintsToBeRemoved, isUnCordon, false, err
20472034
}
20482035

20492036
annotationsToBeRemoved = append(annotationsToBeRemoved, common.QuarantineValidationHealthEventAnnotationKey)
20502037

20512038
if validationRequestCreated {
2039+
taintsToBeRemoved = nil
20522040
isUnCordon = false
20532041

20542042
span.SetAttributes(attribute.Bool("fault_quarantine.validation_request.created", true))
20552043
}
20562044

2057-
return annotationsToBeRemoved, isUnCordon, validationRequestCreated, nil
2045+
return annotationsToBeRemoved, taintsToBeRemoved, isUnCordon, validationRequestCreated, nil
20582046
}
20592047

20602048
func (r *Reconciler) createValidationRequestIfRequested(ctx context.Context, event *protos.HealthEvent,

‎fault-quarantine/pkg/reconciler/reconciler_e2e_test.go‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,11 +1813,13 @@ func TestE2E_ValidationRequestCreatedWhenEventDrained(t *testing.T) {
18131813
}, eventuallyTimeout, eventuallyPollInterval, "A ValidationRequest should be created")
18141814
assert.ElementsMatch(t, []string{"dcgm-diag-test", "nccl-test"}, listValidationRequestTests(ctx, t, nodeName)[0])
18151815

1816-
t.Log("Verify the node stays cordoned pending validation, but its taints are removed")
1816+
t.Log("Verify the node stays cordoned and tainted pending validation")
18171817
node, err := e2eTestClient.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{})
18181818
require.NoError(t, err)
18191819
assert.True(t, node.Spec.Unschedulable, "Node should stay unschedulable pending validation")
1820-
verifyFQTaintAbsent(t, node, "nvidia.com/gpu-xid-error")
1820+
verifyFQTaintPresent(t, node, "nvidia.com/gpu-xid-error")
1821+
assert.Empty(t, node.Annotations[common.QuarantineHealthEventAppliedTaintsAnnotationKey],
1822+
"Applied-taints annotation should be cleared once the ValidationRequest is created")
18211823
assert.Empty(t, node.Annotations[common.QuarantineValidationHealthEventAnnotationKey],
18221824
"Validation-session annotation should be cleared once the ValidationRequest is created")
18231825

@@ -1832,21 +1834,21 @@ func TestE2E_ValidationRequestCreatedWhenEventDrained(t *testing.T) {
18321834
"nvsentinel-state label should be removed")
18331835
}
18341836

1835-
func TestE2E_RetainTaints(t *testing.T) {
1837+
func TestE2E_ValidationRequestCreationKeepsTaints(t *testing.T) {
18361838
tests := []struct {
18371839
name string
18381840
drained bool
18391841
}{
1840-
{name: "validation requested keeps taints", drained: true},
1841-
{name: "validation skipped removes taints", drained: false},
1842+
{name: "ValidationRequest created keeps taints", drained: true},
1843+
{name: "ValidationRequest skipped removes taints", drained: false},
18421844
}
18431845

18441846
for _, tc := range tests {
18451847
t.Run(tc.name, func(t *testing.T) {
18461848
ctx, cancel := context.WithTimeout(e2eTestContext, 20*time.Second)
18471849
defer cancel()
18481850

1849-
nodeName := "e2e-retain-taints-" + generateShortTestID()
1851+
nodeName := "e2e-validation-taints-" + generateShortTestID()
18501852
createE2ETestNode(ctx, t, nodeName, nil, nil, nil, false)
18511853
defer func() {
18521854
_ = e2eTestClient.CoreV1().Nodes().Delete(ctx, nodeName, metav1.DeleteOptions{})
@@ -1866,7 +1868,6 @@ func TestE2E_RetainTaints(t *testing.T) {
18661868
Tests: []string{"dcgm-diag-test"},
18671869
},
18681870
})
1869-
validation.RetainTaints = true
18701871

18711872
tomlConfig := config.TomlConfig{
18721873
LabelPrefix: "k8s.nvidia.com/",

0 commit comments

Comments
 (0)