feat(recipe): adopt the ADR-015 gpuStack profile on AKS - #1967
Conversation
Recipe evidence checkProtected recipesRecipes with committed evidence (
Other affected recipes without evidence yet: 7These recipes are affected by this PR but carry no committed evidence pointer, so there is
How to refresh evidenceRun on a cluster matching the recipe's aicr snapshot -o snapshot.yaml
# Profiled families (AKS gpuStack): capture the pool projection and
# hydrate the recipe with the pointer's recorded 'profile:' selection
# first — validating the raw overlay resolves only the declaration
# default, and 'aicr validate' has no --profile flag:
# az aks nodepool list -g <rg> --cluster-name <cluster> -o json > pools.json
# aicr snapshot --aks-gpu-pools pools.json -o snapshot.yaml
# aicr recipe -s snapshot.yaml --intent <intent> [--platform <platform>] \
# --profile <name>=<value> -o recipe.yaml
# State the target leaf's intent/platform explicitly (the snapshot
# fingerprint supplies service/accelerator/OS but intent and platform
# default to 'any') and pass -r recipe.yaml below instead of the raw
# overlay.
aicr validate \
-r recipes/overlays/<slug>.yaml \
-s snapshot.yaml \
--emit-attestation ./out \
--push ghcr.io/<your-fork>/aicr-evidence
# Copy to the per-source path printed in the emit 'copyTo' hint:
# recipes/evidence/<slug>/<source>/<bundle-digest>.yamlThis gate is warning-only and never blocks merge. See ADR-007 for the trust model. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds the AKS Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/recipe/metadata.go (1)
807-822: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueLegacy string/scalar forms still accept an empty name.
The object forms now reject
name: "", but""(JSON string) and an empty!!strYAML scalar still produce anExcludedOverlaywith an emptyName. Consider applying the same non-empty check on the scalar/string branches so all four decode paths agree.♻️ Proposed change (JSON path)
var name string if err := json.Unmarshal(data, &name); err == nil { + if name == "" { + return errors.New(errors.ErrCodeInvalidRequest, + "excluded overlay requires a non-empty name") + } e.Name = name🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/recipe/metadata.go` around lines 807 - 822, Update the scalar/string decoding branches for ExcludedOverlay to reject empty names, matching the existing raw.Name validation for object forms. In the JSON path around json.Unmarshal into name, validate name before assigning e.Name; apply the equivalent non-empty check to the YAML scalar branch, while preserving successful decoding for non-empty values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/integrator/recipe-development.md`:
- Around line 425-428: Update docs/integrator/recipe-development.md lines
425-428 to identify AKS as the current first adopter through gpuStack rather
than a planned rollout. Update docs/design/015-recipe-configuration-profiles.md
lines 1261-1266 to remove or qualify the “no adopter” wording so it applies only
to the earlier core phase and reflects the delivered AKS adoption.
In `@pkg/bundler/deployer/argocdhelm/argocdhelm.go`:
- Around line 545-553: Update inspectProfileLockTemplate to detect a missing
outputPath using errors.Is with fs.ErrNotExist instead of os.IsNotExist, adding
the io/fs import as needed. Preserve the existing nil return for missing files
and wrapped internal error handling for all other open failures.
In `@pkg/recipe/profile_resolution.go`:
- Around line 182-228: Update the constraint-processing loop in the profile
resolution function to accumulate validated value constraints in a local slice
rather than appending directly to mergedSpec.Constraints. Preserve collision and
evaluation error returns, then append the local constraints to
mergedSpec.Constraints and sort only after the entire loop completes
successfully.
In `@pkg/recipe/profile.go`:
- Around line 580-610: Update the ownership validation loop over
selected.OwnedPaths to iterate components in lexicographically sorted key order,
using the established sorted-key pattern, while preserving all existing path and
component validation behavior.
In `@pkg/serializer/reader_test.go`:
- Around line 1621-1631: Add a non-strict YAML trailing-document test case
alongside the existing format cases in the relevant test table, mirroring the
legacy non-strict JSON case with multi-document YAML input and expected
acceptance. Preserve the existing strict YAML rejection and JSON cases.
In `@pkg/serializer/reader.go`:
- Around line 618-619: In readConfigMapDataWithKubeconfigContext, replace
defaults.ConfigMapWriteTimeout with a new read-specific
defaults.ConfigMapReadTimeout constant when creating the timeout context. Define
the new constant alongside the existing ConfigMapWriteTimeout value, preserving
the current timeout setup and cancellation behavior.
In `@pkg/server/bundle_handler.go`:
- Around line 103-132: In decodeBundleRecipe, resolve and validate the v2 body
format with v2BodyFormat(contentType) before calling io.ReadAll(input). Return
the format error immediately for unsupported media types, then read and decode
the body using the validated format while preserving the existing error
propagation.
In `@pkg/server/openapi_sync_test.go`:
- Around line 288-530: Split TestOpenAPIV2BundleContract into focused t.Run
subtests for the request/response contract, BundleRecipeV2Request,
ProfileRecipeResponse, RecipeResponseBase, LegacyRecipeResponse, and
VersionlessLegacyRecipeResponse. Keep shared spec loading and schema lookup
setup in the parent test, but move each independent assertion group into its
corresponding subtest so failures remain isolated and clearly named.
In `@pkg/server/recipe_handler_test.go`:
- Around line 688-695: Update the “recipe YAML profile without content type
preserves JSON default” case in the recipe handler tests to assert only the
stable “[INVALID_REQUEST] failed to parse JSON body” prefix, rather than the
verbatim encoding/json decoder message; keep the existing invalid-request
expectation and test setup unchanged.
---
Outside diff comments:
In `@pkg/recipe/metadata.go`:
- Around line 807-822: Update the scalar/string decoding branches for
ExcludedOverlay to reject empty names, matching the existing raw.Name validation
for object forms. In the JSON path around json.Unmarshal into name, validate
name before assigning e.Name; apply the equivalent non-empty check to the YAML
scalar branch, while preserving successful decoding for non-empty values.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: e3ec5c6c-6182-4d7e-a238-5c304c103877
📒 Files selected for processing (81)
api/aicr/v1/server.yamldocs/contributor/api-server.mddocs/contributor/recipe.mddocs/design/015-recipe-configuration-profiles.mddocs/integrator/data-flow.mddocs/integrator/go-library.mddocs/integrator/recipe-development.mddocs/user/api-reference.mddocs/user/cli-config.mddocs/user/cli-reference.mdpkg/bundler/bundler.gopkg/bundler/bundler_test.gopkg/bundler/config/config.gopkg/bundler/deployer/argocdhelm/argocdhelm.gopkg/bundler/deployer/argocdhelm/argocdhelm_test.gopkg/bundler/handler.gopkg/bundler/handler_test.gopkg/bundler/validations/checks.gopkg/bundler/validations/checks_test.gopkg/cli/consts.gopkg/cli/mirror.gopkg/cli/query.gopkg/cli/query_test.gopkg/cli/recipe.gopkg/cli/recipe_test.gopkg/client/v1/aicr.gopkg/client/v1/aicr_internal_test.gopkg/client/v1/aicr_test.gopkg/client/v1/bundle.gopkg/client/v1/gpu_driver_state.gopkg/client/v1/gpu_driver_state_test.gopkg/client/v1/stability_test.gopkg/client/v1/types.gopkg/component/overrides.gopkg/component/overrides_test.gopkg/config/accessors.gopkg/config/accessors_test.gopkg/config/config.gopkg/config/config_test.gopkg/config/validate.gopkg/evidence/project/synthesize.gopkg/evidence/project/synthesize_test.gopkg/mirror/discover.gopkg/mirror/discover_test.gopkg/recipe/builder.gopkg/recipe/catalog.gopkg/recipe/criteria.gopkg/recipe/criteria_test.gopkg/recipe/decode.gopkg/recipe/loader.gopkg/recipe/loader_provider_test.gopkg/recipe/loader_test.gopkg/recipe/metadata.gopkg/recipe/metadata_store.gopkg/recipe/metadata_store_test.gopkg/recipe/profile.gopkg/recipe/profile_aks_test.gopkg/recipe/profile_integration_test.gopkg/recipe/profile_resolution.gopkg/recipe/profile_test.gopkg/recipe/query.gopkg/recipe/query_request.gopkg/recipe/query_test.gopkg/recipe/testdata/profile-overlay/overlays/h100-eks-ubuntu-training-kubeflow.yamlpkg/recipe/testdata/profile-overlay/registry.yamlpkg/recipe/yaml_test.gopkg/serializer/reader.gopkg/serializer/reader_test.gopkg/server/bundle_handler.gopkg/server/bundle_handler_test.gopkg/server/consts.gopkg/server/doc.gopkg/server/openapi_sync_test.gopkg/server/recipe_handler.gopkg/server/recipe_handler_test.gopkg/server/serve.gopkg/server/serve_test.gopkg/server/server.gorecipes/overlays/aks.yamltools/testgrid-publish/bundle.gotools/testgrid-publish/bundle_test.go
5405138 to
de3f7fb
Compare
Add the K8s.aks-gpu-pools.gpu-driver reading (ADR-015 DD3): aicr snapshot --aks-gpu-pools <file> reads an operator-supplied 'az aks nodepool list -o json' dump and projects every GPU agent pool's gpuProfile.driver into the K8s measurement — Install (the AKS Driver-only preinstall, also the documented default when gpuProfile is absent), None (--gpu-driver none), Managed for fully AKS-managed pools, and Mixed when pools disagree. Managed, Mixed, and unknown values deliberately match no profile constraint, so profile-qualified resolution fails closed naming the observed state; no GPU pools omits the reading entirely. The file is explicit operator input, so every read or decode failure is an error rather than a degraded measurement: a typoed path must not masquerade as 'reading unavailable' and steer a profile decision. The read is size-bounded (os.Open + io.LimitReader, 1 MiB). Plumbing mirrors --cluster-config end to end: CLI flag, AgentConfig, Job-mode rejection (host path invisible in-pod; ConfigMap forwarding is the same follow-up), AICR_AKS_GPU_POOLS_PATH env for local agent mode, factory option, collector field. The reading is inert until a recipe declaration references it: no in-tree recipe does yet. The AKS gpuStack adoption (NVIDIA#1967) consumes it; the shared contract is pinned by TestReadingShapeMatchesProfileContract. Part of NVIDIA#1761 (rollout PR 2, projection half). Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/design/015-recipe-configuration-profiles.md (1)
147-171: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winRemove the obsolete “PR 2” constraint caveats.
The example says symmetric constraints are deferred, but this PR already makes both AKS profile values constrain
K8s.aks-gpu-pools.gpu-driver. Document the currentInstall/Noneconstraints here rather than leaving the profile contract incomplete.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/design/015-recipe-configuration-profiles.md` around lines 147 - 171, Update the gpuProfile values documentation in the recipe configuration profiles example to remove both obsolete “PR 2” caveats and explicitly document the current constraints for driver-only/Install and operator/None against K8s.aks-gpu-pools.gpu-driver, preserving the existing component overrides.
♻️ Duplicate comments (1)
docs/integrator/recipe-development.md (1)
425-428: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAlign profile rollout documentation with AKS adoption.
These passages still describe a future/no-adopter rollout, while this PR adopts
gpuStackfor AKS. Update them consistently, while retaining that snapshot-qualified AKS resolution remains fail-closed until the pool-projection collector lands.
docs/integrator/recipe-development.md#L425-L428: identify AKS as the current first adopter.docs/integrator/recipe-development.md#L487-L491: describe thegpuProfile.driverprojection as planned and document current fail-closed behavior.docs/user/api-reference.md#L407-L418: remove the claim that no embedded recipe declares a profile and update the v2 example.docs/user/api-reference.md#L463-L464: remove the “after its profile adopter lands” wording or qualify it as a future adopter.docs/user/cli-reference.md#L336-L345: replace the no-adopter/external-overlay rollout description with the current AKS adoption status.Based on the PR objectives, AKS is the first
gpuStackadopter in this rollout.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/integrator/recipe-development.md` around lines 425 - 428, Align the profile rollout documentation with AKS as the first current gpuStack adopter: update docs/integrator/recipe-development.md lines 425-428 to identify AKS, and lines 487-491 to mark gpuProfile.driver projection as planned while documenting fail-closed snapshot-qualified AKS resolution; update docs/user/api-reference.md lines 407-418 to remove the no-adopter claim and revise the v2 example, and lines 463-464 to remove or qualify the future-adopter wording; update docs/user/cli-reference.md lines 336-345 to describe current AKS adoption instead of a no-adopter/external-overlay rollout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@api/aicr/v1/server.yaml`:
- Around line 1932-1994: The criteria enums are duplicated across the v1 inline
parameters, shared components, Criteria, and CriteriaV2; consolidate them into
single reusable definitions. Update the v1 path parameters to reference the
corresponding components/parameters entries, move their detailed descriptions
into those shared definitions, and define CriteriaV2 from Criteria using allOf
with additionalProperties: false so all versions share the same enum sources.
In `@pkg/config/config.go`:
- Line 108: Document the Profile field with a Go doc comment stating that it
expects the strict name=value form accepted by recipe.ParseProfileSelection,
with names and values limited to letters, digits, dots, underscores, and
hyphens. Place the comment directly above Profile, matching the documentation
style of CriteriaStrict.
In `@pkg/config/validate.go`:
- Around line 108-111: The profile validation in the recipe validation flow must
preserve the field context when ParseProfileSelection returns an existing coded
error. Update the handling around ParseProfileSelection to explicitly attach or
prepend “invalid spec.recipe.profile” while retaining the original error code
and detail, rather than relying on PropagateOrWrap’s fallback message.
In `@pkg/recipe/profile_aks_test.go`:
- Around line 97-132: Strengthen the assertions in the profile test around
operator override extraction and constraint matching: require successful type
assertions for the driver, toolkit, and operator maps before checking their
fields, so missing overrides fail explicitly. In the constraint loop, count
every K8s.aks-gpu-pools.gpu-driver match, retain the matched value for
comparison, and require exactly one match before validating it against
tt.wantConstraint.
In `@pkg/recipe/profile_integration_test.go`:
- Around line 189-193: Update the comparison around
first.Metadata.SelectedProfile and second.Metadata.SelectedProfile to validate
both SelectedProfile values are non-nil before accessing OwnedPaths; report a
clear test failure if either selection is missing, then preserve the existing
owned-path equality assertion.
In `@pkg/server/openapi_sync_test.go`:
- Around line 342-347: Guard the LegacyBundleRecipeV2Request allOf access by
asserting that legacyBranchAllOf has exactly two entries before indexing [1],
and report a named contract failure through the test assertion. Keep the
existing legacyOverlay required-field validation unchanged after the length
check.
In `@pkg/server/recipe_handler.go`:
- Around line 143-159: Extract the duplicated bounded POST-body reading and
size-limit response logic from the current handler and parseQueryPOSTBody into a
shared helper such as readBoundedPOSTBody, preserving MaxBytesReader setup,
drain/close cleanup, MaxBytesError logging, and the 413 response with
keyLimitBytes. Update both callers to use the helper and retain their existing
handling for non-size-limit read errors.
- Around line 616-631: Review the v2 body validation flow around
validateV2EnvelopeProfile and decodeStrictV2Envelope to confirm whether the
second full-payload parse via bodyHasTopLevelProfile is necessary. If the
decoded envelope can expose omitted versus explicit null profile states, carry
that distinction through a Profile *json.RawMessage or *yaml.Node field and
remove the redundant parse; otherwise document or preserve the intentional
bounded double-parse behavior.
---
Outside diff comments:
In `@docs/design/015-recipe-configuration-profiles.md`:
- Around line 147-171: Update the gpuProfile values documentation in the recipe
configuration profiles example to remove both obsolete “PR 2” caveats and
explicitly document the current constraints for driver-only/Install and
operator/None against K8s.aks-gpu-pools.gpu-driver, preserving the existing
component overrides.
---
Duplicate comments:
In `@docs/integrator/recipe-development.md`:
- Around line 425-428: Align the profile rollout documentation with AKS as the
first current gpuStack adopter: update docs/integrator/recipe-development.md
lines 425-428 to identify AKS, and lines 487-491 to mark gpuProfile.driver
projection as planned while documenting fail-closed snapshot-qualified AKS
resolution; update docs/user/api-reference.md lines 407-418 to remove the
no-adopter claim and revise the v2 example, and lines 463-464 to remove or
qualify the future-adopter wording; update docs/user/cli-reference.md lines
336-345 to describe current AKS adoption instead of a
no-adopter/external-overlay rollout.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: c228e937-e084-48a5-a3e1-0dcf796fb56d
📒 Files selected for processing (81)
api/aicr/v1/server.yamldocs/contributor/api-server.mddocs/contributor/recipe.mddocs/design/015-recipe-configuration-profiles.mddocs/integrator/data-flow.mddocs/integrator/go-library.mddocs/integrator/recipe-development.mddocs/user/api-reference.mddocs/user/cli-config.mddocs/user/cli-reference.mdpkg/bundler/bundler.gopkg/bundler/bundler_test.gopkg/bundler/config/config.gopkg/bundler/deployer/argocdhelm/argocdhelm.gopkg/bundler/deployer/argocdhelm/argocdhelm_test.gopkg/bundler/handler.gopkg/bundler/handler_test.gopkg/bundler/validations/checks.gopkg/bundler/validations/checks_test.gopkg/cli/consts.gopkg/cli/mirror.gopkg/cli/query.gopkg/cli/query_test.gopkg/cli/recipe.gopkg/cli/recipe_test.gopkg/client/v1/aicr.gopkg/client/v1/aicr_internal_test.gopkg/client/v1/aicr_test.gopkg/client/v1/bundle.gopkg/client/v1/gpu_driver_state.gopkg/client/v1/gpu_driver_state_test.gopkg/client/v1/stability_test.gopkg/client/v1/types.gopkg/component/overrides.gopkg/component/overrides_test.gopkg/config/accessors.gopkg/config/accessors_test.gopkg/config/config.gopkg/config/config_test.gopkg/config/validate.gopkg/evidence/project/synthesize.gopkg/evidence/project/synthesize_test.gopkg/mirror/discover.gopkg/mirror/discover_test.gopkg/recipe/builder.gopkg/recipe/catalog.gopkg/recipe/criteria.gopkg/recipe/criteria_test.gopkg/recipe/decode.gopkg/recipe/loader.gopkg/recipe/loader_provider_test.gopkg/recipe/loader_test.gopkg/recipe/metadata.gopkg/recipe/metadata_store.gopkg/recipe/metadata_store_test.gopkg/recipe/profile.gopkg/recipe/profile_aks_test.gopkg/recipe/profile_integration_test.gopkg/recipe/profile_resolution.gopkg/recipe/profile_test.gopkg/recipe/query.gopkg/recipe/query_request.gopkg/recipe/query_test.gopkg/recipe/testdata/profile-overlay/overlays/h100-eks-ubuntu-training-kubeflow.yamlpkg/recipe/testdata/profile-overlay/registry.yamlpkg/recipe/yaml_test.gopkg/serializer/reader.gopkg/serializer/reader_test.gopkg/server/bundle_handler.gopkg/server/bundle_handler_test.gopkg/server/consts.gopkg/server/doc.gopkg/server/openapi_sync_test.gopkg/server/recipe_handler.gopkg/server/recipe_handler_test.gopkg/server/serve.gopkg/server/serve_test.gopkg/server/server.gorecipes/overlays/aks.yamltools/testgrid-publish/bundle.gotools/testgrid-publish/bundle_test.go
Add the K8s.aks-gpu-pools.gpu-driver reading (ADR-015 DD3): aicr snapshot --aks-gpu-pools <file> reads an operator-supplied 'az aks nodepool list -o json' dump and projects every GPU agent pool's gpuProfile.driver into the K8s measurement — Install (the AKS Driver-only preinstall, also the documented default when gpuProfile is absent), None (--gpu-driver none), Managed for fully AKS-managed pools, and Mixed when pools disagree. Managed, Mixed, and unknown values deliberately match no profile constraint, so profile-qualified resolution fails closed naming the observed state; no GPU pools omits the reading entirely. The file is explicit operator input, so every read or decode failure is an error rather than a degraded measurement: a typoed path must not masquerade as 'reading unavailable' and steer a profile decision. The read is size-bounded (os.Open + io.LimitReader, 1 MiB). Plumbing mirrors --cluster-config end to end: CLI flag, AgentConfig, Job-mode rejection (host path invisible in-pod; ConfigMap forwarding is the same follow-up), AICR_AKS_GPU_POOLS_PATH env for local agent mode, factory option, collector field. The reading is inert until a recipe declaration references it: no in-tree recipe does yet. The AKS gpuStack adoption (NVIDIA#1967) consumes it; the shared contract is pinned by TestReadingShapeMatchesProfileContract. Part of NVIDIA#1761 (rollout PR 2, projection half). Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
Add the K8s.aks-gpu-pools.gpu-driver reading (ADR-015 DD3): aicr snapshot --aks-gpu-pools <file> reads an operator-supplied 'az aks nodepool list -o json' dump and projects every GPU agent pool's gpuProfile.driver into the K8s measurement — Install (the AKS Driver-only preinstall, also the documented default when gpuProfile is absent), None (--gpu-driver none), Managed for fully AKS-managed pools, and Mixed when pools disagree. Managed, Mixed, and unknown values deliberately match no profile constraint, so profile-qualified resolution fails closed naming the observed state; no GPU pools omits the reading entirely. The file is explicit operator input, so every read or decode failure is an error rather than a degraded measurement: a typoed path must not masquerade as 'reading unavailable' and steer a profile decision. The read is size-bounded (os.Open + io.LimitReader, 1 MiB). Plumbing mirrors --cluster-config end to end: CLI flag, AgentConfig, Job-mode rejection (host path invisible in-pod; ConfigMap forwarding is the same follow-up), AICR_AKS_GPU_POOLS_PATH env for local agent mode, factory option, collector field. The reading is inert until a recipe declaration references it: no in-tree recipe does yet. The AKS gpuStack adoption (NVIDIA#1967) consumes it; the shared contract is pinned by TestReadingShapeMatchesProfileContract. Part of NVIDIA#1761 (rollout PR 2, projection half). Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
d77914a to
99bf520
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pkg/client/v1/aicr_test.go (1)
872-878: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winUnchecked nested assertion panics instead of failing readably.
If the profile fragment stops applying,
values["driver"]is nil and Line 876 panics rather than reporting the mismatch.🛡️ Proposed fix
- if enabled := values["driver"].(map[string]any)["enabled"]; enabled != true { - t.Fatalf("driver.enabled = %v, want true", enabled) + driver, ok := values["driver"].(map[string]any) + if !ok { + t.Fatalf("gpu-operator values.driver = %#v, want map", values["driver"]) + } + if enabled := driver["enabled"]; enabled != true { + t.Fatalf("driver.enabled = %v, want true", enabled) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/client/v1/aicr_test.go` around lines 872 - 878, Update the assertions in the test around GetValuesForComponentWithContext to avoid unchecked nested type assertions on values["driver"]. Safely validate that the driver entry exists and has the expected map shape before checking enabled, and fail the test with a readable mismatch instead of allowing a panic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@pkg/client/v1/aicr_test.go`:
- Around line 872-878: Update the assertions in the test around
GetValuesForComponentWithContext to avoid unchecked nested type assertions on
values["driver"]. Safely validate that the driver entry exists and has the
expected map shape before checking enabled, and fail the test with a readable
mismatch instead of allowing a panic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: 33e848e7-94b4-4bd8-b6ba-4d9fc0cdda4d
📒 Files selected for processing (9)
docs/integrator/recipe-development.mdpkg/client/v1/aicr_test.gopkg/recipe/loader_provider_test.gopkg/recipe/profile_aks_test.gopkg/recipe/profile_integration_test.gopkg/recipe/testdata/profile-overlay/overlays/h100-eks-ubuntu-training-kubeflow.yamlpkg/recipe/yaml_test.gopkg/server/recipe_handler_test.gorecipes/overlays/aks.yaml
Add the K8s.aks-gpu-pools.gpu-driver reading (ADR-015 DD3): aicr snapshot --aks-gpu-pools <file> reads an operator-supplied 'az aks nodepool list -o json' dump and projects every GPU agent pool's gpuProfile.driver into the K8s measurement — Install (the AKS Driver-only preinstall, also the documented default when gpuProfile is absent), None (--gpu-driver none), Managed for fully AKS-managed pools, and Mixed when pools disagree. Managed, Mixed, and unknown values deliberately match no profile constraint, so profile-qualified resolution fails closed naming the observed state; no GPU pools omits the reading entirely. The file is explicit operator input, so every read or decode failure is an error rather than a degraded measurement: a typoed path must not masquerade as 'reading unavailable' and steer a profile decision. The read is size-bounded (os.Open + io.LimitReader, 1 MiB). Plumbing mirrors --cluster-config end to end: CLI flag, AgentConfig, Job-mode rejection (host path invisible in-pod; ConfigMap forwarding is the same follow-up), AICR_AKS_GPU_POOLS_PATH env for local agent mode, factory option, collector field. The reading is inert until a recipe declaration references it: no in-tree recipe does yet. The AKS gpuStack adoption (NVIDIA#1967) consumes it; the shared contract is pinned by TestReadingShapeMatchesProfileContract. Part of NVIDIA#1761 (rollout PR 2, projection half). Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
Add the K8s.aks-gpu-pools.gpu-driver reading (ADR-015 DD3): aicr snapshot --aks-gpu-pools <file> reads an operator-supplied 'az aks nodepool list -o json' dump and projects every GPU agent pool's gpuProfile.driver into the K8s measurement — Install (the AKS Driver-only preinstall, also the documented default when gpuProfile is absent), None (--gpu-driver none), Managed for fully AKS-managed pools, and Mixed when pools disagree. Managed, Mixed, and unknown values deliberately match no profile constraint, so profile-qualified resolution fails closed naming the observed state; no GPU pools omits the reading entirely. The file is explicit operator input, so every read or decode failure is an error rather than a degraded measurement: a typoed path must not masquerade as 'reading unavailable' and steer a profile decision. The read is size-bounded (os.Open + io.LimitReader, 1 MiB). Plumbing mirrors --cluster-config end to end: CLI flag, AgentConfig, Job-mode rejection (host path invisible in-pod; ConfigMap forwarding is the same follow-up), AICR_AKS_GPU_POOLS_PATH env for local agent mode, factory option, collector field. The reading is inert until a recipe declaration references it: no in-tree recipe does yet. The AKS gpuStack adoption (NVIDIA#1967) consumes it; the shared contract is pinned by TestReadingShapeMatchesProfileContract. Part of NVIDIA#1761 (rollout PR 2, projection half). Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
99bf520 to
20ee657
Compare
Add the K8s.aks-gpu-pools.gpu-driver reading (ADR-015 DD3): aicr snapshot --aks-gpu-pools <file> reads an operator-supplied 'az aks nodepool list -o json' dump and projects every GPU agent pool's gpuProfile.driver into the K8s measurement — Install (the AKS Driver-only preinstall, also the documented default when gpuProfile is absent), None (--gpu-driver none), Managed for fully AKS-managed pools, and Mixed when pools disagree. Managed, Mixed, and unknown values deliberately match no profile constraint, so profile-qualified resolution fails closed naming the observed state; no GPU pools omits the reading entirely. The file is explicit operator input, so every read or decode failure is an error rather than a degraded measurement: a typoed path must not masquerade as 'reading unavailable' and steer a profile decision. The read is size-bounded (os.Open + io.LimitReader, 1 MiB). Plumbing mirrors --cluster-config end to end: CLI flag, AgentConfig, Job-mode rejection (host path invisible in-pod; ConfigMap forwarding is the same follow-up), AICR_AKS_GPU_POOLS_PATH env for local agent mode, factory option, collector field. The reading is inert until a recipe declaration references it: no in-tree recipe does yet. The AKS gpuStack adoption (NVIDIA#1967) consumes it; the shared contract is pinned by TestReadingShapeMatchesProfileContract. Part of NVIDIA#1761 (rollout PR 2, projection half). Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
345c8e0 to
3426861
Compare
a35c5dc to
2054780
Compare
a5b1a96 to
ae5fbe4
Compare
ae5fbe4 to
66542da
Compare
njhensley
left a comment
There was a problem hiding this comment.
Review — ADR-015 gpuStack profile on AKS
Method: five independent persona reviewers (Correctness, Security/fail-closed, Domain & Architecture, Operability/CI-DX, Test-coverage) followed by an adversarial senior meta-reviewer that independently re-derived every finding from the resolved code. Reviewed at 66542dac; re-verified byte-identical to the current head after a rebase onto main (the newer commits are already-merged #1973 base code, not part of this PR's diff).
Overall assessment
This is an unusually well-defended change. The projection, the bounded pool-file reader, the evidence identity binding, and the ownership lock are all hardened correctly, and the new logic is thoroughly test-pinned. No blocker or major defect survived adjudication. The one issue every reviewer converged on — a ConfigMap-rewrite failure path — was retiered from Major to Minor once the writer's Server-Side-Apply (create-or-update) semantics were confirmed: it's a narrow, transient-only robustness issue, not a deterministic bug.
CI at the reviewed commit: green across Tier 1 (all deployers × aks/aks-training/aks-inference), GPU nvkind H100/L40G, Mirror E2E, pointer-contract, CodeRabbit, and Fern.
Recommendation: Approve with comments — the 🟡 is worth addressing (or consciously accepting); the rest are optional cleanups.
Findings (detail inline)
- 🟡 Minor —
pkg/snapshotter/agent.go:321: internal ConfigMap-rewrite failure aborts snapshot delivery for file/stdout output. - 🔵 Nitpick —
pkg/snapshotter/agent.go:735:rawSnapshotDochelpers 0%-covered. - 🔵 Nitpick —
pkg/client/v1/gpu_driver_state.go:73:driverAbsentRemedytwin, 4/6 branches untested. - 🔵 Nitpick —
pkg/snapshotter/snapshot.go:367:attach/mergeAKSGPUPoolsappend without dedup (defensive). - 🔵 Nitpick —
.github/workflows/uat-azure.yaml:526: pool-dump step lacks theazretry the client-connect step has. - 🔵 Nitpick — stale "driver-only" terminology survives in comments/test names on pre-existing files this PR doesn't modify (
pkg/recipe/aks_driver_profile_tuple_test.go:22,toolkit_hardening_gate_test.go:55/70,pkg/client/v1/aicr.go:754). Functional values areazure-managed/operator-managedthroughout; cosmetic only, safe to defer.
Examined and cleared (confirmed non-issues)
- Projection semantics (
aksgpupools.go): AMD markers short-circuit before NC/ND/NV prefix match; nullgpuProfile→Install;managementMode: Managed;Unmanaged→driver field;Mixedon disagreement; no-GPU omission; top-level JSONnullrejected. - Bounded reader (
providerpools.go): descriptor-firstO_NOFOLLOW|O_NONBLOCK,IsRegularon the opened fd (no TOCTOU), dual size cap,ctx.Done()between chunks underFileReadTimeout. - Evidence identity binding: the name-collision spoof is genuinely closed — name/profile/digest derived from manifest-verified recipe bytes after
CheckInventory; suffix checks are fast-fail only. - Ownership lock: runs on final
componentValuespost---set;--dynamicintersection rejected even when identical; argocd-helm install-time values covered — not bypassable. azure-managed= pre-PR default (byte-identical effective values); the breaking change is the added override-lock surface +/v1→/v2cut-over, both documented.- Selection-collision quarantine is order-independent; the profile segment is stripped before criteria inversion (no phantom platform).
- No GHA template injection in the dump step; env-var propagation wired on both
snapshotandvalidateflags;recipe-evidence-check.shSIGPIPE fix present.
Tier tally
🔴 Blocker 0 · 🟠 Major 0 · 🟡 Minor 1 · 🔵 Nitpick 5
| // "reading unavailable" on a cluster whose operator supplied the | ||
| // pool file. This also covers the user-requested cm:// output | ||
| // (agentOutput is that URI in that case). | ||
| if err := rewriteSnapshotConfigMap(ctx, agentOutput, config.Kubeconfig, snapshotData); err != nil { |
There was a problem hiding this comment.
🟡 Minor — ConfigMap-rewrite failure aborts snapshot delivery for file/stdout output
When --aks-gpu-pools is set, deployAndWaitForResult merges the projection then hard-returns on any rewriteSnapshotConfigMap error (this line) before control reaches the file/stdout write switch in measureWithAgent (agent.go:677-698) or the return in DeployAndGetSnapshot. agentOutput is the internal cm://<ns>/aicr-snapshot URI for every case except an explicit cm:// output request, so for -o file, stdout, and every aicr validate live-capture (which consumes the returned struct, not that ConfigMap), a failed Apply to that internal ConfigMap discards an already-captured-and-merged snapshot. The rewrite is hygiene on a ConfigMap that Cleanup orphans anyway (per the comment just above).
Blast radius: The new UAT-Azure lane exports AICR_AKS_GPU_POOLS_PATH job-wide, so every aicr validate there rewrites an internal ConfigMap it never consumes — one transient apiserver throttle/409 reds an otherwise-green burn-in. For end users, aicr snapshot --aks-gpu-pools pools.json -o snapshot.yaml produces no file despite a successful capture. Kept at Minor (not Major): the writer uses Server-Side Apply (create-or-update) with the same identity that just succeeded reading the ConfigMap one step earlier, so the failure is transient and retryable, not deterministic.
Fix: Make the internal-ConfigMap rewrite best-effort (slog.Warn + continue) when the ConfigMap is not the user's requested deliverable; keep it fatal only when the user explicitly requested cm:// output (thread finalOutput/an isDeliverable flag into deployAndWaitForResult).
There was a problem hiding this comment.
Fixed in a907d81. The rewrite now fails the run only when the ConfigMap is the delivery vehicle (explicit cm:// output, where a later consumer reads the ConfigMap). For file/stdout output and SDK callers, a rewrite failure logs a loud warning and the run delivers the returned snapshot, which already carries the merged reading; the warning names the orphaned pre-merge ConfigMap. deployAndWaitForResult takes an explicit deliverViaConfigMap flag from both call sites.
| } | ||
|
|
||
| //nolint:unparam // the (any, error) shape is yaml.Marshaler's fixed contract | ||
| func (r rawSnapshotDoc) MarshalYAML() (any, error) { return r.doc, nil } |
There was a problem hiding this comment.
🔵 Nitpick — rawSnapshotDoc.MarshalYAML/GetKind/GetMetadata are 0%-covered
These three methods are only reachable through the live ConfigMap-rewrite path; TestRewriteSnapshotConfigMapRejectsBadInput exercises only the two early-return guards before the struct is constructed. They populate the rewritten ConfigMap's kind/metadata labels — the version-skew preservation this whole path protects — so a wrong key or type-assertion would silently write empty labels, uncaught.
Blast radius: Low: unexported methods on an unexported type (no coverage-gate block). A regression in label preservation would only surface downstream.
Fix: Add a cluster-free table test that builds a rawSnapshotDoc{doc: {...}} and asserts GetKind/GetMetadata/MarshalYAML round-trip.
There was a problem hiding this comment.
Added TestRawSnapshotDocRoundTrip in a907d81: cluster-free table covering GetKind/GetMetadata/MarshalYAML round-trip on a representative document, non-string metadata values being skipped (not stringified), and mistyped kind/metadata degrading to empty without panicking.
| // anything else gets the generic reprovision wording plus the override | ||
| // set. | ||
| func driverAbsentRemedy(service recipe.CriteriaServiceType, os recipe.CriteriaOSType) string { | ||
| func driverAbsentRemedy(service recipe.CriteriaServiceType, os recipe.CriteriaOSType, profiled bool) string { |
There was a problem hiding this comment.
🔵 Nitpick — client-side driverAbsentRemedy has 4 of 6 branches untested (twin can silently diverge)
driverAbsentRemedy is a deliberate twin of the bundler copy (pkg/bundler/validations/checks.go:400) and the two are currently byte-identical (verified, 51 lines each). The bundler twin is fully branch-tested; this copy exercises only the AKS-profiled and GKE-COS branches (via TestResolveRecipeFromSnapshot_GPUDriverAutoDetect), leaving legacy-!profiled-AKS, GKE-Ubuntu, GKE-default, and generic untested. The CLAUDE.md 'keep both copies in sync' contract is thus test-enforced on one side only.
Blast radius: Low: output is advisory slog.Warn remedy text, no fail-closed logic. But the twins could drift on the legacy-AKS/GKE-Ubuntu/generic wording without a test catching it.
Fix: Add a small direct table test over {service, os, profiled} asserting a stable substring per branch, mirroring the bundler's existing rows.
There was a problem hiding this comment.
Added TestDriverAbsentRemedyBranches in a907d81: a direct table over all six {service, os, profiled} branches with a stable substring per branch, plus two twin-drift guards - the profiled AKS remedy must differ from the legacy one and must not offer the bundle-time tuple.
| func attachAKSGPUPools(snap *Snapshot, subtype measurement.Subtype) { | ||
| for _, m := range snap.Measurements { | ||
| if m != nil && m.Type == measurement.TypeK8s { | ||
| m.Subtypes = append(m.Subtypes, subtype) |
There was a problem hiding this comment.
🔵 Nitpick — attach/mergeAKSGPUPools append the subtype without dedup
attachAKSGPUPools (here) and mergeAKSGPUPools (agent.go:371) append the aks-gpu-pools subtype unconditionally. Each runs once per invocation and no collector emits this subtype, so a duplicate only arises from hand-crafted snapshot input — not reachable in the live/agent flow.
Blast radius: Not reachable today; pure defensive observation.
Fix: Optional: use replace-or-append to fully close the hand-crafted-input ambiguity.
There was a problem hiding this comment.
Agreed on reachability: each function runs once per invocation on the freshly captured document, no collector emits this subtype, and user-supplied snapshot files never pass through either path, so a duplicate cannot arise in a supported flow. Leaving the append as-is to keep the merge semantics minimal; replace-or-append can ride along if hand-crafted snapshot mutation ever becomes a supported input.
| # fail closed. Dump the pool modes once and export the path — the CLI | ||
| # picks it up via the AICR_AKS_GPU_POOLS_PATH env var in every | ||
| # subsequent step (prep, install gate, conformance, CUJ chainsaw). | ||
| - name: Dump AKS GPU pool modes |
There was a problem hiding this comment.
🔵 Nitpick — Pool-dump step has no az retry, unlike the client-connect step
'Dump AKS GPU pool modes' runs set -euo pipefail with a single un-retried az aks nodepool list, while the immediately-preceding client-connect step retries 10x. Because this step exports AICR_AKS_GPU_POOLS_PATH consumed by all downstream phases, one transient az throttle reds an otherwise-green burn-in.
Blast radius: Fail-closed (not fail-open), so low severity — a CI flake surface, not a correctness risk.
Fix: Wrap the az call in the same bounded retry loop the client-connect step already uses.
There was a problem hiding this comment.
Fixed in a907d81: the pool dump is now wrapped in the same bounded retry shape as the client-connect step (5 attempts, 30s apart) and fails the step explicitly with an ::error:: annotation after the last attempt.
njhensley
left a comment
There was a problem hiding this comment.
Re-review — ADR-015 gpuStack profile on AKS (APPROVE)
Re-review of the fixes pushed since my prior review (which was dismissed on push). The head moved via a squash-rebase; the true content delta is 4 files, all directly addressing the earlier findings. I verified each fix against the resolved code and checked the delta for regressions — none found.
Prior-feedback status
| Prior finding | Status | Evidence |
|---|---|---|
🟡 ConfigMap-rewrite aborts file/stdout snapshot delivery (agent.go) |
✔️ Addressed | deployAndWaitForResult now takes deliverViaConfigMap; rewrite failure is fatal only when the ConfigMap is the deliverable, else slog.Warn + continue with the merged bytes. measureWithAgent sets it from HasPrefix(finalOutput, cm://); the SDK path passes false. Both (and only) two callers updated. |
🔵 rawSnapshotDoc helpers 0%-covered (agent.go) |
✔️ Addressed | TestRawSnapshotDocRoundTrip pins GetKind/GetMetadata (incl. non-string-value skip) and MarshalYAML round-trip. |
🔵 driverAbsentRemedy twin 4/6 branches untested (gpu_driver_state.go) |
✔️ Addressed | TestDriverAbsentRemedyBranches covers all six branches, plus legacy≠profiled distinctness and no-tuple-on-profiled. |
🔵 Pool-dump lacks az retry (uat-azure.yaml) |
✔️ Addressed | 5-attempt bounded retry (30s backoff) mirroring the client-connect step; ::error:: + exit 1 on exhaustion; set -e-safe and injection-free. |
🔵 attach/mergeAKSGPUPools append without dedup |
◻︎ Consciously declined | Defensive only, not reachable in the live/agent flow — reasonable to leave. |
| 🔵 Stale "driver-only" terminology | ◻︎ Consciously declined | Cosmetic, on pre-existing files this PR doesn't modify — reasonable to leave. |
Verification of the C1 fix (the only behavior change)
Exactly two callers of deployAndWaitForResult, both updated: the SDK path (false) returns the merged bytes on a rewrite failure instead of discarding the snapshot, and measureWithAgent fails fatally only when the user explicitly requested cm:// output. No other callers; no regression in the retry loop or the new tests.
One residual (follow-up grade, non-blocking — inline)
The new best-effort warn-branch itself is untested. Noted inline; optional.
Net
Four findings fixed correctly, two optional nits consciously declined, zero new defects, no blocker/major. Approving.
🔴 Blocker 0 · 🟠 Major 0 · 🟡 Minor 0 · 🔵 Nitpick 1 (residual, non-blocking)
| if deliverViaConfigMap { | ||
| return nil, err | ||
| } | ||
| slog.Warn("failed to rewrite the internal snapshot ConfigMap with the merged pool projection; "+ |
There was a problem hiding this comment.
🔵 Nitpick — Best-effort ConfigMap-rewrite branch is untested
This new best-effort branch — deliverViaConfigMap == false and rewriteSnapshotConfigMap fails, so we slog.Warn and return the already-merged bytes — is the behavior that resolved the prior C1 finding, but nothing exercises it. TestRewriteSnapshotConfigMapRejectsBadInput still stops at the early guards, and no test drives deployAndWaitForResult with a clientset whose Apply fails.
Blast radius: Low: the branch is a slog.Warn + return, and the fatal counterpart (cm:// deliverable) is the risky direction. But a future refactor could silently make it fatal again — the exact regression this fix closed — without a test catching it.
Fix: Add a snapshotter test with a fake clientset whose ConfigMap Apply returns an error, asserting that with a file/stdout finalOutput the merged snapshot is still returned (warn, not error), and that with a cm:// finalOutput the error propagates. Optional / follow-up grade.
There was a problem hiding this comment.
Pinned in a1df994. The branch is extracted into rewriteMergedSnapshotConfigMap(ctx, uri, kubeconfig, data, deliverViaConfigMap), which encodes the delivery contract in one place, and TestRewriteMergedSnapshotConfigMapDeliveryContract asserts both directions: deliverViaConfigMap=true propagates the rewrite error, false warns and returns nil. Failure is injected via an invalid cm URI (fails inside rewriteSnapshotConfigMap before any cluster access), which exercises the same branch a fake-clientset Apply failure would without the fixture weight — a future refactor that makes the best-effort side fatal again now fails this test.
Rollout PR 2 of the ADR-015 staged plan (NVIDIA#1761): the AKS family is the first embedded adopter of a configuration profile, and the snapshot projection that qualifies it lands in the same change. Profile adoption (recipes/overlays/aks.yaml, aicr.run/v1alpha3): - gpuStack declares two values over the four driver-ownership paths plus nvidia-dra-driver-gpu's nvidiaDriverRoot. azure-managed (default) keeps the AKS "Driver only" preinstall the family has always shipped; operator-managed (--gpu-driver none pools) flips driver.enabled/toolkit.enabled/operator.runtimeClass/nvidiaDriverRoot together — the bundle-time four-flag --set tuple is superseded by generation-time selection, and per-path --set overrides diverging from the selected value are rejected by the ownership lock. Legacy pre-profile recipes (no metadata.selectedProfile) keep the tuple. - Each value records its distinguishing constraint (K8s.aks-gpu-pools.gpu-driver: Install|None), so snapshot-qualified resolution verifies the pool mode and fails closed otherwise. - The driver-absent remedy twins (pkg/client/v1, pkg/bundler/ validations) and values-aks.yaml now name the two workable paths (repair pools + recapture, or recreate with --gpu-driver none + recapture + --profile gpuStack=operator-managed). Install-mode pools with no sampled driver still pass the constraint (pool mode is the ownership contract, not live state) and enter the record-absent bundle gate. Snapshot projection (ADR-015 DD3): - aicr snapshot --aks-gpu-pools <file> reads an operator-supplied 'az aks nodepool list -o json' dump and projects every NVIDIA GPU agent pool's gpuProfile.driver into the K8s measurement — Install, None, Managed (fully AKS-managed), Mixed (pools disagree); an nvidia block with managementMode Unmanaged follows the driver field (a supported azure-managed configuration), while Managed and unknown modes fail closed via the Managed marker. Managed, Mixed, and unknown values match no profile constraint, failing closed with the observed state; no GPU pools omits the reading. AMD accelerators are excluded (NG family; MI300X/MI325X in ND; Radeon Pro V620/V710 in NV) so an NVIDIA Install pool beside an AMD pool does not falsely read Mixed. - The projection is pure file processing and runs at the snapshot orchestration layer, not in a collector: local mode projects before any collector runs; agent Job mode projects controller-side before deploying — the file never enters the pod — then merges the subtype into the returned snapshot and rewrites the Job's result ConfigMap (Cleanup never deletes it) so no pre-merge artifact persists. aicr validate accepts the same flag for live capture, failing before any cluster mutation on a bad file. - Every read/decode failure (including top-level JSON null) is an error, never a degraded measurement. The read is size-bounded (os.Open + io.LimitReader, 1 MiB) and gated to regular files. The bounded reader (providerpools.go) is the shared layer for future per-provider projections (e.g. GKE), each with its own namespaced subtype. - The pkg/client/v1 facade AgentConfig carries AKSGPUPoolsPath so Client.CollectSnapshot supports the documented collect-then-resolve workflow. Docs migrate with the change: aks-gpu-setup.md (profile selection + snapshot recording flow), component-catalog.md (three-way inverse- mismatch flow), bundling.md (profile-owned --set lock), cli-reference (snapshot + validate flag rows, --profile values), api-reference (AKS /v1 → /v2 cut-over), contributor collector.md (provider node-pool projection pattern). Profile-bearing recipes flow through the evidence pipeline under the existing v1 predicate (per the NVIDIA#1761 staging; the descriptor-bound predicate type remains GKE-stage work): the dashboard-synthesis and TestGrid rejections are removed; a shared ProfileSegment joins the selected value into the evidence path name and the corroboration tab (meta.json records it, and the corroborate inversion strips it so it is never misread as a platform); the TestGrid coordinate is deliberately NOT suffixed - its digest-bound build ID already partitions per value; pointers record their selection and the repo evidence gate recomputes each pointer with it (including BASE-side suffixed-dir protection and per-value de-protection: removing one value's last pointer warns even while the sibling value keeps evidence); the corroboration dashboard keeps distinct routes per value (meta.json records the segment; the renderer hydrates it and routes every recipe hash through one profile-aware helper, while criteria facets stay profile-blind); presence live-paths report the suffixed routes; profiled pointers are verifier-checked to carry the name segment; local TestGrid publishes derive a bounded content digest from the bundle's canonical manifest.json (errors propagate; a placeholder is dry-run-only) so bundles sharing a timestamp cannot collide on the unsuffixed coordinate; the health presence manifest withholds AKS entries so Recipe Health cells stay an honest pending until profile-aware links land; aicr evidence digest gains --profile; and profile value names are catalog-validated case-insensitively unique so lowercase path segments cannot collide. The Azure UAT lane exports the pool dump, and the CUJ chainsaw recipe assertions are updated to the profiled shape (and to the emitted componentRefs/deploymentOrder order, fixing a pre-existing per-index mismatch). Part of NVIDIA#1761 (rollout PR 2). ADR: docs/design/015-recipe-configuration-profiles.md (DD3). Signed-off-by: Yuan Chen <yuanchen97@gmail.com>
njhensley
left a comment
There was a problem hiding this comment.
Re-review — ADR-015 gpuStack profile on AKS (APPROVE)
Re-review of the fix pushed since my prior approval (dismissed on push). The head moved via a squash-rebase; the true content delta is 2 files (pkg/snapshotter/agent.go, pkg/snapshotter/aksgpupools_test.go), addressing the last open item.
Prior-feedback status — all resolved
| Prior finding | Status |
|---|---|
| 🟡 ConfigMap-rewrite aborts file/stdout snapshot delivery | ✔️ Addressed (best-effort rewrite; fatal only for cm:// deliverable) |
🔵 rawSnapshotDoc helpers 0%-covered |
✔️ Addressed (TestRawSnapshotDocRoundTrip) |
🔵 driverAbsentRemedy twin 4/6 branches untested |
✔️ Addressed (TestDriverAbsentRemedyBranches, all 6 + distinctness) |
| 🔵 Pool-dump lacks az retry | ✔️ Addressed (5-attempt bounded retry, set -e-safe, injection-free) |
| 🔵 Best-effort rewrite branch untested (this push) | ✔️ Addressed (extracted to rewriteMergedSnapshotConfigMap; TestRewriteMergedSnapshotConfigMapDeliveryContract pins both branches) |
🔵 attach/mergeAKSGPUPools append without dedup |
◻︎ Consciously declined (defensive, unreachable) |
| 🔵 Stale "driver-only" terminology | ◻︎ Consciously declined (cosmetic, pre-existing files) |
Verification
The delivery-contract branch is now a named helper (rewriteMergedSnapshotConfigMap, agent.go:722) called cleanly at the single site; behavior is identical to the prior head, the low-level rewriteSnapshotConfigMap is unchanged, and the new test injects a rewrite failure via an invalid URI (failing before any cluster access) to pin both deliverViaConfigMap directions. No regressions in the delta.
Net
All actionable findings resolved; two optional nits consciously declined; zero new defects; no blocker/major. Approving — no inline comments this pass.
🔴 Blocker 0 · 🟠 Major 0 · 🟡 Minor 0 · 🔵 Nitpick 0 (open)
Summary
Adopt the ADR-015
gpuStackconfiguration profile on the AKS family —azure-managed(default, the AKS "Driver only" preinstall) andoperator-managed(--gpu-driver nonepools) — together with the snapshot projection that qualifies it:aicr snapshot --aks-gpu-pools <file>projects an operator-suppliedaz aks nodepool list -o jsondump into theK8s.aks-gpu-pools.gpu-driverreading (ADR-015 DD3). The bundle-time four-flag override tuple is replaced by generation-time selection and ownership locking.Motivation / Context
Rollout PR 2 of the staged plan on the implementation umbrella #1761, per ADR-015: recipe configuration profiles (Accepted 2026-07-21; the AKS pool-mode signal is Deferred Decision 3). AKS is the first embedded adopter because its recipe already defaults to the four-path azure-managed tuple, and the alternative mode required flipping all four paths together by hand at bundle time — the exact unqualified-hybrid risk the profile core exists to close (#1757).
The projection and the adopter were briefly staged as two PRs (#1968 → this one); they are recombined here because the halves are inseparable in practice — the reading is inert without the declaration, the declaration's snapshot-qualified path is broken without the reading, the docs cross-reference each other, and review findings repeatedly crossed the PR boundary. #1968 is closed with its review history intact; its final content is contained in this PR verbatim.
Fixes: N/A
Related: #1761 (implementation umbrella), #1933 (rollout PR 1, merged), #1968 (closed; projection half recombined here), #1757
Type of Change
/v1/recipeand/v1/queryclients usingservice=akscriteria are rejected after this merges (documented/v2cut-over indocs/user/api-reference.md), and pre-existing AKS snapshots without the pool reading fail snapshot-qualified resolution closed. Other families and criteria-only AKS generation are unaffected. Upgrade hazard for external--datacatalogs: a pre-existing (pre-conversion,v1alpha2) externaloverlays/aks.yamlwholesale-replaces the embedded declaring overlay, so upgrading AICR with such a catalog silently keeps the AKS family unprofiled — no error, noselectedProfile, no pool constraint. Documented with the operator migration step indocs/integrator/data-extension.mdand pinned by a regression test; load-time shadow detection is a candidate follow-up.Component(s) Affected
recipes/)pkg/recipe,pkg/client/v1)cmd/aicr,pkg/cli)pkg/collector,pkg/snapshotter)pkg/bundler/validations)pkg/servercode changes in this PR; the/v1AKS rejection is the merged profile core's behavior, triggered by this PR's adoption)docs/)Implementation Notes
The operator flow, end to end
Three steps; the pool dump is consumed only at step 2 — the snapshot carries the reading from then on (recipe takes the snapshot, bundle takes the recipe):
az aks nodepool list -g <rg> --cluster-name <cluster> -o json > pools.jsonaicr snapshot --aks-gpu-pools pools.json -o snapshot.yaml— works in agent Job and local mode; the file never enters the cluster; a bad file fails before any cluster work.aicr recipe --service aks … --snapshot snapshot.yaml(azure-managed default) or--profile gpuStack=operator-managed; thenaicr bundle -r recipe.yaml.Selection and verification are independent axes (explicit-intent per ADR-015; the ADR's motivating-example wording is clarified accordingly in this PR):
--profile— or its absence — decides the selected value (the declaration default,azure-managed, when omitted; never derived from the snapshot), and--snapshot— or its absence — decides whether that selection is verified now or later:--profile, no--snapshotazure-managed)--profile …=operator-managed, no--snapshotoperator-managed--profile,--snapshotazure-managed)Install--profile …=operator-managed,--snapshotoperator-managedNoneWith a snapshot present the check is never skipped — a missing reading fails closed for either selection; deliberately unverified generation is the criteria-only path (drop
--snapshot). Every pools × selection combination is deterministic and was confirmed live on both clusters at this head:azure-managed)--profile gpuStack=operator-managedInstall(aicr-test6)driver.enabled=false/nvidia-container-runtime/ root/NoneNone(aicr-test5)Installtrue/nvidia//run/nvidia/driverMixed/Managed--aks-gpu-pools)The same flow is documented for operators in
docs/integrator/aks-gpu-setup.md("End-to-end flow").Profile adoption
Declaration (
recipes/overlays/aks.yaml, nowaicr.run/v1alpha3):gpuStackdeclares two values overgpu-operator(driver.enabled,toolkit.enabled,operator.runtimeClass) andnvidia-dra-driver-gpu(nvidiaDriverRoot), plus the syntheticenabledpresence paths.azure-managedis the default and is value-identical to what the family has always shipped;operator-managedflips all four paths together. (Value names were finalized asazure-managed/operator-managed— naming ownership of the declared driver+toolkit layer — replacing the earlierdriver-only/operator, which inverted perspective and misstated the Azure preinstall; the qualifying constraint keeps Azure's officialInstall/Nonevocabulary.) Each value records its distinguishing constraint (K8s.aks-gpu-pools.gpu-driver: Install/None), so snapshot-qualified resolution verifies the actual pool mode and fails closed otherwise —Managed,Mixed, unknown values, and a missing reading match neither constraint.Ownership lock: per-path
--setoverrides of the owned paths that diverge from the selected value are rejected at bundle time (identical values pass); legacy pre-profile recipes withoutmetadata.selectedProfilekeep the old tuple behavior.Driver-state interplay (three cases, all documented in
component-catalog.mdand pinned by tests):metadata.gpuDriverState: absentand the bundle-timeCheckDriverOwnershipCoherencegate blocks with the reworded AKS remedy (repair pools + recapture, or recreate with--gpu-driver none+ recapture +--profile gpuStack=operator-managed). The remedy twins (pkg/client/v1/gpu_driver_state.go,pkg/bundler/validations/checks.go) branch onmetadata.selectedProfile: profiled artifacts get the recapture +--profilewording, legacy pre-profile artifacts keep the four-flag tuple wording (the lock does not apply to them). Thevalues-aks.yamlheader is updated; the legacy auto-override is subordinated on owned paths.Snapshot projection (ADR-015 DD3)
Projection semantics (
pkg/collector/k8s/aksgpupools.go): NVIDIA GPU pools are identified by VM-size family (NC/ND/NV) minus AMD accelerators (NGfamily; MI300X/MI325X ND sizes, which AKS requires creating with--gpu-driver none; Radeon Pro V620/V710 NV sizes) — without the exclusion, a supported NVIDIA-Install + AMD cluster would falsely projectMixed. Absent/nullgpuProfilefollows the provider's documentedInstalldefault;gpuProfile.nvidiawithmanagementMode: Managed(or unknown/empty mode) projectsManaged, whileUnmanagedfollows thedriverfield (a supported azure-managed configuration); disagreement projectsMixed; no GPU pools omits the reading.gpu-pool-countand a sortedgpu-poolsroster accompany the reading for diagnostics. Known limitation (documented in code): detection is prefix+marker based; the AgentPool object carries no vendor field.Orchestration-layer design: the projection is pure file processing and never enters the cluster. The Job-mode merge is performed on generic maps, not through the controller's typed
Snapshotstruct, so a version-skewed (newer) agent image's unknown fields survive the merge and the ConfigMap rewrite. Local mode projects before any collector runs; agent Job mode projects controller-side before deploying — a bad file fails in milliseconds with zero cluster mutations — then merges the subtype into the returned snapshot and rewrites the Job's result ConfigMap (Cleanup deletes Job+RBAC but never that ConfigMap, so without the rewrite a stale projection-less snapshot would persist, including when the ConfigMap is the requested output).aicr validateaccepts the same flag for its live-capture path, with--aks-gpu-poolsin its duplicate-flag guard, structured errors preserved (PropagateOrWrap), and a warning when the flag is passed alongside--snapshot(where it is ignored).Fail-loud contract: every read/decode failure — including a top-level JSON
null, whichjson.Unmarshalsilently accepts into a slice — is an error that fails the run, never a degraded measurement subject to the snapshotter's degrade-to-warning collector policy. The read is descriptor-first and context-aware: open withO_NOFOLLOW|O_NONBLOCK(no symlinks; a substituted FIFO cannot block the open), regular-file and 1 MiB size checks on the opened descriptor (pkg/defaultscap), then a chunked read with cancellation checks underFileReadTimeout— the verifier's established bounded-read pattern.Evidence-pipeline integration details (review-hardened):
meta.jsonrecords it; the corroborate criteria inversion strips it, so it is never misread as a phantom platform).Operator-Managed/operator-managedonto one evidence directory.manifest.json(per-file digests of the whole bundle, so even same-recipe/different-results bundles diverge); read errors propagate, and the"local"placeholder survives only under--dry-run— test-pinned. Two bundles sharing a second-resolution timestamp cannot collide on the deliberately unsuffixed TestGrid coordinate.pointer.profilerequires the recipe name to carry the lowercase-<name>-<value>segment (test-pinned), so a hand-written pointer cannot collapse two values into one evidence directory; the ADR-007 example shows the consistent form.docs/user/recipe-health.md— regenerated in this PR — shows the AKS rows as an honestpendingrather than linking soon-to-be-historical unsuffixed routes; profile-aware Health links are the recorded follow-up.-joins likegpu-stack=operatorvsgpu=stack-operator) can no longer merge — every run's selection must re-derive its own segment at intake (inconsistent metadata is skipped), and a same-route/different-selection conflict quarantines the whole coordinate order-independently instead of crowning a first-writer. Test-pinned (collision, mismatched-derivation, and the existing distinct-values rows).--dynamicrejects on intersection; argocd-helm install-time values reject any owned-key presence even when identical; component presence is not changeable by reselection (fragments cannot assignenabled). The shadow regression test now exercises the real--datalayered provider (with anti-vacuous source assertions), evidence-identity language distinguishes the three identities (overlay name / criteria coordinate / evidence slug), data-flow documents parse-before/attach-after, and the RQ1/health docs read in present tense.v1alpha2externaloverlays/aks.yamlwholesale-replaces the embedded declaring overlay, silently keeping the family unprofiled on upgrade — the operator migration step is indata-extension.md,TestAKSLegacyExternalShadowStaysUnprofiledpins the behavior as intentional, and load-time shadow detection is the recorded follow-up. The override-lock docs now state the three distinct cases (owned value paths: identical-accepted/divergent-rejected from any static source; syntheticenabled: scalar-only, typed sources always rejected;--dynamic: rejected on intersection).checkRecipeIdentityderives the recipe name, exact profile selection, and canonical digest from the verifiedrecipe.yamland requires the pointer'srecipe/profileand the predicate'sname/digestto match exactly — closing the name-collision spoof (a recipe named…-ubuntu-trainingno longer "satisfies" a fabricatedprofile: ubuntu=training); suffix checks remain as fast-fail pre-checks only. Test-pinned including the exact spoof, digest mismatch, and predicate-name mismatch; legacy criteria-less recipes keep verifying (name equality enforced only when derivable — the profile and digest bindings always run).name=valueselection rides throughmeta.json(profileSelection), aggregation, and the browser model (the lowercase segment is lossy by design and never reversed); both generators emit--profile/spec.recipe.profile— test-pinned end to end.CoordinateForis the shared criteria-only base coordinate; the Golden Path corroboration route appends the profile segment to the tab, TestGrid keeps the unsuffixed base and partitions per value via its digest-bound build ID — both user guides and the publisher comments now say exactly this. Recipe Health documents that profiled families are structurally graded at the declaration default, andpendingmeans "no committed linkable presence" (not "no live evidence").routeSegshelper; sidebar/breadcrumb labels show the suffixed tab so two values are distinguishable. Case-insensitive value-name uniqueness is stated in the integrator guide and the ADR's declaration invariants, matching the enforced rule.meta.jsonrecords the profile segment; the criteria inversion strips it (fail-closed skip on a tab/profile mismatch) and the renderer appends it to the tab route, soazure-managedandoperator-managednever overwrite each other's lookup entry while criteria facets stay profile-blind. The full plumbing (Synthesize writes it → corroborate strips/routes it) is test-pinned.CUJ chainsaw fixtures actually pass now: beyond migrating to the profiled shape, the
componentRefs/deploymentOrderassertions were re-synced to the emitted order — chainsaw list asserts are per-index, and the old alphabetized lists failed against freshly generated recipes even on main (pre-existing; both fixtures now verifiedrc=0withchainsaw assertagainst fresh output).Public SDK surface: the
pkg/client/v1facadeAgentConfigcarriesAKSGPUPoolsPath(translated intoInternalAgentConfig, pinned by an SDK-level test) soClient.CollectSnapshotsupports the documented collect-then-resolve workflow. This deliberately differs fromClusterConfigPath/DiscoverNetwork(optional in-pod enrichments, off the facade): this is controller-side input that AKS profile-qualified resolution requires.Generalization path (GKE and beyond): per-provider projectors with namespaced subtypes (
gke-gpu-poolsbesideaks-gpu-pools), the shared bounded reader (providerpools.go), and additive sibling flags — documented for contributors indocs/contributor/collector.md("Provider Node-Pool Projections").API and version surface
Every resolved AKS recipe is now
aicr.run/v1alpha3; pre-flip artifacts keep resolving as before./v1/recipeand/v1/queryon AKS criteria reject once this merges — the/v2cut-over is documented indocs/user/api-reference.md(with a realgpuStackexample).metadata.selectedProfilerecordsgpuStack, the selected value, and the declaration-wideownedPaths.Test moves. Fixtures that declared a
gpuStackprofile on AKS criteria (profile-core integration, server profile-endpoint, facade profile test) move to EKS: composition-wide uniqueness now collides with the embedded declaration.profile_aks_test.goqualifies both embedded values, the ownership surface, recorded constraints, and leaf inheritance.gpuHardwareSnapshotPoolsdecouples pool mode from sampled driver state.TestReadingShapeMatchesProfileContractpins the reading contract shared by the declaration and the projector.Undraft checklist — complete
All pre-undraft items are done; the only follow-up is post-merge by design (AKS evidence regeneration, item 2).
gpuDriverStateauto-override on owned paths — pinned at the sharp case against the real embedded declaration:gpuStack=operator-managed(fragmentdriver.enabled=true) + loaded-driver snapshot; the injector must skip the owned path and the fragment value survives (TestResolveRecipeFromSnapshot_GPUDriverAutoDetect, aks operator subordination row). Also observed live on aicr-test5.pkg/evidence/project) and TestGrid publication are removed; a sharedProfileSegmenthelper joins the profile value into the evidence path name (RecipeNameFor) and the corroboration Tab so the two values never overwrite each other (the TestGrid coordinate deliberately stays unsuffixed — its digest-bound build ID partitions per value); pointers record their selection (profile: name=value);aicr evidence digest --profilecomputes selection-correct digests (rejected on hydrated-result inputs); and the repo evidence gate recomputes each pointer against its recorded selection, including suffixed-dir → overlay mapping. Committed pre-profile AKS pointers become historical (designed semantics); their regeneration against the merged digest remains the standard post-merge publish flow.make qualifygate below. (The Azure UAT lane is already migrated in this PR:uat-azure.yamldumps the pool modes after cluster connect and exportsAICR_AKS_GPU_POOLS_PATH, which every subsequent snapshot/validate step — prep, install gate, conformance, CUJ chainsaw — picks up. Aspec.snapshot.aksGpuPoolsAICRConfig field is a possible follow-up for config-file parity; the env var is the supported path today.)make qualifyfull gate — green (rc=0) at every review round's head, most recentlya5b1a9652(round 20),20547800d(round 19), anda35c5dccc(round 18 + the value-name finalization) — all green on the first run with no tests/releasepolicy: 10s per-script deadline flakes under full-suite load on dev machines #1974 flake rerun. The head66542dacf(final-review wording fixes, comment-only, plus rebases over two non-overlapping validator commits) was also green on the first run;a907d81b6(first human-review round) was green on the first run as well; the current heada1df9946f(re-review nitpick + rebase onto 018dd55) is green on the first run as well. Roughly half the runs needed one rerun for the tracked tests/releasepolicy: 10s per-script deadline flakes under full-suite load on dev machines #1974 flake (tests/releasepolicy10s script-deadline margin; passes in isolation every time; not this PR — its diff intersects nothing the package executes). CI runs the same gate on CI runners and has been green throughout.Testing
make qualify # full gate green (rc=0) at 3f3d3d93b (round 16; one #1974 flake rerun) — round-17 head pending its own runTwo dedicated backward-compat/ADR-contract tests beyond the coverage described below:
TestAKSDefaultKeepsPreProfileEffectiveValues— the no-behavior-change guarantee for the default (azure-managed) path: criteria-only resolution against the embedded catalog vs the same catalog with the declaration stripped → identical component sets, byte-identical effective values for every component, and a constraint delta of exactlyK8s.aks-gpu-pools.gpu-driver=Install.TestClassifyIgnoredAKSGPUPools— the ignored-flag note's provenance matrix: explicit CLI presence (both flag forms) always warns, ambient env demotes to debug, prefix false-positives excluded.TestResolveRecipeFromSnapshot_GPUDriverAutoDetect—gpuStack=operator-managed+ loaded-driver snapshot: the legacy auto-override must skip the profile-owneddriver.enabledand the fragment'struesurvives (also observed live on aicr-test5).Projection: table-driven tests cover all-Install, all-None, absent/null
gpuProfile→ Install,Mixed,Managed, unknown-value preservation, VM-family + AMD filtering (MI300X/V710 excluded, AMD-only omits the reading), no-GPU-pools omission; fail-loud tests cover missing file, wrong JSON shape, top-levelnull, malformed JSON, size cap, non-regular files. Orchestration: local-mode attach + fail-before-collectors, Job-mode merge round-trip, ConfigMap-rewrite guards, SDK translation. Profile: both embedded values qualified end to end (ownership surface, constraints, leaf inheritance), lock rejection of diverging--set, legacy-tuple acceptance on pre-profile recipes, and the three driver-state cases above.Live qualification (2026-07-30, both values, real AKS clusters, binary built from this branch):
Install→azure-managed)None→operator-managed)aicr snapshot --aks-gpu-pools(stockv0.18.0agent image — the projection is controller-side, so the in-pod image needs no new code)gpu-driver: Install, CPU/system pools filteredgpu-driver: Noneazure-managedselected,selectedProfilerecordedoperator-managedselected; the legacy auto-override's subordination on owned paths fired live (driver observed loaded, mutation skipped with advisory log)gpuStack=operator-managedrejected:constraint "K8s.aks-gpu-pools.gpu-driver" failedazure-managedrejected likewisedriver.enabled=false,toolkit.enabled=false,runtimeClass=nvidia-container-runtime,nvidiaDriverRoot=/true/true/nvidia//run/nvidia/driver--set gpuoperator:driver.enabled=truerejected naming the owned path; identical value acceptedexpected=Install actual=Installpasses; crossed artifacts (test6 recipe × test5 snapshot) fail closed:expected Install, got Nonenull)INVALID_REQUEST, zero cluster mutationsBlast-radius proof (non-AKS families): all 60 non-AKS leaf recipes in the embedded catalog (EKS/GKE/OKE/OCP/Kind/LKE/BCM/metal3 × every accelerator/OS/intent/platform) generated with this branch's binary and a clean-
origin/mainbinary are byte-identical, 60/60 (real outputs verified non-vacuous: every file carriescomponentRefs, ~5.2 MB total).Final live e2e re-run at
a5b1a9652(the reviewed head; the subsequent delta to66542dacfis comment-only): freshazpool dumps → Job-mode snapshots with the controller-side projection (gpu-driver: Installon aicr-test6,Noneon aicr-test5) → the full resolution matrix (default passes on test6 / fails closed on test5;--profile gpuStack=operator-managedfails closed on test6 / passes on test5 with the legacy auto-override subordinated live —driver.enabled=trueand/run/nvidia/driversurvive a loaded-driver snapshot) → criteria-only generation records both values' constraints unevaluated → bundles render the right four-path tuple per value → ownership lock rejects divergent--setand accepts identical on both clusters →aicr validatereadiness fails closed in both mismatch directions with the observed pool state named → deployment-phase validation passes 4/4 on aicr-test6; on aicr-test5 it passes 3/4 withexpected-resourcescorrectly detecting pre-existing stack drift (that cluster's 2026-07-09 deployment never installednodewright-customizations; the check times out waiting for the Skyhook CR — environmental, demonstrated by the same binary+image passing 4/4 on test6, and the check doing exactly its job). Per-profileevidence digest: default == explicitazure-managed,operator-manageddistinct, hydrated-result input rejects--profile.Earlier full live e2e re-run at
0f471c929(the evidence-fixed tree): freshazdumps → Job-mode snapshots → all four resolution-matrix cells → bundles with rendered-value verification → ownership lock → validate readiness both directions — all green on both clusters, plus the new evidence surfaces:evidence digestdefault == explicitazure-managed,operator-manageddigest distinct, hydrated-result input rejects--profile.Independent adversarial review: twenty rounds across two reviewers (read-only worktrees, two-question mechanism/reachability protocol, scoped to the ADR-015 implementation). The arc: rounds 1–3 fixed the Azure UAT lane break, legacy-remedy regression, version-skew merge loss, and follow-ons; rounds 4–9 drove the docs-contract sweep, deterministic marshal, breaking-change metadata, and the case-uniqueness rule; rounds 10–14 hardened the evidence pipeline (profile path segments, per-value gate protection, dashboard routing, exact-selection copy generators, content digests); rounds 15–17 closed the security-grade identity binding (pointer/predicate claims derived from manifest-verified recipe bytes, with the name-collision spoof test-pinned), the external-catalog upgrade hazard (documented + regression-pinned), and the selection-collision quarantine in corroboration aggregation; round 18 fixed the profiled evidence-refresh flow (gate output, publishing guide, and signing-workflow header now capture
--aks-gpu-poolsand hydrate the recipe with the pointer's recorded--profileselection — the raw-overlay flow could only regenerate default-value evidence), split the per-surface lock matrix's bundle/mirror rows (mirror exposes only scalar--set; config-filedeployment.setis not applied bymirror list), corrected the argocd-helm claim to presence-based rejection, gave the SDK example separate resolve/snapshot/validate contexts, and cleaned the remaining RQ1, ADR-012 identity, and meta.json-provenance doc residue; round 19 completed the refresh flow with the target leaf's--intent/--platform(the snapshot fingerprint deliberately reportsanyfor author-selected criteria, so the earlier commands could hydrate a different leaf), made the fallback validate the hydrated recipe explicitly, addedWithValidationTimeout(0)to the SDK example (the facade's default 75-minute cap would otherwise override the two-hour context), rewrote the pool-file reader descriptor-first with context threading (see below), and swept the last rename/matrix/identity residue off public surfaces (api-reference,automation, the selection table, thechecks.goremedy twin, the peermem manifest,recipe-development.md's mirror row, and the TestGrid coordinate comment); round 20 found no reachable code or workflow defect — the exit criterion (a round with no valid, blocking finding) — and its three comment/metadata cleanups are folded into the final rebase. A final pre-undraft self-review (two independent adversarial passes over the full range — code correctness and documentation contracts, both applying the mechanism/reachability protocol) found no blocking findings; its minor wording items (lock-rule precision inaks-gpu-setup.md, retiring the old value name from sibling-file comments, a stale values-merge comment, a missing profile-lock bullet in the CLI reference's override rules) are folded into the final head, and its follow-up-grade observations (profile-owned short-circuit skips two advisory warns ingpu_driver_state.go;RecipeNameForempty-name guard comment; ADR-015 lock-mechanics wording predating this PR; anair-gap-mirror.mdprofile caveat) are noted here rather than churned in. First human review (njhensley): APPROVED, with one minor and four nitpicks — all verified and addressed ata907d81b6: the internal ConfigMap rewrite is now fail-loud only when cm:// is the delivery vehicle (file/stdout/SDK runs warn and deliver the merged snapshot instead of discarding a successful capture), the UAT pool-dump step gained the client-connect retry shape, and two coverage gaps got direct tests (TestRawSnapshotDocRoundTrip,TestDriverAbsentRemedyBranches); the dedup observation was agreed unreachable in supported flows and left as-is (replied inline). His re-review re-approved with one follow-on nitpick — the new best-effort branch itself was untested — closed ata1df9946fby extracting the delivery contract intorewriteMergedSnapshotConfigMapand pinning both directions inTestRewriteMergedSnapshotConfigMapDeliveryContract. Deliberately declined with recorded rationale: softening the ConfigMap-rewrite fail-loud, andnvidia: {}as Unmanaged (Azure documentsnull/explicit-Managedshapes; fail-closed retained — independently confirmed by review). The pool-reader Lstat→Open TOCTOU, declined earlier as a self-race, was ultimately implemented in round 19 on stronger grounds: the reader took no context (violating the project's I/O rule — a swapped-in FIFO could stall a snapshot with no cancellation), so it now opens descriptor-first (O_NOFOLLOW|O_NONBLOCK), validates the opened descriptor, and streams with cancellation checks underFileReadTimeout, following the verifier's established pattern.Risk Assessment
Rollout notes: No behavior change for non-AKS families (byte-identical, proven by the 60/60 catalog sweep below). On AKS the migration is unconditional at generation — recipes flip to
aicr.run/v1alpha3with the recorded profile regardless of flags (--aks-gpu-poolsgates only the snapshot reading);/v1AKS clients must move to/v2(documented cut-over).Checklist
make testwith-race)make lint)git commit -S) — GPG signing info