Skip to content

Commit 2418609

Browse files
chore: write a plugin custom remediation tutorial (#1493)
Signed-off-by: Nitin Jain (SW-CLOUD) <nitijain@nvidia.com> Co-authored-by: KaivalyaMDabhadkar <kdabhadkar@nvidia.com>
1 parent 62dc36b commit 2418609

3 files changed

Lines changed: 332 additions & 0 deletions

File tree

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Hands-on walkthroughs for extending NVSentinel:
3030
diagnostic, register it in Helm, and verify it blocks bad GPU pod starts.
3131
- [Writing a Drain Plugin](./tutorials/writing-a-drain-plugin.md) — replace node-drainer's
3232
eviction step with a custom controller.
33+
- [Plugin Custom Remediation](./tutorials/plugin-custom-remediation.md) — extend the
34+
fault-remediation stage with your own repair via custom actions and a controller CRD.
3335

3436
### [configuration/](./configuration/)
3537
**Component configuration guides**

docs/index.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ navigation:
2222
path: tutorials/writing-a-preflight-check.md
2323
- page: Writing a Drain Plugin
2424
path: tutorials/writing-a-drain-plugin.md
25+
- page: Plugin Custom Remediation
26+
path: tutorials/plugin-custom-remediation.md
2527

2628
- section: Architecture
2729
contents:
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
# Tutorial: Plugin Custom Remediation
2+
3+
This tutorial walks you through extending NVSentinel's **fault-remediation** stage with your own
4+
repair automation — for example, a custom controller.
5+
6+
By the end you will understand:
7+
8+
- Where custom remediation fits in the NVSentinel pipeline.
9+
- How to register a **custom remediation action** with no NVSentinel code changes.
10+
- The status-condition completion contract your controller must implement.
11+
12+
> **Who is this for?** Teams **already running NVSentinel** — who need a repair step
13+
> NVSentinel does not ship out of the box (CSP RMA tickets, multi-step orchestration, …) instead of
14+
> the built-in reboot / terminate / GPU-reset actions.
15+
16+
> **Just want the AI to do it?** Jump to [Appendix: One-shot AI prompt](#appendix-one-shot-ai-prompt).
17+
18+
---
19+
20+
## 1. Where custom remediation fits
21+
22+
NVSentinel remediates a faulty node in stages:
23+
24+
```text
25+
health-monitor → fault-quarantine → node-drainer → fault-remediation → your system
26+
```
27+
28+
**fault-remediation** is the extension point. After a node is cordoned and drained, fault-remediation:
29+
30+
1. Resolves the health event's **recommended action** (a built-in enum value or a custom string).
31+
2. Looks up a matching entry in the `maintenance.actions` Helm config.
32+
3. Renders a **Go template** into a Kubernetes Custom Resource.
33+
4. **Creates** that CR in the cluster.
34+
5. **Polls** the CR's status conditions (`completeConditionType`) to avoid duplicate repair requests.
35+
36+
Your controller watches the CR and performs the actual repair.
37+
38+
```text
39+
fault-remediation ──create CR──► Kubernetes API ◄──watch── Your controller
40+
▲ │
41+
└── poll completeConditionType ┘
42+
(repair + set status condition)
43+
```
44+
45+
### Built-in vs custom actions
46+
47+
| Action source | Example | Config key in `maintenance.actions` |
48+
|---------------|---------|-------------------------------------|
49+
| Built-in enum | `RESTART_VM`, `COMPONENT_RESET`, `REPLACE_VM` | `RESTART_VM`, `COMPONENT_RESET`, … |
50+
| Custom string ([ADR-036](../designs/036-custom-remediation-actions.md)) | `recommendedAction: CUSTOM`, `customRecommendedAction: hardware-rma` | `hardware-rma` |
51+
52+
---
53+
54+
## 2. Integration approach
55+
56+
fault-remediation creates a Kubernetes CR for your action; your controller watches it, runs the
57+
repair, and reports completion via a status condition.
58+
59+
### Why your CR must expose status conditions
60+
61+
fault-remediation's status checker only understands **Kubernetes-style conditions** — it looks for a
62+
`status.conditions[]` entry whose `type` equals `completeConditionType` and reads its `status`
63+
(`True` → done, `False` → failed/retry).
64+
65+
Many external orchestrators report completion differently (e.g. a phase field, a job exit code, or a
66+
ticket status). Use a thin wrapper CRD whose controller translates external completion into
67+
`status.conditions[Complete]=True`.
68+
69+
---
70+
71+
## 3. The completion contract
72+
73+
Whatever CR NVSentinel creates, fault-remediation decides "is this repair done?" by reading a status
74+
condition:
75+
76+
```yaml
77+
status:
78+
conditions:
79+
- type: <completeConditionType> # e.g. "Complete"
80+
status: "True" # True = success, False = failed (retry allowed)
81+
reason: RepairSucceeded
82+
message: Node hardware repaired and validated
83+
```
84+
85+
Behavior of the status checker:
86+
87+
- **Condition `True`** → repair succeeded; equivalent events are marked covered.
88+
- **Condition `False`** → repair failed; NVSentinel drops the group and **creates a new CR to retry**.
89+
There is no "terminal failure" state — if a repair is unrecoverable, keep the condition off `True`
90+
(leave it `Unknown`/absent) rather than setting `False`, or stop requeueing after your own retry budget.
91+
- **Condition missing / `Unknown`** → repair in progress; NVSentinel **skips** creating a duplicate CR.
92+
93+
fault-remediation derives an **effective equivalence group** from the matched action — and, when
94+
`impactedEntityScope` is set, the impacted entity (e.g. `reset-GPU-123`). It skips creating a new CR
95+
only while a CR in that effective group (or a superseding group) is in progress or already succeeded;
96+
it does **not** deduplicate every repair on the node.
97+
98+
`supersedingEquivalenceGroups` lets a broader action cover a narrower one. The **subordinate** action
99+
lists the group that supersedes it. For example, a node reboot has the same effect as an RMA, so the
100+
`hardware-rma` action lists `restart`:
101+
102+
```yaml
103+
hardware-rma:
104+
equivalenceGroup: external-rma
105+
supersedingEquivalenceGroups: [restart] # a restart supersedes external-rma
106+
RESTART_VM:
107+
equivalenceGroup: restart # must be a defined action with no impactedEntityScope
108+
```
109+
110+
---
111+
112+
## 4. Walkthrough — custom CR plugin (step by step)
113+
114+
### Step 1 — Emit a custom action from a health event
115+
116+
A health monitor must set both fields — the action type and the custom action name:
117+
118+
```yaml
119+
recommendedAction: CUSTOM
120+
customRecommendedAction: hardware-rma
121+
```
122+
123+
> **Platform-connector overrides cannot configure custom actions.** Overrides remap
124+
> `recommendedAction` between built-in enum values only (`RESTART_VM`, `REPLACE_VM`, …) and do not
125+
> set `customRecommendedAction`. If an override sets `recommendedAction: CUSTOM`, the custom string
126+
> stays empty and fault-remediation skips remediation as an unsupported action.
127+
128+
### Step 2 — Register the action in fault-remediation Helm values
129+
130+
Add your action and its template to
131+
`distros/kubernetes/nvsentinel/charts/fault-remediation/values.yaml`:
132+
133+
```yaml
134+
maintenance:
135+
actions:
136+
hardware-rma:
137+
apiGroup: remediation.example.com
138+
version: v1alpha1
139+
kind: RemediationRequest
140+
scope: Namespaced
141+
namespace: remediation
142+
completeConditionType: Complete
143+
templateFileName: hardware-rma.yaml
144+
equivalenceGroup: external-rma
145+
templates:
146+
hardware-rma.yaml: |
147+
apiVersion: {{ .ApiGroup }}/{{ .Version }}
148+
kind: RemediationRequest
149+
metadata:
150+
name: maintenance-{{ .HealthEvent.NodeName }}-{{ .HealthEventID }}
151+
namespace: {{ .Namespace }}
152+
labels:
153+
app.kubernetes.io/managed-by: nvsentinel
154+
spec:
155+
nodeName: {{ .HealthEvent.NodeName }}
156+
action: hardware-rma
157+
healthEventId: {{ .HealthEventID }}
158+
```
159+
160+
Key fields:
161+
162+
| Field | Purpose |
163+
|-------|---------|
164+
| `equivalenceGroup` | Base group name; combined with the impacted entity into the effective group used for dedup |
165+
| `completeConditionType` | Condition `type` fault-remediation waits for |
166+
| `supersedingEquivalenceGroups` | Optional — e.g. `restart` supersedes `external-rma` |
167+
| `impactedEntityScope` | Optional — per-entity (e.g. per-GPU) dedup; custom actions may set this |
168+
| `templates` | Go template rendered into the CR body |
169+
170+
> **RBAC:** fault-remediation auto-generates RBAC from `maintenance.actions` (resource name =
171+
> lowercase `kind` + `s`). Use CRD kinds with regular plurals (`RemediationRequest` →
172+
> `remediationrequests`); irregular plurals (`Policy` → `policys`) break RBAC.
173+
174+
### Step 3 — Template variables
175+
176+
Available in every maintenance template:
177+
178+
| Variable | Description |
179+
|----------|-------------|
180+
| `.NodeName` / `.HealthEvent.NodeName` | Target node |
181+
| `.HealthEventID` | Triggering event ID |
182+
| `.HealthEvent` | Full health event (all fields) |
183+
| `.RecommendedAction` | Numeric action code |
184+
| `.RecommendedActionName` | Resolved action name (`hardware-rma`, `RESTART_VM`, …) |
185+
| `.ImpactedEntityScopeValue` | GPU UUID for per-GPU actions |
186+
| `.ApiGroup`, `.Version`, `.Kind`, `.Namespace` | From the action config |
187+
| `.TraceID`, `.SpanID` | OpenTelemetry correlation (when tracing enabled) |
188+
189+
### Step 4 — Build your controller
190+
191+
Scaffold a controller that:
192+
193+
1. Watches your CR kind (`RemediationRequest`).
194+
2. Executes the repair (call a CSP API, open a support ticket, run diagnostics, …).
195+
3. Patches the status condition:
196+
197+
```yaml
198+
status:
199+
conditions:
200+
- type: Complete # must match completeConditionType
201+
status: "True" # "True" = done; "False" makes NVSentinel retry with a new CR
202+
reason: RepairSucceeded
203+
message: Node hardware repaired and validated
204+
```
205+
206+
**Golden rules** (same as any NVSentinel plugin):
207+
208+
1. Reconcile is **idempotent** — a no-op once `Complete=True`.
209+
2. **Make non-idempotent repairs durably idempotent.** A controller restart (or a retry after a
210+
`False` condition, which spawns a fresh CR) can re-run your repair before `Complete=True` is
211+
persisted. Guard one-shot side effects (opening a ticket, calling a CSP API) with a durable key
212+
such as `spec.healthEventId` or a status phase, so the effect happens **at most once**.
213+
3. Only set `Complete=True` when the repair is **actually** done; requeue until then. Set
214+
`Complete=False` only when you want NVSentinel to retry with a new CR.
215+
4. One CR per health event — NVSentinel manages its lifecycle via the equivalence group.
216+
217+
Build and push the controller image with Kubebuilder's Makefile, pointing `IMG` at a registry
218+
**your cluster can pull from**:
219+
220+
```bash
221+
docker login # once, to authenticate
222+
223+
# Replace YOUR_USER and my-remediation with your Docker Hub user and image name.
224+
export IMG=docker.io/YOUR_USER/my-remediation:dev # or your cluster's private registry
225+
make docker-build docker-push IMG=$IMG
226+
make install && make deploy IMG=$IMG
227+
```
228+
229+
> Use a **public** repo so the cluster can pull without credentials. For a private registry (e.g.
230+
> NVCR), add the cluster's `imagePullSecret` to the controller Deployment via `config/`.
231+
232+
For a full Kubebuilder scaffold, reconciler walkthrough, and one-shot AI prompt, see
233+
[Writing a Drain Plugin — Appendix: One-shot AI prompt](./writing-a-drain-plugin.md#appendix-one-shot-ai-prompt)
234+
— the pattern is the same (NVSentinel creates the CR and polls a status condition; your controller
235+
reconciles it and sets completion), even though the upstream stage is node-drainer instead of
236+
fault-remediation.
237+
238+
For a full working example (custom health monitor, CRD, controller, and Helm values), see the
239+
[local custom remediation demo](../../demos/local-custom-remediation-demo/README.md) — a memory-pressure
240+
monitor and reclaim controller on a local KIND cluster (no GPU required).
241+
242+
---
243+
244+
## Appendix: One-shot AI prompt
245+
246+
Paste this to an AI coding agent. It is **self-contained**: the controller is a standalone Kubebuilder
247+
project in **any repository**; produce Helm values snippets for fault-remediation registration
248+
(separate from the controller repo). Replace the bracketed parts.
249+
250+
```text
251+
Create a new NVSentinel custom remediation plugin named "[my-action]" (e.g. "hardware-rma") that
252+
runs [the repair, e.g. "opens a CSP support ticket and waits for hardware replacement"] after
253+
fault-remediation has cordoned and drained the node. Follow this spec exactly.
254+
255+
Architecture (direct CR plugin):
256+
- A health monitor emits recommendedAction=CUSTOM with customRecommendedAction="[my-action]".
257+
- fault-remediation looks up maintenance.actions["[my-action]"], renders a Go template into a
258+
Kubernetes CR, creates it, and polls status.conditions[] until completeConditionType is True.
259+
- Your controller watches that CR, performs the repair, and sets the completion condition.
260+
261+
Health event contract (emit from a health monitor):
262+
- Set BOTH recommendedAction=CUSTOM and customRecommendedAction="[my-action]" on the HealthEvent
263+
(protobuf field customRecommendedAction on datamodels.HealthEvent).
264+
- Platform-connector overrides cannot configure custom actions — they remap built-in
265+
recommendedAction enums only and do not set customRecommendedAction.
266+
267+
Controller (standalone Kubebuilder project, any repo):
268+
- Scaffold: kubebuilder init --domain example.com --repo github.com/<your-org>/[my-remediation]
269+
then kubebuilder create api --group remediation --version v1alpha1 --kind RemediationRequest
270+
--resource --controller.
271+
- Spec: nodeName (string), action (string), healthEventId (string) — minimal fields from the
272+
fault-remediation template below.
273+
- Status: conditions []metav1.Condition with a subresource.
274+
- Reconciler: idempotent; no-op once Complete=True; run the repair at most once (guard non-idempotent
275+
side effects with a durable key such as spec.healthEventId so a restart or retry cannot repeat them);
276+
patch status with Type="Complete", Status="True" when repaired (or "False" to make NVSentinel retry
277+
with a new CR) using append-or-update-by-type (meta.SetStatusCondition + Status().Update).
278+
RBAC: remediationrequests + status; add any API permissions your repair needs.
279+
- Build and push with the generated Dockerfile/Makefile, pointing IMG at a registry your cluster can
280+
pull from (e.g. docker.io/<your-user>/[my-remediation]:dev or a private registry such as NVCR).
281+
Run docker login once, then make docker-build docker-push IMG=$IMG. Use a public repo so the
282+
cluster can pull without credentials; for a private registry, add the cluster's imagePullSecret
283+
to the controller Deployment via config/. Deploy with make install && make deploy IMG=$IMG.
284+
285+
fault-remediation registration (Helm values snippet — merge into your NVSentinel release's
286+
fault-remediation chart values; exact file/chart path depends on your deployment):
287+
- Add under maintenance.actions:
288+
[my-action]:
289+
apiGroup: remediation.example.com
290+
version: v1alpha1
291+
kind: RemediationRequest
292+
scope: Namespaced
293+
namespace: remediation
294+
completeConditionType: Complete
295+
templateFileName: [my-action].yaml
296+
equivalenceGroup: [my-equivalence-group]
297+
- Add under maintenance.templates.[my-action].yaml a Go text/template body:
298+
apiVersion: {{ .ApiGroup }}/{{ .Version }}
299+
kind: RemediationRequest
300+
metadata:
301+
name: maintenance-{{ .HealthEvent.NodeName }}-{{ .HealthEventID }}
302+
namespace: {{ .Namespace }}
303+
labels:
304+
app.kubernetes.io/managed-by: nvsentinel
305+
spec:
306+
nodeName: {{ .HealthEvent.NodeName }}
307+
action: [my-action]
308+
healthEventId: {{ .HealthEventID }}
309+
- Template rules: plain Go text/template ONLY (no Sprig — no quote, trunc, or hash helpers).
310+
Use full {{ .HealthEvent.NodeName }}-{{ .HealthEventID }} for metadata.name. Do not template
311+
free-form message text into YAML.
312+
- Use a CRD kind with a regular plural (RemediationRequest -> remediationrequests); irregular
313+
plurals break fault-remediation's auto-generated RBAC.
314+
- Deploy the values via your usual NVSentinel Helm upgrade (e.g. helm upgrade <release>
315+
<chart> -n nvsentinel --reuse-values -f <overrides>.yaml), then wait for the fault-remediation
316+
Deployment rollout.
317+
318+
Verification (against a running NVSentinel cluster):
319+
- Trigger a health event with CUSTOM + customRecommendedAction="[my-action]" on a test node.
320+
- Confirm fault-remediation created the RemediationRequest (kubectl get remediationrequests -n
321+
remediation).
322+
- Confirm your controller reconciles it and sets status.conditions Type=Complete Status=True.
323+
- Confirm fault-remediation logs show the repair as complete and does not create a duplicate CR.
324+
325+
Ensure controller `go mod tidy` and `go build ./...` pass. Do not run docker push, helm upgrade,
326+
or kubectl apply unless credentials/cluster are available — but do create the Dockerfile, CRD,
327+
controller, and Helm values snippets.
328+
```

0 commit comments

Comments
 (0)