Tutorial: Writing a Preflight Check

View as Markdown

This tutorial walks you through building preflight-demo-gpu-visible — a preflight check that runs nvidia-smi -L, requires at least MIN_GPU_COUNT GPUs, reports via gRPC, and exits — from an empty Go module to a container image registered in the preflight webhook and verified on a GPU pod.

By the end you will have:

  • A working init-container check that validates GPUs are visible before the workload starts.
  • A container image for it, registered in the preflight Helm chart.
  • Verification that a failing check blocks pod startup and emits a health event.

Who is this for? Teams already running NVSentinel preflight who want to add a custom diagnostic (or ship a third-party image) without changing the webhook code.

Just want the AI to do it? Jump to Appendix: One-shot AI prompt.

Prerequisites

  • go 1.26+ and docker — build the check image.
  • kubectl with access to a cluster where:
    • NVSentinel is running (platform-connectors at minimum).
    • Preflight is enabled (global.preflight.enabled: true).
    • At least one namespace is labeled nvsentinel.nvidia.com/preflight=enabled.
    • You can schedule a GPU pod (device plugin or DRA).

See Preflight overview, ADR-026, and Preflight configuration for how preflight works. DEVELOPMENT.md covers local dev cluster setup.


1. Scaffold the module

Create a standalone Go module (it can live in any repository; you only need the image URI at deploy time):

$mkdir demo-preflight-check && cd demo-preflight-check
$go mod init github.com/nvidia/demo-preflight-check

Add the NVSentinel protobuf module (not published as tagged releases — pin a commit):

$export GOTOOLCHAIN=auto
$go get github.com/nvidia/nvsentinel/data-models@a3fb47d4

2. Write the check

Create main.go. Structure: parse env → run diagnostic → send health event → exit.

1package main
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "os"
8 "os/exec"
9 "strconv"
10 "strings"
11 "time"
12
13 pb "github.com/nvidia/nvsentinel/data-models/pkg/protos"
14 "google.golang.org/grpc"
15 "google.golang.org/grpc/credentials/insecure"
16 "google.golang.org/protobuf/types/known/timestamppb"
17)
18
19const (
20 agentName = "preflight-demo-gpu-visible"
21 componentClass = "Node"
22 checkName = "GPUVisibility"
23
24 exitSuccess = 0
25 exitTestFailed = 1
26 exitConfigError = 2
27 exitSendEventError = 3
28)
29
30func main() \{
31 os.Exit(run())
32\}
33
34func run() int \{
35 ctx := context.Background()
36
37 minGPUs, err := parsePositiveInt("MIN_GPU_COUNT", 1)
38 if err != nil \{
39 slog.Error("config error", "error", err)
40 return exitConfigError
41 \}
42
43 nodeName, err := requireEnv("NODE_NAME")
44 if err != nil \{
45 return exitConfigError
46 \}
47
48 socket, err := requireEnv("PLATFORM_CONNECTOR_SOCKET")
49 if err != nil \{
50 return exitConfigError
51 \}
52
53 strategy, err := parseProcessingStrategy()
54 if err != nil \{
55 return exitConfigError
56 \}
57
58 gpuLines, err := listGPUs(ctx)
59 healthy := err == nil && len(gpuLines) >= minGPUs
60
61 var message string
62 var errorCode string
63 if err != nil \{
64 message = fmt.Sprintf("nvidia-smi failed: %v", err)
65 errorCode = "NVIDIA_SMI_ERROR"
66 \} else if len(gpuLines) < minGPUs \{
67 message = fmt.Sprintf("expected >= %d GPUs, found %d", minGPUs, len(gpuLines))
68 errorCode = "NO_GPUS_VISIBLE"
69 \} else \{
70 message = fmt.Sprintf("found %d GPU(s): %s", len(gpuLines), strings.Join(gpuLines, "; "))
71 \}
72
73 exitCode := exitSuccess
74 if !healthy \{
75 exitCode = exitTestFailed
76 \}
77
78 if sendErr := sendEvent(ctx, socket, nodeName, strategy, healthy, healthy == false, message, errorCode); sendErr != nil \{
79 slog.Error("failed to send health event", "error", sendErr)
80 return exitSendEventError
81 \}
82
83 if exitCode != exitSuccess && strategy == pb.ProcessingStrategy_STORE_ONLY \{
84 slog.Warn("check failed (STORE_ONLY — not blocking pod)")
85 return exitSuccess
86 \}
87
88 return exitCode
89\}
90
91func listGPUs(ctx context.Context) ([]string, error) \{
92 ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
93 defer cancel()
94
95 cmd := exec.CommandContext(ctx, "nvidia-smi", "-L")
96 out, err := cmd.CombinedOutput()
97 if err != nil \{
98 return nil, fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out)))
99 \}
100
101 lines := strings.Split(strings.TrimSpace(string(out)), "\n")
102 var gpus []string
103 for _, line := range lines \{
104 line = strings.TrimSpace(line)
105 if line != "" \{
106 gpus = append(gpus, line)
107 \}
108 \}
109 return gpus, nil
110\}
111
112func sendEvent(ctx context.Context, socket, nodeName string, strategy pb.ProcessingStrategy,
113 isHealthy, isFatal bool, message, errorCode string) error \{
114
115 recommendedAction := pb.RecommendedAction_NONE
116 if !isHealthy \{
117 recommendedAction = pb.RecommendedAction_CONTACT_SUPPORT
118 \}
119
120 var codes []string
121 if errorCode != "" \{
122 codes = []string\{errorCode\}
123 \}
124
125 event := &pb.HealthEvent\{
126 Version: 1,
127 Agent: agentName,
128 ComponentClass: componentClass,
129 CheckName: checkName,
130 IsHealthy: isHealthy,
131 IsFatal: isFatal,
132 Message: message,
133 RecommendedAction: recommendedAction,
134 ErrorCode: codes,
135 GeneratedTimestamp: timestamppb.Now(),
136 NodeName: nodeName,
137 ProcessingStrategy: strategy,
138 \}
139
140 events := &pb.HealthEvents\{Version: 1, Events: []*pb.HealthEvent\{event\}\}
141
142 target := strings.TrimPrefix(socket, "unix://")
143 conn, err := grpc.NewClient("unix://"+target, grpc.WithTransportCredentials(insecure.NewCredentials()))
144 if err != nil \{
145 return err
146 \}
147 defer conn.Close()
148
149 client := pb.NewPlatformConnectorClient(conn)
150 ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
151 defer cancel()
152
153 _, err = client.HealthEventOccurredV1(ctx, events)
154 return err
155\}
156
157func parseProcessingStrategy() (pb.ProcessingStrategy, error) \{
158 raw := os.Getenv("PROCESSING_STRATEGY")
159 if raw == "" \{
160 raw = "EXECUTE_REMEDIATION"
161 \}
162 v, ok := pb.ProcessingStrategy_value[raw]
163 if !ok \{
164 return 0, fmt.Errorf("invalid PROCESSING_STRATEGY: %s", raw)
165 \}
166 return pb.ProcessingStrategy(v), nil
167\}
168
169func parsePositiveInt(key string, defaultVal int) (int, error) \{
170 raw := os.Getenv(key)
171 if raw == "" \{
172 return defaultVal, nil
173 \}
174 i, err := strconv.Atoi(raw)
175 if err != nil || i <= 0 \{
176 return 0, fmt.Errorf("%s must be a positive integer", key)
177 \}
178 return i, nil
179\}
180
181func requireEnv(key string) (string, error) \{
182 v := os.Getenv(key)
183 if v == "" \{
184 return "", fmt.Errorf("%s is required", key)
185 \}
186 return v, nil
187\}

Tidy and build (on a machine without nvidia-smi, go build still succeeds — the binary only shells out at runtime):

$go mod tidy
$go build ./...

For production code, copy the retry/backoff reporter from preflight-checks/nccl-loopback/pkg/health/reporter.go instead of the inline sendEvent above.


3. Containerize

1FROM public.ecr.aws/docker/library/golang:1.26.4-trixie AS builder
2WORKDIR /src
3COPY go.mod go.sum ./
4RUN go mod download
5COPY . .
6RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o demo-preflight-check .
7
8FROM nvcr.io/nvidia/cuda:12.8.1-base-ubuntu24.04
9COPY --from=builder /src/demo-preflight-check /demo-preflight-check
10ENTRYPOINT ["/demo-preflight-check"]

Build and push to a registry your cluster can pull from. Example using NGC (nvcr.io):

$docker login nvcr.io
$# Username: $oauthtoken
$# Password: <your NGC API key>
$
$docker build . -t nvcr.io/nv-ngc-devops/preflight-demo-gpu-visible:dev
$docker push nvcr.io/nv-ngc-devops/preflight-demo-gpu-visible:dev

4. Register and deploy

Add your check to distros/kubernetes/nvsentinel/charts/preflight/values.yaml:

1initContainers:
2 # ... existing checks ...
3 - name: preflight-demo-gpu-visible
4 defaultEnabled: false # opt-in via annotation while testing
5 image:
6 repository: nvcr.io/nv-ngc-devops/preflight-demo-gpu-visible
7 tag: "dev"
8 env:
9 - name: MIN_GPU_COUNT
10 value: "1"
11 volumeMounts:
12 - name: nvsentinel-socket
13 mountPath: /var/run

Upgrade the release:

$helm upgrade nvsentinel ./distros/kubernetes/nvsentinel \
> --namespace nvsentinel \
> --reuse-values \
> -f your-overrides.yaml
$
$kubectl rollout status deploy/preflight-injector -n nvsentinel

Because defaultEnabled: false, pods must select this check via annotation (see ADR-034):

1nvsentinel.nvidia.com/preflight-checks: "preflight-demo-gpu-visible"

5. Test and verify

Manual container smoke test

On a GPU node where platform-connectors is running (or docker run --gpus all on that host):

$docker run --rm --gpus all \
> -v /var/run/nvsentinel:/var/run \
> -e NODE_NAME=my-node \
> -e PLATFORM_CONNECTOR_SOCKET=unix:///var/run/nvsentinel.sock \
> -e PROCESSING_STRATEGY=STORE_ONLY \
> -e MIN_GPU_COUNT=1 \
> nvcr.io/nv-ngc-devops/preflight-demo-gpu-visible:dev
$echo "exit code: $?"

Verify on a GPU pod

Label the namespace so the preflight webhook injects init containers:

$kubectl label namespace training nvsentinel.nvidia.com/preflight=enabled --overwrite
$kubectl apply -f - <<'YAML'
$apiVersion: v1
$kind: Pod
$metadata:
$ name: preflight-demo-test
$ namespace: training
$ annotations:
$ nvsentinel.nvidia.com/preflight-checks: "preflight-demo-gpu-visible"
$spec:
$ restartPolicy: Never
$ containers:
$ - name: main
$ image: nvcr.io/nvidia/cuda:12.8.1-base-ubuntu24.04
$ command: ["sleep", "3600"]
$ resources:
$ limits:
$ nvidia.com/gpu: "1"
$YAML
$
$kubectl get pod preflight-demo-test -n training -w
$kubectl logs preflight-demo-test -n training -c preflight-demo-gpu-visible

To test the fail path, set MIN_GPU_COUNT unreasonably high in Helm, upgrade, and recreate the pod. The pod should stay Init:Error.

$kubectl delete pod preflight-demo-test -n training

E2E (in-repo checks only)

If you contributed the check under preflight-checks/ in the NVSentinel repo (build context and Makefile differ — see DEVELOPMENT.md):

$make -C preflight-checks/<your-check> lint-test
$cd tests && go test -tags=amd64_group -run TestPreflightEndToEnd -v ./...

Appendix: One-shot AI prompt

Paste this to an AI coding agent. It is self-contained: the check is a standalone Go module and container image that can live in any repository (it does not need to be inside the NVSentinel repo).

Create a new NVSentinel preflight check named "preflight-demo-gpu-visible" that validates GPU
visibility before a workload starts. It is a standalone Go program that can live in any repo;
follow this spec exactly.
Preflight checks are init containers injected by the NVSentinel preflight webhook into GPU pods.
They read config from env, run a diagnostic once, send a HealthEvent over gRPC to the
platform-connector Unix socket, then exit. Contract (do not duplicate long background prose in
output docs — just implement it):
- Exit codes: 0 pass, 1 check failed, 2 config error, 3 failed to send health event.
- Required webhook-injected env: NODE_NAME, PLATFORM_CONNECTOR_SOCKET (e.g.
unix:///var/run/nvsentinel.sock), PROCESSING_STRATEGY (EXECUTE_REMEDIATION or STORE_ONLY).
- On failure: send HealthEvent via PlatformConnector.HealthEventOccurredV1 (proto package
github.com/nvidia/nvsentinel/data-models/pkg/protos) before exiting non-zero.
- When PROCESSING_STRATEGY=STORE_ONLY: still send the unhealthy event, but exit 0 so the pod is
not blocked.
Implementation:
- Scaffold: mkdir demo-preflight-check && cd demo-preflight-check; go mod init
github.com/nvidia/demo-preflight-check; export GOTOOLCHAIN=auto (Go 1.26+); go get
github.com/nvidia/nvsentinel/data-models@a3fb47d4
- main.go: parse MIN_GPU_COUNT (default 1), NODE_NAME, PLATFORM_CONNECTOR_SOCKET,
PROCESSING_STRATEGY from env; run `nvidia-smi -L`; pass when visible GPU count >= MIN_GPU_COUNT;
send HealthEvent on both pass and fail with agent="preflight-demo-gpu-visible",
componentClass="Node", checkName="GPUVisibility", nodeName from NODE_NAME,
processingStrategy from PROCESSING_STRATEGY; on failure set isHealthy=false, isFatal=true,
recommendedAction=CONTACT_SUPPORT, errorCode NVIDIA_SMI_ERROR or NO_GPUS_VISIBLE; on success
isHealthy=true, isFatal=false, recommendedAction=NONE.
- Multi-stage Dockerfile (build context = demo-preflight-check directory):
builder public.ecr.aws/docker/library/golang:1.26.4-trixie, runtime
nvcr.io/nvidia/cuda:12.8.1-base-ubuntu24.04 (needs nvidia-smi), ENTRYPOINT the built binary.
- Build/push example (NGC):
docker login nvcr.io # Username: $oauthtoken, Password: <NGC API key>
docker build . -t nvcr.io/nv-ngc-devops/preflight-demo-gpu-visible:dev
docker push nvcr.io/nv-ngc-devops/preflight-demo-gpu-visible:dev
- Helm registration snippet for distros/kubernetes/nvsentinel/charts/preflight/values.yaml under
initContainers: name preflight-demo-gpu-visible, defaultEnabled: false, image.repository
nvcr.io/nv-ngc-devops/preflight-demo-gpu-visible, image.tag dev, env MIN_GPU_COUNT=1,
volumeMounts nvsentinel-socket -> /var/run. Upgrade:
helm upgrade nvsentinel ./distros/kubernetes/nvsentinel \
--namespace nvsentinel --reuse-values -f your-overrides.yaml
kubectl rollout status deploy/preflight-injector -n nvsentinel
- Verification manifest: GPU Pod in a preflight-enabled namespace (e.g. training) with annotation
nvsentinel.nvidia.com/preflight-checks: "preflight-demo-gpu-visible", nvidia.com/gpu: "1",
then kubectl logs on init container preflight-demo-gpu-visible.
Ensure `go mod tidy` and `go build ./...` pass. Do not run docker push, helm upgrade, or kubectl
apply unless credentials/cluster are available — but do create the Dockerfile and YAML snippets.