Skip to content

Commit bdcd708

Browse files
authored
Merge branch 'main' into fix/uat-helm-diff-checksum
2 parents 9be954b + 427c2ec commit bdcd708

50 files changed

Lines changed: 3055 additions & 136 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/e2e/action.yml

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -43,38 +43,98 @@ runs:
4343
- name: Install E2E testing tools
4444
uses: ./.github/actions/install-e2e-tools
4545

46-
- name: Create Kind cluster
47-
shell: bash
48-
run: make cluster-create
49-
50-
- name: Deploy aicrd to Kind
46+
- name: Create Kind cluster and deploy aicrd
5147
shell: bash
5248
env:
5349
GOFLAGS: -mod=vendor
5450
run: |
55-
# Build and push aicrd image (replaces Tilt custom_build)
51+
# If any `wait` below fails, `set -e` (GitHub's default shell flags)
52+
# exits this script immediately - any background job not yet reached
53+
# by its own `wait` would otherwise keep running. Most consequential
54+
# for pid_cluster specifically: the job's always()-run Cleanup step
55+
# (`make cluster-delete`) could then race a still-in-flight
56+
# `make cluster-create`. Kill whatever is still alive on any exit.
57+
cleanup_background_jobs() {
58+
local rc=$?
59+
for pid in "$pid_cluster" "$pid_dep" "$pid_perf" "$pid_conf" "$pid_cli" "$pid_aicr" "$pid_validators"; do
60+
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && kill "$pid" 2>/dev/null
61+
done
62+
exit "$rc"
63+
}
64+
trap cleanup_background_jobs EXIT
65+
66+
# This step folds what used to be two separate steps (Create Kind
67+
# cluster / Deploy aicrd) into one, so GitHub's per-step red-X no
68+
# longer says at a glance which phase failed. Group markers fold the
69+
# log into the same phases instead.
70+
echo "::group::cluster-and-builds"
71+
# cluster-create (~35s locally; Kind bring-up + registry) and the
72+
# host compiles below touch none of each other's inputs/outputs, so
73+
# launch everything in the background up front. Each later command
74+
# waits only on the specific PID(s) its own inputs actually depend
75+
# on: aicrd's ko build needs just the cluster/registry (not the
76+
# validator/CLI binaries), so it must not be gated behind builds it
77+
# doesn't read from - an earlier version of this step wait'ed on all
78+
# four build PIDs before it, which measured ~19s slower in CI than
79+
# the original sequential script for no reason: nothing here reads
80+
# dist/validator/* or dist/e2e/aicr until the docker build step below.
81+
pid_cluster=""; pid_dep=""; pid_perf=""; pid_conf=""; pid_cli=""; pid_aicr=""; pid_validators=""
82+
make cluster-create > /tmp/cluster-create.log 2>&1 &
83+
pid_cluster=$!
84+
85+
mkdir -p dist/validator dist/e2e
86+
for phase in deployment performance conformance; do
87+
mkdir -p "validators/${phase}/testdata"
88+
done
89+
90+
# Compile validator binaries + the host aicr CLI on the runner (Go
91+
# build cache hit) while the cluster comes up. This is also much
92+
# faster than building inside Docker (no cache sharing there). Go's
93+
# build cache is safe for concurrent use by simultaneous go
94+
# commands, including ko's own internal compile of aicrd below.
95+
CGO_ENABLED=0 go build -trimpath -o dist/validator/deployment ./validators/deployment &
96+
pid_dep=$!
97+
CGO_ENABLED=0 go build -trimpath -o dist/validator/performance ./validators/performance &
98+
pid_perf=$!
99+
CGO_ENABLED=0 go build -trimpath -o dist/validator/conformance ./validators/conformance &
100+
pid_conf=$!
101+
go build -o dist/e2e/aicr ./cmd/aicr &
102+
pid_cli=$!
103+
104+
# aicrd only needs the cluster/registry - build and push it (replaces
105+
# Tilt custom_build) as soon as that's ready, not gated behind the
106+
# unrelated validator/CLI builds above.
107+
#
108+
# cluster-create's output was redirected to a file above so it
109+
# wouldn't interleave with the concurrent builds; print it on
110+
# failure too (not just success), since a failed `wait` here is the
111+
# most common, most opaque failure on this step and would otherwise
112+
# exit via the EXIT trap with zero diagnostic output.
113+
wait $pid_cluster || { cat /tmp/cluster-create.log; exit 1; }
114+
cat /tmp/cluster-create.log
115+
echo "::endgroup::"
116+
117+
echo "::group::deploy-aicrd"
56118
KO_DOCKER_REPO=localhost:5001/aicrd ko build --bare --tags=tilt ./cmd/aicrd
57119
58120
# Apply namespace first (must exist before deployment references it)
59121
kubectl apply -f tilt/k8s/namespace.yaml
60122
kubectl apply -f tilt/k8s/
61-
62-
# Build images + CLI binary in parallel while deployment starts
123+
echo "::endgroup::"
124+
125+
echo "::group::validator-images"
126+
# The validator images COPY the binaries built above, so wait for
127+
# those specifically (not pid_cli, which nothing here needs) before
128+
# building them.
129+
wait $pid_dep
130+
wait $pid_perf
131+
wait $pid_conf
132+
133+
# Build + push the remaining images. Validator binaries are already
134+
# compiled above; this is just the COPY-only image build + push.
63135
KO_DOCKER_REPO=localhost:5001/aicr ko build --bare --tags=local ./cmd/aicr &
64136
pid_aicr=$!
65-
# Compile validator binaries on host (Go build cache hit) then COPY-only images.
66-
# This is much faster than building inside Docker (no cache sharing).
67137
(
68-
mkdir -p dist/validator
69-
CGO_ENABLED=0 go build -trimpath -o dist/validator/deployment ./validators/deployment &
70-
CGO_ENABLED=0 go build -trimpath -o dist/validator/performance ./validators/performance &
71-
CGO_ENABLED=0 go build -trimpath -o dist/validator/conformance ./validators/conformance &
72-
wait
73-
# Build per-phase COPY-only images and push to local registry.
74-
# Ensure testdata dirs exist (conformance has none).
75-
for phase in deployment performance conformance; do
76-
mkdir -p "validators/${phase}/testdata"
77-
done
78138
for phase in deployment performance conformance; do
79139
docker build -t "localhost:5001/aicr-validators/${phase}:latest" -f - . <<DOCKERFILE
80140
FROM nvcr.io/nvidia/distroless/static:v4.0.0@sha256:d90158b69e250d2018f32622b5c622925202ee97224a990a54b63811cb1e3d69
@@ -99,10 +159,14 @@ runs:
99159
docker push "localhost:5001/aicr-validators/aiperf-bench:latest"
100160
) &
101161
pid_validators=$!
102-
go build -o dist/e2e/aicr ./cmd/aicr &
103-
pid_cli=$!
104-
wait $pid_aicr $pid_validators $pid_cli
105-
162+
# Separate waits, not `wait $pid_aicr $pid_validators`: bash returns
163+
# only the LAST pid's exit status from a combined wait, so a failed
164+
# pid_aicr would be silently masked whenever pid_validators succeeds.
165+
wait $pid_aicr
166+
wait $pid_validators
167+
echo "::endgroup::"
168+
169+
echo "::group::rollout-and-verify"
106170
# Wait for deployment rollout (may already be ready)
107171
kubectl rollout status deployment/aicrd -n aicr --timeout=120s
108172
@@ -112,6 +176,13 @@ runs:
112176
curl -sf http://localhost:5001/v2/aicr-validators/performance/tags/list
113177
curl -sf http://localhost:5001/v2/aicr-validators/conformance/tags/list
114178
curl -sf http://localhost:5001/v2/aicr-validators/aiperf-bench/tags/list
179+
echo "::endgroup::"
180+
181+
# Not needed by anything above, but its output (dist/e2e/aicr) is
182+
# used by a later step (AICR_BIN) - wait now so a build failure here
183+
# fails this step instead of surfacing as a confusing missing-binary
184+
# error two steps later, and so the step's own exit code reflects it.
185+
wait $pid_cli
115186
116187
- name: Set up fake GPU environment
117188
shell: bash
@@ -151,6 +222,7 @@ runs:
151222
shell: bash
152223
run: |
153224
mkdir -p /tmp/debug-artifacts
225+
cp /tmp/cluster-create.log /tmp/debug-artifacts/cluster-create.log 2>/dev/null || true
154226
kubectl get all --all-namespaces > /tmp/debug-artifacts/all-resources.txt || true
155227
kubectl get events --all-namespaces --sort-by='.lastTimestamp' > /tmp/debug-artifacts/events.txt || true
156228
kubectl logs -n aicr -l app.kubernetes.io/name=aicrd --tail=500 > /tmp/debug-artifacts/aicrd-logs.txt || true

api/aicr/v1/server.yaml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2622,6 +2622,13 @@ components:
26222622
mode:
26232623
type: string
26242624
enum: [disabled, customer-managed, aicr-provided]
2625+
runtimeInventory:
2626+
type: object
2627+
required: [mode]
2628+
properties:
2629+
mode:
2630+
type: string
2631+
enum: [enabled, disabled]
26252632
componentRefs:
26262633
type: array
26272634
items:
@@ -2812,7 +2819,10 @@ components:
28122819
allOf:
28132820
- $ref: "#/components/schemas/RecipeResponseBase/properties/configuration"
28142821
- type: object
2815-
required: [slurm]
2822+
# At least one section, but not any particular one: a recipe may
2823+
# record a runtime-inventory selection without Slurm accounting, or
2824+
# the reverse. Requiring `slurm` rejected the former outright.
2825+
minProperties: 1
28162826
additionalProperties: false
28172827
properties:
28182828
slurm:
@@ -2828,6 +2838,14 @@ components:
28282838
mode:
28292839
type: string
28302840
enum: [disabled, customer-managed, aicr-provided]
2841+
runtimeInventory:
2842+
type: object
2843+
required: [mode]
2844+
additionalProperties: false
2845+
properties:
2846+
mode:
2847+
type: string
2848+
enum: [enabled, disabled]
28312849

28322850
ProfileRecipeResponse:
28332851
allOf:

docs/contributor/recipe.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,22 @@ spec:
345345
value: ">= v25.10.0"
346346
```
347347

348+
Host-managed driver floors (GKE COS / A4X Max and similar platforms where
349+
`driver.enabled: false`) use a separate deployment constraint,
350+
`Deployment.gpu-driver.version` (e.g. `">= 580.95.05"`).
351+
`check-nvidia-smi` evaluates it against the nvidia-smi banner on each
352+
verified node; when the constraint is absent the check keeps its
353+
banner-presence behavior and does not invent a floor (#1995).
354+
When the constraint is set but the host driver cannot be measured —
355+
unreadable nvidia-smi banner, no GPU nodes, all GPU nodes cordoned, or
356+
GPU nodes busy with workloads — the check fails closed rather than
357+
Skip. Skip on those paths is preserved only when no floor is
358+
configured. The value must carry a comparison operator (`>=`, `>`,
359+
`<=`, `<`) to behave as a floor; a bare version is exact string match,
360+
so a newer driver would fail. The constraint name is an exact match;
361+
a typo silently disables the floor (shared with
362+
`Deployment.gpu-operator.version`).
363+
348364
For a query `{service: eks, accelerator: gb200, intent: training}`,
349365
the resolver returns three independent maximal leaves —
350366
`gb200-eks-training` (matched by explicit criteria), `gb200-any`

docs/contributor/validator.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,11 @@ tagged with its cordon state, and:
611611
schedulable count: it is `0` on the all-cordoned and busy-skip
612612
paths (nothing was attempted yet) and the successful-node count on
613613
the failure path (a partial pass is not conflated with a full one).
614+
Exception: when the recipe declares `Deployment.gpu-driver.version`,
615+
those same all-cordoned and busy paths (and the no-GPU-nodes path)
616+
fail closed instead of Skip — a declared host-driver floor that
617+
cannot be measured must not PASS (#1995). Skip remains only when
618+
the floor constraint is absent.
614619
The `RESULT:` prefix is `pkg/validator/validator.go`'s
615620
`resultSummaryPrefix` convention: the validator runtime echoes the
616621
trailing text of any such stdout line into live CLI output via

docs/design/019-k8s-aibom-runtime-inventory.md

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -555,9 +555,9 @@ semantics.
555555

556556
## Follow-Up Decisions
557557

558-
Three of the six requirements below are resolved by
558+
Four of the six requirements below are resolved by
559559
[Amendment: stock adoption on one GKE recipe](#amendment-stock-adoption-on-one-gke-recipe);
560-
one remains open, and two are planned work tracked in the epic. The list is
560+
the remaining two are planned work tracked in the epic. The list is
561561
retained as originally written; the amendment records what changed, what
562562
remains open, and where the rest is tracked.
563563

@@ -634,25 +634,50 @@ cluster and establishing the pattern for later stock adoptions. This is
634634
recorded plainly rather than framed as customer demand, because the
635635
requirement exists to prevent adoption justified only by availability.
636636

637-
### Still open: selection and opt-out semantics
637+
### E. Selection and opt-out semantics
638638

639-
Unresolved and deliberately not decided here. `ComponentRef.IsEnabled()`
640-
already reads a recipe-recorded `enabled` override, which satisfies "recipe-
641-
recorded" and is not the bundle-time toggle Decision 2 rejects. What remains
642-
undecided is how a user generating from a stock recipe declines the
643-
component: by authoring a custom overlay using the existing mechanism, or by
644-
a generation-time flag that `aicr recipe` records into the emitted recipe.
639+
Resolved 2026-08-20. A **generation-time flag recorded in the emitted recipe**,
640+
modelled on the existing `--slurm-accounting-mode` selection rather than
641+
invented:
645642

646-
The second shape changes the CLI contract, so it is a decision rather than an
647-
implementation detail. It must be resolved before the overlay change in C
648-
merges, and whichever shape is chosen is recorded by amending this section.
643+
```bash
644+
aicr recipe ... --runtime-inventory disabled
645+
```
646+
647+
The selection is recorded as `configuration.runtimeInventory.mode`, the recipe's
648+
`apiVersion` becomes `ConfiguredRecipeResultAPIVersion`, and the component's ref
649+
carries `install: false`. `ComponentRef.IsEnabled()` already reads that key, so
650+
the component leaves the resolved set, the bundle, and deployment validation.
651+
652+
This satisfies Decision 2's specific objection. A bundle-time
653+
`--set k8s-aibom:enabled=false` was rejected because it changes neither the
654+
recipe nor its health checks; here both change, and the health-check half comes
655+
for free because the check lives on the component's own ref rather than on a
656+
sibling. That is simpler than the Slurm accounting precedent, which has to
657+
append and omit a check on a different component.
658+
659+
Passing the flag on a recipe that does not declare the component is an error,
660+
not a silent no-op. Selecting a mode there is a mistake — wrong criteria, a
661+
typo, a recipe that never carried it — and succeeding quietly would record a
662+
decision the recipe cannot honor. The check runs before the configuration is
663+
written, so a rejected build leaves no partial record.
664+
665+
The same selection is available in an `AICRConfig` document at
666+
`spec.recipe.configuration.runtimeInventory.mode`.
667+
668+
**Scope boundary worth naming.** This is the second entry under
669+
`RecipeConfiguration`, and the pattern is one bespoke selection per optional
670+
component. That is deliberate: this ADR asks for this component specifically,
671+
and a generic per-component disable would need a policy for which components
672+
may be declined at all — nothing should let a recipe decline `gpu-operator`.
673+
A third entry is the signal to revisit rather than extend by reflex.
649674

650675
### Requirement status
651676

652677
| Follow-Up requirement | Status |
653678
|---|---|
654679
| Exact recipe families in scope | Resolved — C |
655-
| Selection and opt-out semantics | **Open**see above |
680+
| Selection and opt-out semantics | ResolvedE |
656681
| Non-alpha storage API and migration policy | Resolved — A |
657682
| Concrete user-demand case | Resolved — D |
658683
| Managed-cluster qualification and measured cost | Planned — [#2271](https://github.com/NVIDIA/aicr/issues/2271) |

docs/integrator/go-library.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ func resolveCommittedConfig(ctx context.Context) (retErr error) {
721721
if err != nil {
722722
return err
723723
}
724-
opts, err := cfg.RecipeResolveOptions() // spec.recipe.profile + accounting mode
724+
opts, err := cfg.RecipeResolveOptions() // profile + accounting + runtime inventory
725725
if err != nil {
726726
return err
727727
}
@@ -769,8 +769,8 @@ derive step rather than the load step.
769769
| `BundleVerifyOptions()` | `spec.verify.policy` + `spec.verify.trust` |
770770
| `RecipeSource()` | `spec.recipe.data` |
771771
| `RecipeCriteria(reg)` | `spec.recipe.criteria` |
772-
| `RecipeResolveOptions()` | `spec.recipe.profile`, `spec.recipe.configuration.slurm.accounting.mode` |
773-
| `RecipeProfile()` / `RecipeAccountingMode()` | the same two, raw, for callers applying their own precedence first |
772+
| `RecipeResolveOptions()` | `spec.recipe.profile`, `spec.recipe.configuration.slurm.accounting.mode`, `spec.recipe.configuration.runtimeInventory.mode` |
773+
| `RecipeProfile()` / `RecipeAccountingMode()` / `RecipeRuntimeInventoryMode()` | the same three, raw, for callers applying their own precedence first |
774774
| `SnapshotPath()` | `spec.recipe.input.snapshot` |
775775
| `IsCriteriaStrict()` | `spec.recipe.criteriaStrict` |
776776

0 commit comments

Comments
 (0)