Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/contributor/recipe.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ rejected. Profile-version metadata and recipe artifacts are strictly decoded
so an unknown field cannot silently disappear.

The core `ProfileValue` contract is closed to `advertiser`, `constraints`,
and `componentRefs{name,overrides}`. It rejects `valuesFile`, component
`readinessConstraints`, and `componentRefs{name,overrides}`. It rejects `valuesFile`, component
identity/deployment fields, root `overrides.enabled`, literal dotted keys,
and nested empty maps. The `advertiser` field accepts exactly one non-empty
value, `external` (validated against `pkg/allocpolicy`, the canonical
Expand Down Expand Up @@ -252,6 +252,12 @@ Resolution enforces these invariants:
collisions.
5. Evaluate selected profile constraints fail closed. A missing reading has a
distinct invalid-request diagnostic; other evaluator failures propagate.
A value's `readinessConstraints` are exempt from this step by design:
they name post-deployment properties (ADR-015 DD5) and route into
`spec.validation.readiness.constraints`, where the `aicr validate`
readiness pre-flight evaluates them fail closed. Names deduplicate
per phase — the same measurement path may carry a generation-time
pre-condition and a readiness-time post-deployment state.
6. Stamp the result `aicr.run/v1alpha3` and persist
`metadata.selectedProfile`. Its sorted `ownedPaths` is the
declaration-wide path union plus synthetic `enabled` for each referenced
Expand Down
78 changes: 71 additions & 7 deletions docs/design/015-recipe-configuration-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ spec:
componentRefs:
- name: gcp-driver-installer
overrides:
install: false # every value assigns every union path
# every value assigns every union path; nested gate — see the
# amendment on operator-selfdriver below.
installer: {enabled: false}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: gpu-operator
overrides:
devicePlugin: {enabled: true}
Expand All @@ -262,7 +264,7 @@ spec:
componentRefs:
- name: gcp-driver-installer
overrides:
install: false
installer: {enabled: false}
- name: gpu-operator
overrides:
devicePlugin: {enabled: false}
Expand All @@ -277,7 +279,14 @@ spec:
componentRefs:
- name: gcp-driver-installer
overrides:
install: true # the chart-level gate
# Amended 2026-08-22: the gate is the nested installer.enabled,
# not the top-level `install` originally drawn here — top-level
# `install`/`enabled` are component-PRESENCE gates (IsEnabled),
# so a false default would make the component "not enabled in
# the surviving composition" and deadlock resolution for every
# value; root `overrides.enabled` is separately rejected in
# fragments. A nested key is an ordinary owned value path.
installer: {enabled: true}
Comment thread
yuanchen8911 marked this conversation as resolved.
- name: gpu-operator
overrides:
devicePlugin: {enabled: true}
Expand Down Expand Up @@ -511,7 +520,7 @@ to the surviving composition:
# digest, so ordering must be byte-stable
# Post-DD5 state shown; the initial recording is
# gpu-operator: [devicePlugin.enabled, enabled] only.
gcp-driver-installer: [enabled, install]
gcp-driver-installer: [enabled, installer.enabled]
gpu-operator: [devicePlugin.enabled, enabled]
```

Expand Down Expand Up @@ -1453,9 +1462,10 @@ recurrence — the shape the Problem section expects.
distinguishing signal is identified (Deferred Decision 5). The other
two values do not wait on it. The dormant component and the third value
land **together**, in one event: declaring the value later is an
ownership-surface expansion (`install` joins the union and the
installer's synthetic `enabled` joins `ownedPaths`), which is a
family-wide re-qualification and evidence re-signing event.
ownership-surface expansion (`installer.enabled` joins the union, so
every existing value gains an assignment for it — the sketch above
draws that end state), which is a family-wide re-qualification and
evidence re-signing event.

Any dcgm-exporter GPU-ID-mapping adjustment for `csp-managed` is an
external GKE behavior not verifiable from this repository. It is
Expand Down Expand Up @@ -1522,3 +1532,57 @@ work that resolves it.
absence, so the two values stay mutually distinguishable.
**Proposed: identify a durable signal during the value's adoption;
the `operator` and `csp-managed` values do not wait on it.**

*Amended 2026-08-24: mechanism only.* `ProfileValue` gains
`readinessConstraints` — same catalog-load validation as `constraints`
with per-phase name deduplication (the same measurement path may carry
a generation-time pre-condition and a readiness-time post-deployment
state), routed into `spec.validation.readiness.constraints` at
resolution and **never evaluated at generation time**. The
`aicr validate` readiness pre-flight evaluates them with the same
fail-closed exit as every other readiness gate.

The mechanism exists for values whose distinguishers are
deployment-created — where no generation-time reading can hold. Two
rules govern its use:

- **The self-falsifying pre-condition trap.** Generation-time
constraints are re-evaluated by the validate pre-flight, so a
pre-condition that the value's own success erases (e.g. "no NVIDIA
driver loaded" on a value whose operator installs the driver) must
never be declared as a generation constraint — it fails every
post-deployment validate on a correctly working cluster. Such state
belongs in `readinessConstraints`, asserted in its post-deployment
form.
- **Self-rendered readings do not qualify.** A reading the selected
bundle itself renders (e.g. deployed ClusterPolicy fields) is
Comment thread
yuanchen8911 marked this conversation as resolved.
satisfied by construction under every value — it is a useful
rendered-policy **drift check**, but it cannot serve as a value's
distinguishing constraint. Qualification requires cluster state
independent of the bundle's own output (provider properties, node
labels set at provisioning, externally-owned objects).

Deployment-created markers sit between the two: state the value's
own workload writes at runtime (the loaded driver a self-falsified
pre-condition asserts in post-form, a label its DaemonSet applies
after a successful install) is a legal readiness constraint as an
**outcome check** — unlike a rendered `.spec` readback it can
fail, and it observes state some deployment actually produced. It
does not establish *which* deployment produced it: the readiness
gate compares only the snapshot value, with no deployment
identity, owner, or timestamp binding, so an unversioned marker
left by an earlier deployment satisfies a later check. A
workload-written marker is therefore valid only when its producer
owns the marker's full lifecycle — clearing or versioning it when
the outcome no longer holds. And an outcome check verifies
*execution*, not *selection*: every value's own success satisfies
its own markers, so it cannot establish that the cluster's
pre-existing mode matches the selected value. A value's
**qualifying** constraint must rest on the bundle-independent
state above, whichever list it is declared in.

This PR resolves no GKE signal: the GKE family's DD5 question was
settled separately by value replacement (see the adoption-step
amendment), and its shipped values are generation-time
distinguishable. The mechanism's consumers are families whose values
are distinct cluster shapes with deployment-created distinguishers.
29 changes: 28 additions & 1 deletion docs/integrator/recipe-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,32 @@ identical to a sibling's — does not support the "validated against deployed
config" claim and must not be declared. The snippet above shows the declaration
shape only; it is not a declaration you should copy into an overlay.

**When the distinguishing signal only exists after deployment**, declare it
under the value's `readinessConstraints` instead of `constraints`. Both lists
get the same catalog-load validation (names deduplicate per list; the same
measurement path may appear in both, carrying a pre-condition at generation
and a post-deployment state at readiness), but
`readinessConstraints` are never evaluated at generation time — they route
into `spec.validation.readiness.constraints` and are evaluated fail-closed by
the `aicr validate` readiness pre-flight. Two kinds of state belong here:
externally-grounded cluster state evaluated post-deployment (provider
properties, node labels set at provisioning), and **deployment-outcome
checks** — properties the value's own workload creates (the post-deployment
form of a self-falsified pre-condition, or a node label its DaemonSet applies
after a successful install), which a fresh deployment cannot find in the
pre-deployment snapshot that generation-time constraints are checked against.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Only the externally-grounded kind can **qualify** the value — establish that
the cluster's pre-existing mode matches the selection. An outcome check
observes post-deployment state without establishing which deployment
produced it: the readiness gate compares only the snapshot value, with no
deployment identity, owner, or timestamp binding, so a marker left by an
earlier deployment satisfies a later check. Declare a workload-written
marker only when its producer owns the marker's full lifecycle — clearing
or versioning it when the outcome no longer holds. And because every
value's own success satisfies its own markers, an outcome check can never
distinguish one value from another (ADR-015, "Self-rendered readings do
not qualify").

**Constraint names must be measurement paths a supported snapshot producer
actually emits** — a collector, or a provider projection attached at the
snapshot orchestration layer (e.g. `K8s.aks-gpu-pools.gpu-driver` from
Expand All @@ -473,7 +499,8 @@ snapshot orchestration layer (e.g. `K8s.aks-gpu-pools.gpu-driver` from

**Paths are validated when recipe data is loaded, not when a snapshot is
evaluated.** Every constraint name in `spec.constraints`,
`spec.validation.readiness.constraints`, and `spec.profile.values.*.constraints`
`spec.validation.readiness.constraints`, `spec.profile.values.*.constraints`,
and `spec.profile.values.*.readinessConstraints`
is checked against the measurement catalog (`pkg/measurement/catalog.go`) as the
overlay, mixin, or base file is read. A path the catalog cannot address fails
the load with the file, the field, and — where there is a near match — a
Expand Down
4 changes: 4 additions & 0 deletions pkg/recipe/constraint_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ func validateSpecConstraintPaths(spec *RecipeMetadataSpec, source string) error
if err := validateConstraintPaths(spec.Profile.Values[name].Constraints, source, location); err != nil {
return err
}
location = fmt.Sprintf("%s.%s.readinessConstraints", locProfileConstraints, name)
if err := validateConstraintPaths(spec.Profile.Values[name].ReadinessConstraints, source, location); err != nil {
return err
}
}
}

Expand Down
75 changes: 57 additions & 18 deletions pkg/recipe/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,27 @@ type ProfileDeclaration struct {
// metadata.selectedProfile.advertiser and extends the dual-advertisement
// gates fail-closed. Any other value is rejected.
type ProfileValue struct {
Advertiser string `json:"advertiser,omitempty" yaml:"advertiser,omitempty"`
Constraints []Constraint `json:"constraints,omitempty" yaml:"constraints,omitempty"`
Advertiser string `json:"advertiser,omitempty" yaml:"advertiser,omitempty"`
Constraints []Constraint `json:"constraints,omitempty" yaml:"constraints,omitempty"`

// ReadinessConstraints are evaluated only by the aicr validate readiness
// pre-flight, never at generation time: applyEffectiveProfile routes them
// into spec.validation.readiness.constraints instead of spec.constraints.
// Two kinds of state legally live here (ADR-015, "Self-rendered readings
// do not qualify"): externally-grounded cluster state evaluated
// post-deployment (provider properties, provisioning-set node labels),
// and deployment-outcome checks — the post-deployment form of a
// self-falsified pre-condition, or a marker the value's own workload
// writes, which a fresh deployment cannot find in the pre-deployment
// snapshot that generation-time constraints are evaluated against.
// Only the first kind QUALIFIES the value (establishes the cluster's
// pre-existing mode matches the selection). An outcome check binds no
// deployment identity — a stale marker from an earlier deployment
// satisfies it — so declare workload-written markers only when the
// producer owns the marker's lifecycle. Same fail-closed semantics as
// Constraints once the pre-flight runs; same catalog-load validation.
ReadinessConstraints []Constraint `json:"readinessConstraints,omitempty" yaml:"readinessConstraints,omitempty"`
Comment thread
yuanchen8911 marked this conversation as resolved.

ComponentRefs []ProfileComponentRef `json:"componentRefs,omitempty" yaml:"componentRefs,omitempty"`
}

Expand Down Expand Up @@ -207,23 +226,43 @@ func ValidateProfileDeclaration(decl *ProfileDeclaration) (map[string][]string,
// constraints already fail closed on an empty name or value
// (validateConstraintWarningSource); catalog load is the equivalent
// boundary for profile-contributed ones.
seenConstraints := make(map[string]struct{}, len(value.Constraints))
for _, constraint := range value.Constraints {
if constraint.Name == "" {
return nil, errors.New(errors.ErrCodeInvalidRequest,
fmt.Sprintf("profile %q value %q declares a constraint with no name", decl.Name, valueName))
}
if constraint.Value == "" {
return nil, errors.New(errors.ErrCodeInvalidRequest,
fmt.Sprintf("profile %q value %q constraint %q has no value",
decl.Name, valueName, constraint.Name))
}
if _, repeat := seenConstraints[constraint.Name]; repeat {
return nil, errors.New(errors.ErrCodeInvalidRequest,
fmt.Sprintf("profile %q value %q repeats constraint %q",
decl.Name, valueName, constraint.Name))
// Each list deduplicates independently: constraint names are
// measurement paths, and the same reading legitimately appears in
// both lists of one value with different expected states — the DD5
// pattern reads NodeTopology.gpu-nodes.label at generation (a pool
// pre-condition) AND at readiness (a post-deployment marker). The
// two lists evaluate in different phases with per-phase diagnostics,
// so cross-list reuse is unambiguous; a repeat WITHIN a list is two
// gates with one identity and stays rejected.
checkConstraints := func(constraints []Constraint, kind string) error {
seen := make(map[string]struct{}, len(constraints))
for _, constraint := range constraints {
if constraint.Name == "" {
return errors.New(errors.ErrCodeInvalidRequest,
fmt.Sprintf("profile %q value %q declares a %s with no name", decl.Name, valueName, kind))
}
if constraint.Value == "" {
return errors.New(errors.ErrCodeInvalidRequest,
fmt.Sprintf("profile %q value %q %s %q has no value",
decl.Name, valueName, kind, constraint.Name))
}
if _, repeat := seen[constraint.Name]; repeat {
// Name the list: the same measurement path is legal in
// both constraints and readinessConstraints (the DD5
// pattern), so a repeat must say which list to fix.
return errors.New(errors.ErrCodeInvalidRequest,
fmt.Sprintf("profile %q value %q repeats %s %q",
decl.Name, valueName, kind, constraint.Name))
}
seen[constraint.Name] = struct{}{}
}
seenConstraints[constraint.Name] = struct{}{}
return nil
}
if err := checkConstraints(value.Constraints, "constraint"); err != nil {
return nil, err
}
if err := checkConstraints(value.ReadinessConstraints, "readiness constraint"); err != nil {
return nil, err
}

seenComponents := make(map[string]struct{}, len(value.ComponentRefs))
Expand Down
Loading
Loading