aicr

package
v0.21.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: Apache-2.0 Imports: 46 Imported by: 0

Documentation

Overview

Package aicr is the public, compatibility-reviewed Go library surface for external consumers of the AI Cluster Runtime.

External projects should import THIS package and use the types and constructors re-exported here. The underlying pkg/* packages are public and will remain importable, but this facade is the reviewed compatibility contract the project intends to stabilize at v1.0.

Surface

Client exposes the end-to-end operations the CLI / server share:

  • ResolveRecipe / ResolveRecipeFromCriteria / ResolveRecipeFromSnapshot and LoadRecipe — produce or load a *RecipeResult.
  • BundleComponents — resolve Helm values and stitched manifests for each component in a *RecipeResult.
  • CollectSnapshot — deploy the snapshotter Job and retrieve a *Snapshot.
  • LoadSnapshot — read a previously captured *Snapshot from a file, URL, or cm:// ConfigMap, for the common case where the snapshot already exists and no cluster is needed.
  • DiffSnapshots — compare two loaded or collected snapshots in memory and return facade-owned field-level changes for drift detection.
  • ValidateState — evaluate a resolved recipe against a snapshot, running deployment / conformance / performance phases.
  • LoadConfig — read and validate the AICRConfig a team commits, from a file or an HTTP(S) URL. WrapConfig lifts one already parsed elsewhere; it does no parsing itself. Either way the resulting Config DERIVES options (Config.BundleVerifyOptions, Config.RecipeSource, Config.RecipeCriteria, ...) rather than applying them: a Config never attaches to a Client and is never consulted implicitly, so caller precedence stays one readable line at the call site.

Resolution behavior is tuned per call with RecipeResolveOption — WithProfile, WithAccountingMode, and WithSnapshotCriteriaRelaxation (the relax-and-retry policy behind `aicr recipe --snapshot`, which takes the criteria dimensions the caller stated explicitly and may clear the rest).

The supply-chain half covers both producing and checking artifacts:

  • VerifyBundle — check a deployment bundle's checksums and attestation chain, and evaluate a trust-floor / creator / version policy.
  • VerifyEvidence — check a recipe-evidence bundle's signature and hash chain, from a pointer file, an OCI reference, or a directory.
  • VerifyCatalog / SignCatalog — check or produce the Sigstore signature over this Client's recipe catalog.
  • RecipeDigest — the canonical recipe digest an evidence predicate records, for CI gates detecting stale evidence.
  • EmitRecipeEvidence / PublishEvidence — build, then sign and push, a recipe-evidence bundle.
  • VerifyBinaryAttestation — package-level; prove an aicr binary was built by NVIDIA CI.

All facade types (Snapshot, SnapshotDiff, SnapshotChange, AgentConfig, Criteria, RecipeRequest, RecipeResult, ComponentBundle, ComponentRef, PhaseResult, AllowLists) are facade-owned structs translated to and from the upstream pkg/* shapes, so internal field renames don't churn external callers.

Seven types remain deliberate transparent aliases: BundleConfig, BundleAttester, BundleArtifact, OIDCResolveOptions, CriteriaRegistry, BundleVerifyReport, and EvidenceVerification. They preserve direct interoperability with the configuration builders, attestation implementations, bundle results, and provider-scoped criteria registry used elsewhere in AICR. The API compatibility gate compares their repository-local reachable type closure without freezing unrelated exports in the evolving target packages.

Example

client, err := aicr.NewClient(
    aicr.WithRecipeSource(aicr.FilesystemSource("/etc/aicr/recipes")),
)
if err != nil {
    return err
}
defer func() {
    if closeErr := client.Close(); closeErr != nil {
        slog.Error("failed to close AICR client", "error", closeErr)
    }
}()

result, err := client.ResolveRecipe(ctx, aicr.RecipeRequest{
    Service:     "eks",
    Region:      "us-east-1",
    Accelerator: "h100",
    Nodes:       8, // worker-node count, not GPU count
    Intent:      "training",
})

Stability

AICR is currently pre-1.0. Under Go module versioning, a v0 minor release may contain breaking API changes. The project detects and explicitly records incompatible changes to this facade, but v0 consumers must pin a patch version and audit upgrades.

Starting with v1.0, this package's exported API follows semantic versioning: breaking changes require a major release, minor releases may add API, and patch releases contain compatible fixes. The underlying pkg/* packages may continue to evolve under the stability tiers documented in docs/integrator/public-api.md.

Concurrency and Client lifecycle

Each Client owns its own DataProvider and per-DataProvider cached metadata store, component registry, and criteria registry. Multiple Clients constructed from different sources can resolve recipes concurrently without clobbering each other — a property multi-tenant consumers (e.g., a controller managing one Client per per-tenant configuration) rely on. This is a v0.12+ guarantee; earlier facade builds mutated a process-global DataProvider via recipe.SetDataProvider and were unsafe to construct concurrently.

**Retain and reuse Client instances.** The recipe package keys its internal caches on DataProvider identity (pointer-equality of the interface value). Each call to NewClient builds a fresh DataProvider, so two Clients constructed from the same recipe source still produce distinct cache entries and do their own directory walk on first use. Long-running consumers should cache Clients keyed by their configuration (e.g., a content hash of the recipe-source settings) rather than constructing one per request.

**Call Close when done.** When a Client is no longer needed (cache eviction, controller shutdown), call Close to drop its metadata store, component registry, and criteria registry from the recipe package's internal caches. Without this, memory grows monotonically with the number of unique DataProviders ever observed.

See docs/integrator/go-library.md for the integration guide.

Example

Example is the quick start: build a Client over the embedded recipe data and resolve a recipe from explicit criteria.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(
		aicr.WithRecipeSource(aicr.EmbeddedSource()),
		aicr.WithVersion("v0.19.0"),
	)
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	result, err := client.ResolveRecipeFromCriteria(ctx, &aicr.Criteria{
		Service:     "eks",
		Accelerator: "h100",
		Intent:      "training",
	})
	if err != nil {
		log.Print(err)
		return
	}

	// Name is the resolved criteria's canonical string. Unstated dimensions
	// still render, with an empty value.
	fmt.Println(result.Name)
}
Output:
criteria(service=eks, accelerator=h100, intent=training, os=, platform=)
Example (BundleAndVerify)

Example_bundleAndVerify is the integrator path end to end: resolve a recipe, render its deployment bundle, then check what was written.

It runs hermetically against the embedded catalog, into a temporary directory, with no signing and no network — which is why it can assert its output, and why that output is "unverified".

Reading the result

Failure arrives on THREE independent channels, and checking one is not enough:

  • the returned error — the bundle could not be produced at all;
  • BundleArtifact.HasErrors() — per-bundler failures that did not abort the run, so files exist but the set is incomplete;
  • on verification, BundleVerification.PolicyFailure (the trust floor was not met) AND Report.Errors (a check itself failed, e.g. a bad checksum).

Why "unverified"

BundleOptions.Attester is nil here, so MakeBundle uses the no-op attester — the same default as `aicr bundle` without --attest. An unsigned bundle can reach "unverified" (checksums valid, no attestation) and no higher, so demanding MinTrustLevel "verified" would fail every time. Leaving it empty selects the "max" default: verify against the highest level this bundle can actually achieve. To reach "verified", pass an Attester and a binary attestation.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(
		aicr.WithRecipeSource(aicr.EmbeddedSource()),
		aicr.WithVersion("v0.19.0"),
	)
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	result, err := client.ResolveRecipeFromCriteria(ctx, &aicr.Criteria{
		Service:     "eks",
		Accelerator: "h100",
		Intent:      "training",
	})
	if err != nil {
		log.Print(err)
		return
	}

	// Per-component Helm values and stitched manifests, without touching disk.
	bundles, err := client.BundleComponents(ctx, result)
	if err != nil {
		log.Print(err)
		return
	}
	for _, b := range bundles {
		_ = b.Component.Name
		_ = b.HelmValues
		_ = b.Manifests
	}

	// Or write a full bundle directory.
	outputDir, err := os.MkdirTemp("", "aicr-bundle-")
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = os.RemoveAll(outputDir) }()

	artifact, err := client.MakeBundle(ctx, result, aicr.BundleOptions{
		OutputDir: outputDir,
	})
	if err != nil {
		log.Print(err)
		return
	}
	// Non-fatal per-bundler failures: files were written, but not all of them.
	if artifact.HasErrors() {
		log.Printf("bundle completed with %d errors", len(artifact.Errors))
		return
	}

	verification, err := client.VerifyBundle(ctx, outputDir, aicr.BundleVerifyOptions{
		// Empty means "max": verify against the highest level achievable.
		MinTrustLevel: "",
	})
	if err != nil {
		log.Print(err)
		return
	}
	if verification.PolicyFailure != "" {
		log.Printf("policy: %s", verification.PolicyFailure)
		return
	}
	if len(verification.Report.Errors) > 0 {
		log.Printf("verification: %s", verification.Report.Errors[0])
		return
	}

	fmt.Println(verification.Report.TrustLevel)
}
Output:
unverified
Example (CommittedConfig)

Example_committedConfig resolves from an AICRConfig a team commits alongside their code, so snapshot / recipe / bundle / verify settings are not retyped on each invocation.

The ORDER matters and is the reason this example exists. Criteria membership is validated against a CriteriaRegistry, which is per-DataProvider — so the Client must exist and its catalog must be loaded before RecipeCriteria can resolve a value an external --data overlay contributed. Calling RecipeCriteria first works only for values in the embedded catalog.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	cfg, err := aicr.LoadConfig(ctx, "aicr-config.yaml")
	if err != nil {
		log.Print(err)
		return
	}

	// spec.recipe.data, when the document sets one.
	source, ok := cfg.RecipeSource()
	if !ok {
		source = aicr.EmbeddedSource()
	}

	client, err := aicr.NewClient(aicr.WithRecipeSource(source))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	// Seeds the registry RecipeCriteria validates against.
	if err = client.LoadCatalog(ctx); err != nil {
		log.Print(err)
		return
	}

	criteria, err := cfg.RecipeCriteria(client.CriteriaRegistry())
	if err != nil {
		log.Print(err)
		return
	}

	opts, err := cfg.RecipeResolveOptions()
	if err != nil {
		log.Print(err)
		return
	}

	result, err := client.ResolveRecipeFromCriteriaWithOptions(ctx, criteria, opts...)
	if err != nil {
		log.Print(err)
		return
	}
	fmt.Println(result.Name)
}
Example (CriteriaDimensions)

Example_criteriaDimensions lists the criteria dimensions subject to the coverage post-condition — the values WithSnapshotCriteriaRelaxation accepts.

nodes is deliberately absent: no overlay gates on it, so it never participates in overlay selection or coverage.

package main

import (
	"fmt"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	for _, dim := range aicr.AllCriteriaDimensions() {
		fmt.Println(dim)
	}
}
Output:
service
accelerator
intent
os
platform
Example (ErrorCodes)

Example_errorCodes shows the error-handling contract. Every facade error is a *pkg/errors.StructuredError carrying an ErrorCode, and StructuredError.Is matches on that code — so errors.Is works through wrap chains without unwrapping by hand.

package main

import (
	"context"
	stderrors "errors"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
	aicrerrors "github.com/NVIDIA/aicr/pkg/errors"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	// A service no catalog defines. Membership is checked against this
	// Client's CriteriaRegistry, so the request is rejected rather than
	// silently resolving something broader.
	_, err = client.ResolveRecipeFromCriteria(ctx, &aicr.Criteria{Service: "no-such-service"})

	switch {
	case err == nil:
		fmt.Println("resolved")
	case stderrors.Is(err, aicrerrors.New(aicrerrors.ErrCodeInvalidRequest, "")):
		fmt.Println("invalid request")
	case stderrors.Is(err, aicrerrors.New(aicrerrors.ErrCodeNotFound, "")):
		fmt.Println("not found")
	default:
		fmt.Println("other")
	}
}
Output:
invalid request
Example (ResolveFromSnapshot)

Example_resolveFromSnapshot approximates `aicr recipe --snapshot`: resolve against a captured snapshot with the relax-and-retry policy the CLI applies.

The facade does not derive criteria from the snapshot

ResolveRecipeFromSnapshotWithOptions takes your Criteria verbatim and uses the snapshot only to evaluate constraints and drive snapshot-aware post-processing. Producing criteria from a snapshot's measurements is the CLI's job, and there is no facade entry point for it yet — the integration guide shows the pkg/fingerprint escape hatch and the coupling it costs.

So SUPPLY every dimension yourself, then use WithSnapshotCriteriaRelaxation to say which ones the user actually typed. Below, intent was typed and service and os were derived, so only those two may be relaxed.

These values are chosen so relaxation genuinely fires: no kind overlay states an os, so the derived os comes back uncovered and is cleared, while the stated intent is protected. Two ways to make the policy inert, both silent: name every dimension you supplied (a specified-and-stated dimension is never cleared), or leave dimensions unset (the coverage post-condition only reports dimensions you SPECIFIED, so an unset one is never uncovered).

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	// File path, HTTP(S) URL, or cm://namespace/name ConfigMap.
	snap, err := client.LoadSnapshot(ctx, "snapshot.yaml", "")
	if err != nil {
		log.Print(err)
		return
	}

	criteria := &aicr.Criteria{
		Service: "kind",      // derived: read off the snapshot
		OS:      "ubuntu",    // derived, and uncovered by every kind overlay
		Intent:  "inference", // stated: the user asked for this
	}

	result, err := client.ResolveRecipeFromSnapshotWithOptions(ctx, criteria, snap,
		aicr.WithSnapshotCriteriaRelaxation(aicr.DimensionIntent))
	if err != nil {
		log.Print(err)
		return
	}

	// Prints "relaxed os" for the criteria above: no kind overlay distinguishes
	// ubuntu, so the derived os is cleared and the retry succeeds. Intent can
	// never appear here — it was declared stated.
	for _, dim := range result.RelaxedDimensions {
		fmt.Printf("relaxed %s; resolved recipe is broader than requested\n", dim)
	}
}
Example (TrustLevels)

Example_trustLevels enumerates the bundle trust levels BundleVerifyOptions.MinTrustLevel accepts. The CLI's --min-trust-level completion is generated from this same list.

Two properties to note before validating input against it. The order is ALPHABETICAL, not by rank — do not treat position as severity. And the list is not the full accepted set: the default "max" (auto-detect the highest achievable level) and the empty string are both valid and both absent here, so a membership check built from this list alone rejects the option's own default.

package main

import (
	"fmt"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	for _, level := range aicr.TrustLevels() {
		fmt.Println(level)
	}
}
Output:
attested
unknown
unverified
verified
Example (Workflow)

Example_workflow runs the full Snapshot -> Recipe -> Bundle workflow this guide documents: load a snapshot, derive criteria from it, layer the caller's own intent on top, resolve a recipe, render a bundle, and verify what was written.

It runs hermetically against the embedded catalog and a checked-in snapshot fixture (testdata/snapshot.yaml), with no cluster and no network — which is why it can assert its output.

Two legs of the real workflow are cluster-dependent and are therefore NOT part of this runnable body: capturing the snapshot in the first place (Client.CollectSnapshot, see ExampleClient_CollectSnapshot) and validating the resolved recipe against observed state (Client.ValidateState, see ExampleClient_ValidateState). Both need a reachable cluster and are only mentioned here.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(
		aicr.WithRecipeSource(aicr.EmbeddedSource()),
		aicr.WithVersion("v0.19.0"),
	)
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	// A snapshot already captured elsewhere (see CollectSnapshot above) and
	// delivered to this pipeline stage as a file.
	snap, err := client.LoadSnapshot(ctx, "testdata/snapshot.yaml", "")
	if err != nil {
		log.Print(err)
		return
	}

	// Every dimension the snapshot could not determine stays "any"; nothing
	// is guessed.
	criteria, err := client.CriteriaFromSnapshot(snap)
	if err != nil {
		log.Print(err)
		return
	}
	// Intent is a recipe-author choice the cluster cannot reveal, so the
	// caller states it explicitly on top of what was derived.
	criteria.Intent = "training"

	result, err := client.ResolveRecipeFromCriteria(ctx, criteria)
	if err != nil {
		log.Print(err)
		return
	}

	outputDir, err := os.MkdirTemp("", "aicr-workflow-")
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = os.RemoveAll(outputDir) }()

	artifact, err := client.MakeBundle(ctx, result, aicr.BundleOptions{
		OutputDir: outputDir,
	})
	if err != nil {
		log.Print(err)
		return
	}
	if artifact.HasErrors() {
		log.Printf("bundle completed with %d errors", len(artifact.Errors))
		return
	}

	verification, err := client.VerifyBundle(ctx, outputDir, aicr.BundleVerifyOptions{
		// Empty means "max": verify against the highest level achievable.
		MinTrustLevel: "",
	})
	if err != nil {
		log.Print(err)
		return
	}
	if verification.PolicyFailure != "" {
		log.Printf("policy: %s", verification.PolicyFailure)
		return
	}
	if len(verification.Report.Errors) > 0 {
		log.Printf("verification: %s", verification.Report.Errors[0])
		return
	}

	fmt.Println(result.Name)
	fmt.Println(verification.Report.TrustLevel)
}
Output:
criteria(service=eks, accelerator=h100, intent=training)
unverified

Index

Examples

Constants

View Source
const (
	// CatalogSourceEmbedded is the Source value for built-in OSS overlays.
	CatalogSourceEmbedded = recipe.CatalogSourceEmbedded

	// CatalogSourceExternal is the Source value for overlays loaded via --data.
	CatalogSourceExternal = recipe.CatalogSourceExternal
)

CatalogSource constants for CatalogEntry.Source comparisons.

View Source
const (
	// EvidenceExitValidPassed: bundle valid, every check passed.
	EvidenceExitValidPassed = evverifier.ExitValidPassed

	// EvidenceExitValidPhaseFailures: bundle valid, but the validator
	// results recorded inside it show phase failures. Informational — the
	// evidence itself is sound.
	EvidenceExitValidPhaseFailures = evverifier.ExitValidPhaseFailures

	// EvidenceExitInvalid: bundle invalid (signature, schema, or integrity
	// failure).
	EvidenceExitInvalid = evverifier.ExitInvalid

	// EvidenceExitIncomplete: verification did not complete, so no verdict
	// was reached. Read FailureCause.Class to tell an environmental fault
	// from an operator abort (EvidenceCauseCanceled).
	EvidenceExitIncomplete = evverifier.ExitIncomplete
)

Evidence verification verdicts, mirroring the "exit" field on an EvidenceVerification. Re-exported so a consumer can branch on the verdict without importing pkg/evidence/verifier.

The verdict is NOT the process exit code. VerifyEvidence returns a verdict and a nil error whenever verification ran to completion — including when it concluded the bundle is invalid. A non-nil error means verification could not be performed at all (bad options, closed Client).

View Source
const EvidenceCauseCanceled = evverifier.CauseCanceled

EvidenceCauseCanceled is the EvidenceVerification.FailureCause.Class value marking an operator-aborted run, as opposed to the environmental faults that also produce EvidenceExitIncomplete. A CI gate branches on this to tell "we could not check this" from "the run was canceled".

View Source
const TrustedIdentityPattern = bundleverifier.TrustedRepositoryPattern

TrustedIdentityPattern is the default certificate-identity regexp that binary-attestation verification pins to: NVIDIA's release workflow on tag refs.

Override it only to pin a DIFFERENT WORKFLOW within NVIDIA/aicr — a pre-release or e2e build, say. Verifying a fork is not possible through this option and is not meant to be: ValidateIdentityPattern requires every override to begin with https://github.com/NVIDIA/aicr/, so a fork's certificate identity can never satisfy it.

Variables

This section is empty.

Functions

func RenderEvidenceJSON added in v0.20.0

func RenderEvidenceJSON(r *EvidenceVerification) ([]byte, error)

RenderEvidenceJSON renders an EvidenceVerification as the structured JSON document `aicr evidence verify --format json` emits.

func RenderEvidenceMarkdown added in v0.20.0

func RenderEvidenceMarkdown(r *EvidenceVerification) string

RenderEvidenceMarkdown renders an EvidenceVerification as the Markdown summary `aicr evidence verify` prints by default. Returns an empty string for a nil result.

func SelectFromRecipe

func SelectFromRecipe(r *RecipeResult, selector string) (any, error)

SelectFromRecipe is the context-less form of SelectFromRecipeWithContext, kept for source compatibility. It derives a defaults.FileReadTimeout-bounded context so hydration's values reads stay bounded, but the caller cannot cancel them.

Prefer SelectFromRecipeWithContext wherever a context.Context is available.

func SelectFromRecipeWithContext added in v0.19.0

func SelectFromRecipeWithContext(ctx context.Context, r *RecipeResult, selector string) (any, error)

SelectFromRecipeWithContext hydrates a resolved recipe and extracts a dot-path selector (e.g. "components.gpu-operator.values.driver.version"). An empty selector returns the entire hydrated structure. Mirrors `aicr query`, and is the implementation both the CLI query command and the REST query handler run.

Hydration reads each component's values through the DataProvider bound to the recipe, so ctx bounds real I/O: a canceled or expired context aborts the hydration rather than running to completion.

The recipe must carry internal pkg/recipe state — obtain one from Client.ResolveRecipe, Client.LoadRecipe, Client.AdoptRecipe, or WrapResolved. A facade RecipeResult constructed any other way is rejected with ErrCodeInvalidRequest.

Error contract

The OUTERMOST structured error code distinguishes the two failure stages, so a caller (e.g. an HTTP handler mapping to a status code) can shape its response without a parallel hydrate+select implementation:

  • ErrCodeNotFound — the selector path does not exist in the hydrated recipe. This code is only ever produced by the selection stage.
  • ErrCodeInvalidRequest — ctx or r is nil, or r carries no internal state.
  • Anything else (ErrCodeInternal, ErrCodeTimeout, ...) — hydration failed. Hydration never surfaces ErrCodeNotFound as the outermost code: pkg/recipe.HydrateResultWithContext wraps every per-component values failure as ErrCodeInternal, so a missing values file cannot be mistaken for a missing selector path.

Inspect the outermost code with stderrors.As, NOT stderrors.Is — Is walks the wrap chain and would match an ErrCodeNotFound cause nested inside a hydration failure.

func ToInternalAllowLists

func ToInternalAllowLists(al *AllowLists) *recipe.AllowLists

ToInternalAllowLists translates a facade AllowLists into the pkg/recipe.AllowLists enum-typed shape the resolver consumes. The string values are wrapped in the corresponding pkg/recipe enum types without validation; registry-strict mode at resolve time rejects unknown values.

Exposed so in-tree adapters (e.g., the REST handler's pre-check) share the same facade→internal projection as the Client's internal backstop, instead of inlining a parallel mapping that can drift if AllowLists gains a field.

func ToInternalCriteria added in v0.20.0

func ToInternalCriteria(c *Criteria) *recipe.Criteria

ToInternalCriteria projects a facade Criteria back onto the upstream pkg/recipe shape, parsing the plain-string fields into their enum types. Returns nil for nil input.

The counterpart to WrapCriteria, and the same bridge role ToInternalAllowLists plays: a caller that derived criteria from an AICRConfig via Config.RecipeCriteria but must hand them to a pkg/recipe API needs a supported way across, rather than reconstructing the enums by hand.

func TrustLevels added in v0.20.0

func TrustLevels() []string

TrustLevels returns the bundle trust levels that BundleVerifyOptions.MinTrustLevel accepts, sorted alphabetically (NOT by rank). Intended for building help text, shell completions, and input validation. The meta-value "max" is deliberately absent: it is a policy instruction rather than a level a bundle can be at.

That matters when validating BundleVerifyOptions.MinTrustLevel input: this list alone is NOT the accepted set. Accept "max" and the empty string too, or the check rejects the very default the option documents.

Each call returns a fresh slice, so a caller may sort or filter it.

func ValidateIdentityPattern added in v0.20.0

func ValidateIdentityPattern(pattern string) error

ValidateIdentityPattern reports whether pattern is usable as a certificate-identity override, rejecting anything that does not stay pinned to the NVIDIA/aicr repository. Call it to validate operator-supplied input before handing it to VerifyBundle or VerifyBinaryAttestation, both of which apply the same check internally.

func VerifyBinaryAttestation added in v0.20.0

func VerifyBinaryAttestation(ctx context.Context, opts BinaryAttestationVerifyOptions) (string, error)

VerifyBinaryAttestation verifies an aicr binary's own provenance attestation against a certificate identity and the binary's digest, returning the verified certificate subject.

Use it to prove the aicr binary being embedded or executed was built by NVIDIA CI, before trusting anything it produces. It is package-level rather than a Client method because it involves no recipe catalog and no configurable policy — there is nothing for a Client to contribute.

Example

ExampleVerifyBinaryAttestation proves an aicr binary was built by NVIDIA CI. It is package-level rather than a Client method: verifying a binary needs no recipe data, so it requires no Client.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	builder, err := aicr.VerifyBinaryAttestation(ctx, aicr.BinaryAttestationVerifyOptions{
		Attestation: []byte(`{}`), // the .intoto.jsonl bundle shipped with the release
		BinaryDigest: []byte{
			0x00, 0x01, 0x02, 0x03,
		},
		// Defaults to the release workflow on tag refs. An override must still
		// begin with the NVIDIA/aicr repository prefix; ValidateIdentityPattern
		// reports whether a candidate is acceptable before you use it.
		IdentityRegexp: aicr.TrustedIdentityPattern,
	})
	if err != nil {
		log.Print(err)
		return
	}
	fmt.Println(builder)
}

func WriteSnapshotDiffTable added in v0.20.0

func WriteSnapshotDiffTable(w io.Writer, result *SnapshotDiff) error

WriteSnapshotDiffTable writes a human-readable snapshot diff table.

Types

type AgentConfig

type AgentConfig struct {
	Kubeconfig       string
	Namespace        string
	Image            string
	ImagePullSecrets []string
	JobName          string

	// ServiceAccountName selects the ServiceAccount the agent pod runs
	// as. It is EXACT-IF-EXISTS, so it carries two meanings:
	//
	//   - A ServiceAccount of exactly this name already exists in
	//     Namespace: it is used verbatim, and the run creates NO
	//     ServiceAccount, Role, RoleBinding, ClusterRole or
	//     ClusterRoleBinding — and deletes none at cleanup. This is how a
	//     ServiceAccount carrying IRSA (eks.amazonaws.com/role-arn) or
	//     GKE Workload Identity (iam.gke.io/gcp-service-account)
	//     annotations stays usable: both providers pin trust to the
	//     ServiceAccount NAME, which a run-scoped name can never satisfy.
	//     Generate the RBAC that grants it the agent's permissions with
	//     snapshotter.WriteAgentRoleManifests (CLI:
	//     `aicr snapshot --add-roles-to-service-account`), which writes
	//     manifests and applies nothing, then apply them yourself.
	//   - Otherwise: a name prefix. The run creates "<prefix>-<RunID>"
	//     and the full run-scoped RBAC set, and deletes them at cleanup.
	//
	// Empty falls back to NameBase and is never probed for existence.
	//
	// Using an existing ServiceAccount waives per-run permission
	// isolation: concurrent runs sharing it share its grants, and grants
	// provisioned for DiscoverNetwork persist beyond any one run.
	ServiceAccountName string

	NodeSelector     map[string]string
	Tolerations      []corev1.Toleration
	Timeout          time.Duration
	Cleanup          bool
	Debug            bool
	Privileged       bool
	RequireGPU       bool
	RuntimeClassName string
	TemplatePath     string
	MaxNodesPerEntry int
	OS               string
	Requests         corev1.ResourceList
	Limits           corev1.ResourceList

	// Output selects where the agent Job stages its result. A cm://namespace/name
	// URI makes that ConfigMap the delivery vehicle — the Job writes there and
	// CollectSnapshot leaves it in place. A malformed cm:// URI is rejected
	// with ErrCodeInvalidRequest before any cluster access, so a typo never
	// costs a deployed Job. Any other value (including empty) stages to an
	// internal ConfigMap in Namespace; delivering the snapshot to a file,
	// stdout, or a template is then the caller's job — pass Snapshot.Raw to
	// snapshotter.DeliverSnapshot.
	Output string

	// ClusterConfigPath asks the in-pod network collector to ingest a
	// pre-existing k8s-launch-kit (l8k) cluster-config.yaml at this path.
	// The path must resolve INSIDE the agent pod, which the Job does not yet
	// mount — so CollectSnapshot rejects a non-empty value with
	// ErrCodeInvalidRequest. Use DiscoverNetwork for live discovery from a
	// Job. Local (in-pod) collection honors the file, but that path is
	// outside the facade; see Client.CollectSnapshot.
	ClusterConfigPath string

	// DiscoverNetwork enables the in-pod network collector's live l8k
	// discovery. Discovery is NOT read-only — it writes
	// nvidia.kubernetes-launch-kit.* node labels and patches NicClusterPolicy
	// via server-side-apply, so the agent's RBAC must allow those writes.
	DiscoverNetwork bool

	// AKSGPUPoolsPath points at an operator-supplied
	// `az aks nodepool list -o json` dump on the machine running this
	// client. The snapshotter projects it into the snapshot's
	// K8s.aks-gpu-pools.gpu-driver reading (ADR-015 DD3) — controller-
	// side, before any cluster work; the file never enters the cluster.
	// Required for AKS profile-qualified resolution from a collected
	// snapshot; empty disables the projection.
	AKSGPUPoolsPath string

	// RunID scopes every resource this deployment creates (Job, RBAC, and
	// the internal staging ConfigMap when Output does not name one) to a
	// single run, so concurrent snapshot-agent runs never collide on a
	// shared resource name. Empty lets CollectSnapshot's underlying
	// deployment generate one; SDK and CLI callers normally leave it
	// unset. Set it explicitly to correlate this run with an external
	// identifier — `aicr validate` does this to give its live-capture
	// snapshot agent and its validator Jobs the same RunID.
	RunID string

	// NameBase prefixes generated Job/ServiceAccount/RBAC names. The
	// fallback is per name, not all-or-nothing: the Job uses JobName when
	// set and NameBase otherwise, while the ServiceAccount, Role and
	// RoleBinding use ServiceAccountName when set and NameBase otherwise.
	// Setting only one of the two therefore leaves NameBase governing the
	// other. Defaults to "aicr" when also empty.
	//
	// JobName is likewise an optional prefix, not a required name —
	// RunID is appended to whichever prefix applies, so the deployed Job
	// name is always run-scoped. ServiceAccountName is a prefix only
	// when no ServiceAccount of that exact name exists; see its own
	// documentation above.
	NameBase string

	// OKEAddonsPath points at an operator-supplied
	// `oci ce cluster list-addons --cluster-id <cluster-ocid> --all --output json` dump. Same
	// contract as AKSGPUPoolsPath: projected controller-side and merged
	// into the snapshot as the oke-addons subtype.
	OKEAddonsPath string
}

AgentConfig is the deployment-time configuration for the snapshot- collection Job passed to Client.CollectSnapshot. Facade-owned; field-for-field mirror of pkg/snapshotter.AgentConfig. Tolerations keep k8s.io/api/core/v1.Toleration since kubernetes/api is itself stable. Nil Tolerations use a tolerate-all default; a non-nil empty slice explicitly disables that default.

The mirror is enforced, not conventional: TestAgentConfigMirrorsInternal fails when either struct gains, drops, or retypes a field, and every in-tree snapshot Job (`aicr snapshot`, `aicr validate`) is deployed through this type — so an unplumbed field is a test failure rather than a silent zero value.

type AllowLists

type AllowLists struct {
	// Accelerators is the set of accepted accelerator identifiers
	// (e.g., "h100", "b200"). Empty = accept all.
	Accelerators []string

	// Services is the set of accepted service identifiers
	// (e.g., "eks", "gke"). Empty = accept all.
	Services []string

	// Intents is the set of accepted intent identifiers
	// (e.g., "training", "inference"). Empty = accept all.
	Intents []string

	// OSTypes is the set of accepted OS identifiers
	// (e.g., "ubuntu", "rhel"). Empty = accept all.
	OSTypes []string
}

AllowLists fences which criteria values the resolve path accepts on a Client constructed via WithAllowLists. Facade-owned; the typed-enum fields on pkg/recipe.AllowLists project to plain string slices so the facade does not propagate pkg/recipe's enum identifiers across the semver boundary. A nil receiver, or an AllowLists whose slices are all empty, accepts every value (the documented "no fencing" mode). An "any" value on a Criteria field is always accepted regardless of the allowlist, matching the pkg/recipe behavior.

func ParseAllowListsFromEnv

func ParseAllowListsFromEnv() (*AllowLists, error)

ParseAllowListsFromEnv builds an AllowLists from the AICR_ALLOWED_* environment variables (AICR_ALLOWED_ACCELERATORS, AICR_ALLOWED_SERVICES, AICR_ALLOWED_INTENTS, AICR_ALLOWED_OS). Returns nil when none are set — WithAllowLists treats a nil AllowLists as allow-all. Pass the result to WithAllowLists.

func WrapAllowLists

func WrapAllowLists(al *recipe.AllowLists) *AllowLists

WrapAllowLists projects a pkg/recipe.AllowLists into the facade AllowLists shape. Use at the boundary where in-tree callers parse allowlists from configuration (e.g., recipe.ParseAllowListsFromEnv) and hand them to the facade. Returns nil for nil input.

The facade slices are independent copies; mutating either side after wrap does not affect the other.

type BinaryAttestationVerifyOptions added in v0.20.0

type BinaryAttestationVerifyOptions struct {
	// Attestation is the raw Sigstore bundle for the binary. Required.
	//
	// Taking bytes rather than a path is deliberate: it lets a caller
	// verify the EXACT content it is about to use, with no
	// verify-then-reread window in which the file could change.
	Attestation []byte

	// BinaryDigest is the RAW (not hex-encoded) SHA-256 of the binary the
	// attestation must cover. Required.
	BinaryDigest []byte

	// IdentityRegexp overrides the certificate identity the attestation is
	// pinned to. Empty uses TrustedIdentityPattern. Must BEGIN with
	// "https://github.com/NVIDIA/aicr/" (a leading "^" is allowed) and must
	// not use top-level alternation, so the override stays confined to the
	// repository; see ValidateIdentityPattern.
	IdentityRegexp string
}

BinaryAttestationVerifyOptions configures VerifyBinaryAttestation.

type BundleArtifact

type BundleArtifact = *result.Output

BundleArtifact summarizes a completed bundle generation: file count, total size, duration, per-bundler results, and the output directory the files were written to. Deliberate transparent alias of pkg/bundler/result.Output. Inspect HasErrors() for non-fatal per-bundler failures; the bundle files themselves are on disk under OutputDir.

type BundleAttester

type BundleAttester = attestation.Attester

BundleAttester signs bundle content. Deliberate transparent alias of pkg/bundler/attestation.Attester. The zero value of BundleOptions leaves this nil, in which case MakeBundle uses the bundler's no-op attester (the same default bundler.New applies when --attest is not set).

type BundleConfig

type BundleConfig = config.Config

BundleConfig is the bundler configuration — deployer mode, value overrides, node selectors, tolerations, vendoring, app/chart names, etc. Deliberate transparent alias of pkg/bundler/config.Config. Construct one with config.NewConfig(config.WithDeployer(...), ...) — the same builder the CLI bundle command and the REST /v1/bundle handler use, so MakeBundle reproduces their exact output byte-for-byte.

type BundleInputOptions added in v0.21.0

type BundleInputOptions struct {
	RecipePath      string
	ImageRefsPath   string
	OutputTarget    *oci.Reference
	OutputTargetRaw string
	InsecureTLS     bool
	PlainHTTP       bool
}

BundleInputOptions carries the spec.bundle fields the CALLER consumes, not the bundler: which recipe to load, which image-refs file to write to, and where to push the finished bundle.

Separate from BundleOptions on purpose. MakeBundle takes an already-resolved RecipeResult and does not push — the caller does, after it returns — so these on MakeBundle's parameter would be surface that nothing reads. The transport pair mirrors EvidenceOptions/SignOptions carrying PlainHTTP/InsecureTLS for the same "the caller reaches a registry, MakeBundle does not" reason.

type BundleOptions

type BundleOptions struct {
	// Config carries an already-built bundler configuration and, when
	// non-nil, wins over the 18 flat fields below (Deployer through AppName).
	// It does NOT override Attester, OIDCResolve, BinaryAttestation,
	// OutputDir or Timeout — see "Two ways to supply the bundler
	// configuration" above. When both Config and every flat field are unset,
	// MakeBundle uses config.NewConfig() — the same default bundler.New
	// applies (Helm deployer, no overrides).
	Config *BundleConfig

	// Deployer selects the bundle output format (Helm, Argo CD, Argo CD Helm
	// chart, Flux, or Helmfile). The zero value is DeployerType(""), which
	// bundlerConfig treats as "not set" and leaves at bundler config.NewConfig's
	// own default (Helm) — unlike every other flat field here, an empty
	// DeployerType is not a safe pass-through: WithDeployer("") would
	// overwrite that default with an empty deployer, which fails bundling
	// with "unsupported deployer type" instead of defaulting to Helm.
	Deployer config.DeployerType

	// Repo is the deployment repository URL: the Argo CD Application source,
	// or (for --deployer argocd with OCI output) the origin baked into
	// argocd-helm's values.yaml default repoURL.
	Repo string

	// ValueOverrides are parsed "component:path=value" Helm value overrides.
	ValueOverrides []config.ComponentPath

	// DynamicValues declares component:path value paths supplied at install
	// time rather than baked into the bundle.
	DynamicValues []config.ComponentPath

	// SystemNodeSelector / SystemNodeTolerations schedule non-accelerated
	// (system) component workloads.
	SystemNodeSelector    map[string]string
	SystemNodeTolerations []corev1.Toleration

	// AcceleratedNodeSelector / AcceleratedNodeTolerations schedule
	// GPU-accelerated component workloads.
	AcceleratedNodeSelector    map[string]string
	AcceleratedNodeTolerations []corev1.Toleration

	// DRAEvictionNodeLabel is the singular DRA eviction contract label. Nil
	// means "not requested" — bundlerConfig leaves the bundler's own
	// NVIDIA-documented default in place rather than overwriting it with a
	// zero label. See issue #2469.
	DRAEvictionNodeLabel *config.NodeLabel

	// WorkloadGate is the taint applied to prevent eviction of running
	// workloads while the gate is active. Nil means no gate is configured;
	// WithWorkloadGateTaint treats nil as a no-op, so passing it through
	// unconditionally is safe.
	WorkloadGate *corev1.Taint

	// WorkloadSelector labels the workloads WorkloadGate protects.
	WorkloadSelector map[string]string

	// Nodes is the estimated node count used for capacity-aware rendering.
	// 0 means unset.
	Nodes int

	// StorageClass / SharedStorageClass name the Kubernetes StorageClasses
	// components request for local and RWX-shared volumes respectively.
	StorageClass       string
	SharedStorageClass string

	// Attest enables bundle attestation and binary verification. Off
	// (false) by default — the same default bundler.New applies when
	// --attest is not passed, so the zero value stays a true no-op.
	Attest bool

	// CertIDRegexp overrides the expected certificate identity pattern for
	// binary attestation verification.
	CertIDRegexp string

	// VendorCharts pulls upstream Helm chart bytes into the bundle so the
	// artifact is air-gap deployable. Off (false) by default.
	VendorCharts bool

	// AppName overrides the parent Argo Application's metadata.name. Empty
	// means each deployer applies its own default. See #1011.
	AppName string

	// Attester signs bundle content. When nil, MakeBundle derives one from
	// OIDCResolve; when both are unset, the no-op attester applies (matching
	// bundler.New's default when --attest is not set).
	//
	// A non-nil Attester WINS over OIDCResolve. That precedence is the
	// facade's derive-don't-apply rule applied to signing: an explicitly
	// supplied signer is a caller decision, and silently rebuilding one from
	// config would discard it. The aicrd server relies on this to inject its
	// own attester; the CLI sets it via attestation.ResolveAttesterLazy when
	// --attest is passed, so both keep working unchanged.
	Attester BundleAttester

	// OIDCResolve configures keyless/KMS signing when Attester is nil and
	// OIDCResolve.Attest is true, letting a committed spec.bundle.attestation
	// reach the bundler without the caller constructing an Attester itself.
	// Config.BundleOptions derives it.
	//
	// Ignored when Attest is false, so the zero value stays a true no-op
	// rather than resolving a signer nobody asked for. Mirrors the
	// OIDCResolve field EvidenceOptions and SignOptions already carry.
	OIDCResolve OIDCResolveOptions

	// BinaryAttestation, when non-empty, is a pre-verified binary attestation
	// (Sigstore bundle bytes) embedded into attested bundles as tool provenance.
	// The caller (e.g. the aicrd server, which verifies its in-image attestation
	// once at startup) is responsible for having verified these bytes. Empty
	// leaves the bundler's default per-run discover-and-verify path unchanged
	// (the CLI passes nil and relies on the attestation shipped next to its
	// install-script binary).
	BinaryAttestation []byte

	// OutputDir is the directory bundle files are written to. Empty
	// means the current directory ("."), matching Make's default.
	OutputDir string

	// Timeout optionally caps the bundle run. When > 0, MakeBundle wraps
	// the caller's context with context.WithTimeout(ctx, Timeout) so the
	// run is bounded by the smaller of this and any tighter parent
	// deadline. When 0 (the zero value), MakeBundle imposes NO
	// facade-level deadline and runs under the caller's ctx as-is —
	// large bundles, --vendor-charts, and attestation/signing can each
	// exceed a fixed cap. The REST /v1/bundle handler sets this to
	// defaults.BundleHandlerTimeout to preserve its 60s request boundary;
	// the CLI bundle command leaves it 0 so long bundles are uncapped.
	Timeout time.Duration
}

BundleOptions configures a MakeBundle call. It mirrors exactly what bundler.New / (*DefaultBundler).Make accept so the facade reproduces the same full deployer-mode bundle artifact the CLI bundle command and REST /v1/bundle handler produce today.

Two ways to supply the bundler configuration

The 18 flat fields below (Deployer through AppName) are what Config.BundleOptions derives from spec.bundle, field by field, so a config-driven caller (the CLI in particular) can read and override individual settings with its own flag precedence instead of reaching into an opaque built Config. bundlerConfig assembles them into a *BundleConfig via the same bundlerconfig.With* options the CLI and the REST handler use.

Config remains as an escape hatch for a caller that already has a fully built *BundleConfig from a source the flat fields cannot express — the CLI's own bundle-generation call (Version, TargetRevision, ReadinessHooks, Serial, Flux/OCI naming, typed value overrides — all CLI-flag-only, no spec.bundle counterpart) and the aicrd /v1/bundle handler (built from HTTP query parameters via bundler.ParseBundleConfig, then mutated in place with bundlercfg.WithAttest once the request's signing decision is known). When Config is non-nil, it WINS outright over the 18 flat fields it supersedes (Deployer through AppName — bundlerConfig's own inputs) — the same derive-don't-apply precedence Attester already documents against OIDCResolve. Config.BundleOptions never sets Config, so a config-driven derivation always routes through the flat fields.

Config does NOT reach Attester, OIDCResolve, BinaryAttestation, OutputDir or Timeout: bundlerConfig() only ever reads Config in place of the 18 bundler-config fields, so these five stay caller-supplied regardless of Config. The REST /v1/bundle handler depends on this split — it sets Config and OutputDir/Timeout in the same BundleOptions literal and requires both to take effect.

Config's Attest must agree with OIDCResolve.Attest

The split above has one guarded exception. Config's baked-in Attest and OIDCResolve.Attest are independent gates — one reaches the bundler's attestBundle check, the other decides whether MakeBundle derives a signer — and when Attester is nil (so OIDCResolve is actually consulted) MakeBundle rejects a Config and OIDCResolve.Attest that disagree, with ErrCodeInvalidRequest naming both values, rather than silently producing unsigned "signed" output or burning an OIDC/KMS round trip whose result Config then discards. Supplying Attester directly sidesteps the check entirely — it wins outright and neither gate is consulted. Both current callers keep the two in lockstep already: the CLI always supplies Attester (so OIDCResolve is never consulted), and the REST handler sets Config's Attest and Attester together.

type BundleVerification added in v0.20.0

type BundleVerification struct {
	// Report is the per-check verification outcome. Never nil on a nil
	// error.
	Report *BundleVerifyReport

	// PolicyFailure describes the first policy assertion the bundle failed
	// (trust floor, required creator, version constraint), or is empty
	// when every assertion passed.
	//
	// A policy failure is DATA, not an error: VerifyBundle still returns
	// the full Report so a caller can render or log why the bundle fell
	// short. Callers that need a failed policy to abort should check this
	// field (and Report.Errors) explicitly.
	PolicyFailure string
}

BundleVerification pairs the verification report with the outcome of the policy assertions in BundleVerifyOptions.

type BundleVerifyOptions added in v0.20.0

type BundleVerifyOptions struct {
	// CertificateIdentityRegexp overrides the identity pattern that binary
	// attestation verification pins to. Must BEGIN with
	// "https://github.com/NVIDIA/aicr/" (a leading "^" is allowed) and must
	// not use top-level alternation, so it stays confined to the repository;
	// VerifyBundle rejects a pattern that does not before doing any work.
	// Empty uses TrustedIdentityPattern.
	CertificateIdentityRegexp string

	// Key verifies a key-signed bundle attestation instead of a keyless
	// one: a KMS key URI (awskms:// | gcpkms:// | azurekms:// |
	// hashivault://) or a path to a local PEM public key. Independent of
	// CertificateIdentityRegexp, which pins the separate binary
	// attestation; the two coexist.
	Key string

	// TrustRoot is a path to a private Sigstore trusted_root.json (from a
	// self-hosted Fulcio/Rekor). ADDITIVE to AICR's built-in public-good
	// root, so NVIDIA-signed and privately-signed bundles both verify.
	TrustRoot string

	// MinTrustLevel is the trust floor the verified bundle must reach: one
	// of "verified", "attested", "unverified", "unknown", or the
	// meta-value "max".
	//
	// EMPTY MEANS "max" — auto-detect the highest level this bundle could
	// achieve and require it. This deliberately differs from the
	// underlying pkg/bundler/verifier.Policy, where an empty value skips
	// the trust check entirely: a caller who does not think about the
	// trust floor should get the strict default, not no gate. Lower the
	// floor by naming a level explicitly.
	MinTrustLevel string

	// RequireCreator pins the OIDC identity on the bundle attestation's
	// signing certificate. Empty accepts any creator.
	RequireCreator string

	// CLIVersionConstraint constrains the aicr version recorded in the
	// attestation predicate. Supports >=, >, <=, <, ==, != ; a bare
	// version (e.g. "0.16.0") is treated as ">= 0.16.0".
	CLIVersionConstraint string

	// IgnoreTLog skips transparency-log verification so a bundle produced
	// by `bundle --signing-key ... --tlog-upload=false` verifies with no
	// transparency-log network calls. REQUIRES Key — the air-gapped path
	// is key-based, and VerifyBundle rejects the combination otherwise.
	// Insecure relative to the default: with no transparency log there is
	// no trusted timestamp proving when the signature was made.
	IgnoreTLog bool

	// Timeout overrides the facade's operation cap for this call.
	//
	// Nil -- the zero value -- keeps defaults.VerifyOperationTimeout, so a
	// caller who never considered this gets today's behavior. A pointer to 0
	// imposes NO facade cap and runs under the caller's context unchanged. A
	// positive value sets an explicit cap.
	//
	// The pointer exists to make 0 mean "uncapped" rather than "default",
	// matching WithValidationTimeout(0). A plain duration cannot distinguish
	// unset from zero, so it would have had to spell uncapped some other way --
	// and a caller who learned 0-means-uncapped from ValidateState would then
	// get the opposite here, silently capped when they asked for unbounded.
	Timeout *time.Duration
}

BundleVerifyOptions configures Client.VerifyBundle.

The fields mirror pkg/config.VerifySpec one-for-one so a caller holding an AICRConfig can populate this struct without a translation table: the first three come from spec.verify.trust and the next three from spec.verify.policy. IgnoreTLog has no config counterpart by design — it weakens the trust floor by dropping the transparency-log requirement, and keeping it out of the schema means a checked-in file can never silently disable that check.

type BundleVerifyReport added in v0.20.0

type BundleVerifyReport = bundleverifier.VerifyResult

BundleVerifyReport is the per-check outcome of bundle verification.

Deliberate transparent alias of pkg/bundler/verifier.VerifyResult. It is a flat, read-only report whose fields are already the documented JSON contract of `aicr verify --format json`; owning a translated copy would duplicate that contract without insulating consumers from anything, since any field change would have to propagate to stay useful.

type CNCFEvidenceOptions added in v0.21.0

type CNCFEvidenceOptions struct {
	// Dir is spec.validate.evidence.cncf.dir, the directory CNCF AI
	// Conformance evidence markdown is written to.
	Dir string

	// CNCFSubmission is spec.validate.evidence.cncf.cncfSubmission: whether to
	// collect detailed behavioral evidence for a CNCF AI Conformance
	// submission rather than the lighter default evidence.
	CNCFSubmission bool

	// Features is spec.validate.evidence.cncf.features, restricting collection
	// to specific evidence features. Empty means "all features" — only
	// honored when CNCFSubmission is true.
	Features []string
}

CNCFEvidenceOptions carries spec.validate.evidence.cncf — the CNCF AI Conformance evidence-markdown settings (--evidence-dir / --cncf-submission / --feature). Consumed by the CALLER, not by a Client method: there is no Client.Emit* for CNCF evidence, so validateFlagCombinations, cncf.New and runCNCFSubmission read this directly. Mirrors SnapshotOutputOptions, which carries spec.snapshot.output the same way despite Client.CollectSnapshot not consuming it either.

type CatalogEntry added in v0.15.0

type CatalogEntry struct {
	// Name is the overlay name, e.g. "h100-eks-ubuntu-training".
	Name string `json:"name" yaml:"name"`

	// Criteria is the set of dimensions this overlay targets.
	Criteria Criteria `json:"criteria" yaml:"criteria"`

	// IsLeaf is true when this overlay is a catalog leaf (no other
	// overlay inherits from it).
	IsLeaf bool `json:"is_leaf" yaml:"is_leaf"`

	// Source is the data provenance: "embedded" or "external".
	Source string `json:"source" yaml:"source"`

	// Profile is the effective declaration after inheritance and co-match
	// resolution. It is nil for an unprofiled catalog entry.
	Profile *ProfileSummary `json:"profile,omitempty" yaml:"profile,omitempty"`
}

CatalogEntry describes one overlay in the recipe catalog, returned by Client.ListCatalog.

IsLeaf is true when the overlay is a leaf — no other overlay in the catalog lists this one as its spec.base. Leaf overlays are the most specific recipes for a given criteria combination.

Source is one of CatalogSourceEmbedded or CatalogSourceExternal.

type CatalogSignOptions added in v0.20.0

type CatalogSignOptions struct {
	// Output is the path the Sigstore bundle is written to. Empty computes
	// and signs the digest but writes no file, leaving the serialized
	// bundle available on CatalogSignResult.BundleJSON.
	Output string

	// OIDCResolve carries the keyless-signing token-resolution inputs.
	// SignCatalog sets Attest itself — signing is the whole operation —
	// so leaving that field false does not disable it.
	//
	// Four fields are REJECTED rather than passed through, because
	// VerifyCatalog cannot verify what they would produce: SigningKey,
	// FulcioURL, RekorURL, and DisableTLogUpload. See SignCatalog's godoc
	// for the full statement of that constraint, including what it does
	// NOT cover.
	OIDCResolve OIDCResolveOptions
}

CatalogSignOptions configures Client.SignCatalog.

type CatalogSignResult added in v0.20.0

type CatalogSignResult struct {
	// Digest is the hex-encoded SHA-256 of the combined catalog content
	// that was signed.
	Digest string

	// BundleJSON is the serialized Sigstore bundle. Never nil on a nil
	// error.
	BundleJSON []byte
}

CatalogSignResult is what SignCatalog returns on success.

type CatalogVerification added in v0.20.0

type CatalogVerification struct {
	// Identity is the SubjectAlternativeName claim from the signing
	// certificate.
	Identity string

	// Digest is the hex-encoded SHA-256 of the catalog content that was
	// verified.
	Digest string
}

CatalogVerification is what VerifyCatalog returns on success.

type CatalogVerifyOptions added in v0.20.0

type CatalogVerifyOptions struct {
	// CertificateIdentityRegexp overrides the NVIDIA CI identity pattern.
	// Must BEGIN with "https://github.com/NVIDIA/aicr/" (a leading "^" is
	// allowed) and must not use top-level alternation. Empty uses
	// TrustedIdentityPattern.
	CertificateIdentityRegexp string

	// Timeout overrides the facade's operation cap for this call.
	//
	// Nil -- the zero value -- keeps defaults.VerifyOperationTimeout, so a
	// caller who never considered this gets today's behavior. A pointer to 0
	// imposes NO facade cap and runs under the caller's context unchanged. A
	// positive value sets an explicit cap.
	//
	// The pointer exists to make 0 mean "uncapped" rather than "default",
	// matching WithValidationTimeout(0). A plain duration cannot distinguish
	// unset from zero, so it would have had to spell uncapped some other way --
	// and a caller who learned 0-means-uncapped from ValidateState would then
	// get the opposite here, silently capped when they asked for unbounded.
	Timeout *time.Duration
}

CatalogVerifyOptions configures Client.VerifyCatalog.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the single entry point for external Go consumers.

Concurrent ResolveRecipe calls are safe — the Builder itself is thread-safe over its read-only state. The mu guards the small window where Close swaps builder/dp to nil; without it, concurrent ResolveRecipe + Close on the same Client is a data race because the field write in Close is unsynchronised against the field read at the top of ResolveRecipe.

func NewClient

func NewClient(opts ...Option) (*Client, error)

NewClient constructs a Client with the supplied functional options. OCI source construction uses a bounded compatibility context; callers that need cancellation or a tighter deadline should use NewClientContext. Callers must provide a recipe source via WithRecipeSource.

For FilesystemSource, the external directory is layered OVER the embedded recipe data — files in the directory override embedded equivalents, and recipes must include a registry.yaml at the root.

OCI sources require an immutable sha256 manifest digest. One defaults.OCIRecipeConstructionTimeout deadline bounds the complete OCI source construction; nested per-phase pull deadlines can only shorten that shared budget.

func NewClientContext added in v0.20.0

func NewClientContext(ctx context.Context, opts ...Option) (*Client, error)

NewClientContext constructs a Client with the supplied functional options and derives all OCI source I/O from ctx. The complete operation remains bounded by defaults.OCIRecipeConstructionTimeout when the caller provides a longer deadline or no deadline.

func (*Client) AdoptRecipe

func (c *Client) AdoptRecipe(ctx context.Context, rec *recipe.RecipeResult) (*RecipeResult, error)

AdoptRecipe wraps a raw pkg/recipe.RecipeResult — typically decoded from an external source such as a REST /v1/bundle POST body — into a Client-owned *RecipeResult ready for MakeBundle. The returned RecipeResult is bound to this Client's DataProvider and owner-stamped, so it passes MakeBundle's ownership and provider-isolation checks exactly as a LoadRecipe result does.

Use this when the caller already holds a fully-hydrated RecipeResult (not a criteria request or a file path) and needs to bundle it through the facade. In-process consumers that resolve via ResolveRecipe / LoadRecipe should use those results directly; AdoptRecipe is for the decode-then-bundle boundary.

func (*Client) BundleComponents

func (c *Client) BundleComponents(ctx context.Context, r *RecipeResult) ([]ComponentBundle, error)

BundleComponents resolves Helm values and rendered manifests for each component in a previously-resolved RecipeResult. The returned slice mirrors r.Components 1:1 — same order, same length — so callers correlate by index.

When to call

Call AFTER ResolveRecipe; pass that call's *RecipeResult unchanged. BundleComponents reads the internal pkg/recipe.RecipeResult that ResolveRecipe attached to the facade RecipeResult — it does NOT re-resolve from criteria. A RecipeResult constructed by the caller (rather than returned from ResolveRecipe) has a nil internal field and BundleComponents returns ErrCodeInvalidRequest.

Per-Client DataProvider isolation

Both values-file reads (Helm components) and manifest-file reads (Helm supplemental + Kustomize) are bound to this Client's own DataProvider via the WithProvider variants on the recipe package (recipe.RecipeResult.GetValuesForComponentWithProvider, recipe.GetManifestContentWithProvider). Two Clients constructed from different recipe sources can BundleComponents concurrently without contaminating each other's bundle output.

History: pre-v0.2 the values and manifest paths short-circuited through recipe.GetDataProvider() — the process-global DataProvider singleton. With two Clients A and B pointing at different sources, an eviction+repopulate sequence on A's cache followed by a B BundleComponents call could return values or manifests resolved against A's recipe source. That gap is closed; the metadata store and component registry were already per-Client at the time and stayed correct throughout, so ResolveRecipe results never drifted.

Read-once value coherence

Each component's effective Helm values are resolved exactly once per call, and that same snapshot feeds the accounting check, the registry-declared component validations (including the gpu-operator driver-ownership coherence gate), and the returned ComponentBundle. A DataProvider need not be stable — LayeredDataProvider re-reads external --data files on every call — so a gate that resolved values independently could validate one set of values while a different set was returned. Pinning the snapshot removes that window: what the gates examined is what you get back (issue #1873 item A).

Synchronization

Read-locks Client.mu so a concurrent Close can't race the values load. The lock is held only across the snapshot of c.builder and c.dp; the values and manifest reads themselves run unlocked (consistent with ResolveRecipe's pattern). The DataProvider snapshot is the per-Client provider this Client owns — the same one its Builder is bound to via recipe.WithDataProvider.

func (*Client) Close

func (c *Client) Close() error

Close releases this Client's cached metadata store, component registry, and criteria registry from the recipe package's internal caches. Call when a Client is no longer needed (cache eviction in a higher-level memoiser, controller shutdown) to prevent unbounded memory growth — the recipe package keys its caches on DataProvider identity and does not auto-evict, so a process that observes many distinct recipe sources over time would otherwise grow memory monotonically.

Safe to call on a nil receiver and safe to call concurrently or multiple times. Every non-nil caller waits for the same teardown and receives the same cached cleanup result. OCI-backed Clients remove only the private child workspace they own; a removal failure is returned with its structured code.

func (*Client) CollectSnapshot

func (c *Client) CollectSnapshot(ctx context.Context, cfg *AgentConfig) (*Snapshot, error)

CollectSnapshot deploys the snapshotter Job to the cluster identified by cfg.Kubeconfig and returns the captured Snapshot.

This is the single Job-mode collection path in the tree: `aicr snapshot` and `aicr validate` both run it, so the facade AgentConfig mirror is exercised on every snapshot AICR takes.

CollectSnapshot does NOT consult the Client's recipe data provider — the Client is required only to keep the facade surface uniform (every public operation goes through a Client) and to leave room for future per-Client telemetry hooks or cluster-connection caching without breaking signatures. CollectSnapshot is therefore safe even on a Client whose recipe source is unrelated to the target cluster.

cfg.Kubeconfig is the path (or empty for in-cluster).

Required and defaulted fields

cfg.Namespace is the only field you must set: it is where the Job, its RBAC, and the result ConfigMap are created, and deploying a privileged cluster-reading Job into an unstated namespace is not a safe default. An empty one is rejected with ErrCodeInvalidRequest before any cluster access.

cfg.Image is defaulted when empty to the tag matching this Client's WithVersion (a Client with no version, like a development build, gets :latest). Set it explicitly to pin a different agent generation or a mirrored registry.

cfg.JobName and cfg.ServiceAccountName are optional naming PREFIXES, not exact names. Leaving them empty is fine: cfg.NameBase (default defaults.AgentName) supplies the prefix instead. cfg.RunID is appended to whichever prefix applies, so two callers that both omit these names still get distinct objects rather than shared ones — see Concurrency. Other fields fall back to the defaults documented on snapshotter.AgentConfig.

One exception to the prefix rule, because it is the point of the mode: a cfg.ServiceAccountName that names a ServiceAccount already present in cfg.Namespace is used VERBATIM, and this call then creates and deletes no RBAC at all. That is how a caller runs the agent under an IRSA or Workload Identity account, whose cloud trust is pinned to the account name and so cannot survive a run-scoped rename. It waives per-run permission isolation: concurrent runs sharing that account share its grants.

Output and delivery

The returned Snapshot carries both the parsed form and, in Snapshot.Raw, the exact bytes the agent emitted. CollectSnapshot does not write them anywhere except when cfg.Output names a ConfigMap (cm://namespace/name), which the Job writes directly. Persisting to a file, stdout, or a Go template is the caller's step — pass Snapshot.Raw to snapshotter.DeliverSnapshot, as `aicr snapshot` does. Delivering Raw rather than re-serializing the parsed snapshot is what keeps the output byte-identical to the agent's when a newer agent image emits fields this binary does not model.

Fail-before-mutate

Inputs that can be rejected without contacting the cluster are checked before the Kubernetes client is built, so a rejection never leaves RBAC or a Job behind (with cfg.Cleanup false — the zero value — they would persist). That covers a malformed cfg.Output ConfigMap URI and a non-empty cfg.ClusterConfigPath, both ErrCodeInvalidRequest.

Deliberately outside this method

Two snapshot capabilities are NOT reachable through CollectSnapshot, by design, because neither deploys a Job:

  • Local (in-pod) collection — the mode the agent container itself runs under AICR_AGENT_MODE=true, and the dev bypass of the same name. It runs collectors in-process against the local node instead of deploying an agent, so it needs a collector.Factory and a serializer.Serializer, types the semver-stable facade deliberately does not expose. Use snapshotter.NodeSnapshotter directly, as pkg/cli/snapshot.go does. It takes no AgentConfig, so leaving it out costs no coverage of the field mirror: every deployed Job still projects through this method.
  • cfg.ClusterConfigPath — an l8k cluster-config.yaml ingested by the in-pod network collector. The path must resolve inside the pod and the Job does not mount it, so Job mode rejects a non-empty value with ErrCodeInvalidRequest. Use cfg.DiscoverNetwork for live discovery from a Job, or the local mode above to read a host file.

Timeout

The operation is bounded by cfg.Timeout + defaults.SnapshotOperationGrace (or defaults.SnapshotOperationTimeout + grace when cfg.Timeout is unset), so a caller passing context.Background() still gets a bounded run. The grace exists because cfg.Timeout budgets Job completion only — deployment and result retrieval sit outside it, and a bare cap would silently shrink the completion budget. A tighter caller deadline always wins.

Errors:

  • ErrCodeInvalidRequest when the Client is nil, cfg is nil, or the Client has been Closed.
  • All snapshotter errors propagate unwrapped — they already carry the appropriate pkg/errors codes (ErrCodeInternal for deployment failures, ErrCodeTimeout for context expiry, etc.).

Concurrency

Concurrent CollectSnapshot calls are safe, at the Client level and against the same cluster. Each call gets its own RunID (cfg.RunID when set, otherwise generated) and, from it, its own Job, ServiceAccount, Role, RoleBinding, ClusterRole, ClusterRoleBinding and — when cfg.Output does not name one — its own staging ConfigMap. Every object is deleted under a UID precondition recorded when this run created it, so no run can delete something another run owns. This replaces the earlier one-at-a-time constraint (#2120).

Two effects remain shared, both by design:

  • Two calls that set cfg.Output to the SAME explicit cm://namespace/name URI write to that one caller-named ConfigMap and overwrite each other. Output identifies a caller-owned destination, not a run-scoped one, so RunID does not disambiguate it.
  • Two calls naming the same existing cfg.ServiceAccountName run under that one identity and share its grants, since neither creates RBAC.
Example

ExampleClient_CollectSnapshot captures cluster state by deploying the snapshotter Job, for callers that do not already have a snapshot file. Requires a reachable cluster and RBAC to create the Job.

Namespace is the only field that must be set. Image, JobName, and ServiceAccountName are defaulted when empty — the image to the tag matching the Client's WithVersion. This example pins the image anyway, which is what an air-gapped or version-skew-sensitive deployment wants.

package main

import (
	"context"
	"log"
	"os"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	snap, err := client.CollectSnapshot(ctx, &aicr.AgentConfig{
		Namespace:          "aicr-system",
		Image:              "ghcr.io/nvidia/aicr:v0.19.0",
		JobName:            "aicr",
		ServiceAccountName: "aicr",
		Cleanup:            true,
	})
	if err != nil {
		log.Print(err)
		return
	}

	// Persist Raw rather than re-serializing: a newer agent image can emit
	// fields this binary's Snapshot type does not model, and a typed round
	// trip silently drops them.
	if err = os.WriteFile("snapshot.yaml", snap.Raw, 0o600); err != nil {
		log.Print(err)
		return
	}
}

func (*Client) ComputeHealth added in v0.15.0

func (c *Client) ComputeHealth(ctx context.Context, filter *Criteria) (*health.Report, error)

ComputeHealth scores the structural health of every leaf recipe in this Client's catalog and returns a deterministic *health.Report, optionally narrowed by filter. Computation is delegated wholesale to pkg/health.Compute; this facade only binds the Client's own DataProvider and version so health is scored against the same catalog (including any --data overlays) the Client resolves with — never the process-global embedded catalog.

filter narrows enumeration to leaf overlays carrying every explicitly set criteria dimension; nil scores all leaf combos. Empty/"any" filter dimensions place no constraint.

health.Compute applies its own catalog-wide timeout (defaults.HealthComputeTimeout), so this method does not impose the shorter per-operation timeout the resolve methods use.

Returns ErrCodeInvalidRequest on a nil/closed Client or nil context, and propagates the underlying structured code (or ErrCodeInternal) if health computation fails.

func (*Client) CriteriaFromSnapshot added in v0.21.0

func (c *Client) CriteriaFromSnapshot(snap *Snapshot) (*Criteria, error)

CriteriaFromSnapshot derives recipe criteria from a snapshot's measurements.

This is the snapshot-to-recipe entry point. Without it the workflow could not be completed through pkg/client/v1 alone: the CLI reached into pkg/fingerprint and pkg/recipe to do this, and the integrator guide had to document that escape hatch, which contradicts this package's promise to be the whole integration surface (#2437, #2016).

Criteria are parsed against this Client's provider-scoped registry, so an external --data catalog that registers its own accelerator or service values resolves them the same way the Client will when it resolves the recipe. A package-level helper could not do that.

The returned criteria are a starting point, not a verdict: callers layer config and flag overrides on top before resolving. Every dimension the snapshot could not determine is left as the "any" wildcard rather than guessed.

Returns ErrCodeInvalidRequest for a nil snapshot, since a caller asking for criteria from nothing has a bug rather than an empty result.

Example

ExampleClient_CriteriaFromSnapshot derives recipe criteria from a captured snapshot without reaching past the facade.

This step previously required pkg/fingerprint, so the documented workflow could not be completed with pkg/client/v1 alone.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	snap, err := client.LoadSnapshot(ctx, "snapshot.yaml", "")
	if err != nil {
		log.Print(err)
		return
	}

	// Every dimension the snapshot could not determine stays "any"; nothing is
	// guessed. Layer your own stated values on top before resolving.
	criteria, err := client.CriteriaFromSnapshot(snap)
	if err != nil {
		log.Print(err)
		return
	}
	criteria.Intent = "training"

	result, err := client.ResolveRecipeFromCriteria(ctx, criteria)
	if err != nil {
		log.Print(err)
		return
	}
	fmt.Printf("resolved %d components\n", len(result.Components))
}

func (*Client) CriteriaRegistry

func (c *Client) CriteriaRegistry() *CriteriaRegistry

CriteriaRegistry returns the per-DataProvider criteria registry for THIS Client. CLI/library callers use it to parse criteria values (so --data overlay contributions validate) and to apply strict mode against the same provider the Client resolves with. Call LoadCatalog first so the registry is seeded from the provider's overlays before parsing.

Returns the registry for this Client's provider via recipe.GetCriteriaRegistryFor. On a nil or closed Client this returns a fresh ephemeral registry so callers can defensively call without nil-checking, matching the existing lenient accessor behavior.

func (*Client) DiffSnapshots added in v0.20.0

func (c *Client) DiffSnapshots(
	ctx context.Context,
	baseline, target *Snapshot,
	opts SnapshotDiffOptions,
) (*SnapshotDiff, error)

DiffSnapshots compares two facade snapshots in memory.

Both snapshots must carry a usable measurement payload from LoadSnapshot, CollectSnapshot, or WrapSnapshot. A hand-constructed Snapshot has no such payload; a wrapped payload containing no typed measurement is equally unusable. Both are rejected rather than being reported as a false no-drift result. Source labels in opts are copied to the result for JSON, YAML, and table consumers; they do not influence comparison semantics.

The operation performs no cluster, filesystem, or recipe-catalog I/O and adds no facade timeout. The caller's context governs unchanged.

Example

ExampleClient_DiffSnapshots compares two previously captured snapshots in memory. Loading local files needs no cluster access; cm:// sources use the kubeconfig argument passed to LoadSnapshot.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	baseline, err := client.LoadSnapshot(ctx, "before.yaml", "")
	if err != nil {
		log.Print(err)
		return
	}
	target, err := client.LoadSnapshot(ctx, "after.yaml", "")
	if err != nil {
		log.Print(err)
		return
	}

	result, err := client.DiffSnapshots(ctx, baseline, target, aicr.SnapshotDiffOptions{
		BaselineSource: "before.yaml",
		TargetSource:   "after.yaml",
	})
	if err != nil {
		log.Print(err)
		return
	}
	if result.HasDrift() {
		fmt.Printf("detected %d change(s)\n", result.Summary.Total)
	}
}

func (*Client) EmitRecipeEvidence added in v0.16.0

func (c *Client) EmitRecipeEvidence(
	ctx context.Context,
	rec *RecipeResult,
	snap *Snapshot,
	results []*PhaseResult,
	opts EvidenceOptions,
) error

EmitRecipeEvidence builds (and optionally pushes) a recipe-evidence attestation bundle from a completed validation run — predicateType v1 for unprofiled recipes, v2 when the recipe carries a configuration profile (metadata.selectedProfile). It is the facade counterpart to the logic the CLI previously assembled inline: it converts the facade PhaseResults back to the internal shape, loads the validator catalog against THIS Client's data source and version, and delegates to the evidence attestation package.

Interactive keyless-signing disclosure is intentionally NOT performed here — that is a UI concern the caller handles (the CLI prompts before calling). This method does no prompting and can run unattended from a server/library.

func (*Client) ListCatalog added in v0.15.0

func (c *Client) ListCatalog(ctx context.Context, filter *Criteria) ([]CatalogEntry, error)

ListCatalog returns catalog entries for all overlays known to this Client, optionally narrowed by the filter criteria. Call LoadCatalog first so the catalog is fully populated before calling this.

Each entry carries the overlay name, its criteria, whether it is a leaf (IsLeaf=true means no other overlay inherits from it), its data provenance ("embedded" or "external"), and its effective configuration profile — the declaration reachable from that overlay after inheritance and co-match resolution, nil when the overlay reaches none. Entries are returned in ascending name order for deterministic output.

When filter is non-nil, only overlays whose criteria carry the exact values specified in each non-empty/non-"any" filter dimension are returned. Setting a filter dimension to "" or "any" places no constraint on that dimension.

Returns ErrCodeInvalidRequest on a nil or closed Client, or when an overlay reachable from the filtered set declares an invalid profile — profile declarations are validated during projection, so a malformed declaration fails the whole call rather than yielding a partial catalog. Returns ErrCodeTimeout if ctx is canceled during projection, and propagates ErrCodeInternal if the underlying metadata store cannot be loaded.

func (*Client) LoadCatalog

func (c *Client) LoadCatalog(ctx context.Context) error

LoadCatalog eagerly loads (and caches) this Client's metadata store, which has the side effect of seeding THIS Client's per-provider criteria registry from every overlay's spec.criteria. Call it before parsing criteria through CriteriaRegistry so values contributed by a FilesystemSource --data overlay are admitted by the registry's lookups.

This mirrors the pre-facade eager recipe.LoadCatalog the CLI ran after SetDataProvider, but seeds the Client's OWN provider registry rather than the process-global one — so two Clients built from different sources keep isolated criteria registries.

Errors propagate with their structured codes preserved (a malformed overlay surfaces as ErrCodeInvalidRequest, not masked as ErrCodeInternal) via PropagateOrWrap.

The same guards as the resolve methods apply: nil receiver and nil context are rejected with ErrCodeInvalidRequest, and a closed Client is rejected.

func (*Client) LoadRecipe

func (c *Client) LoadRecipe(ctx context.Context, path, kubeconfig string) (*RecipeResult, error)

LoadRecipe loads a recipe from a file path (or cm:// ConfigMap URI, honoring kubeconfig) through THIS Client's data provider, and returns it as a Client-owned *RecipeResult ready for ValidateState / BundleComponents. Overlay inputs (kind: RecipeMetadata) are hydrated against the Client's provider, so an external --data overlay resolves against the same recipe source the Client was constructed with rather than the package global. An already-hydrated RecipeResult file is returned with its provider bound to the Client's provider.

The returned RecipeResult is owner-stamped with this Client, so it passes ValidateState / BundleComponents' assertOwns check — same as a RecipeResult produced by ResolveRecipe.

Errors:

  • ErrCodeInvalidRequest when the Client is nil, ctx is nil, path is empty, or the Client has been Closed.
  • All loader errors propagate with their structured codes (e.g., ErrCodeInvalidRequest for an overlay without criteria, ErrCodeInternal for a read or parse failure).
Example

ExampleClient_LoadRecipe reads a recipe emitted earlier by `aicr recipe -o`, instead of resolving a new one. The result is interchangeable with a resolved one: bundle it, or validate it against a snapshot.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	// A local file path, an HTTP(S) URL, or a cm://namespace/name ConfigMap
	// URI. The kubeconfig argument is consulted only for the cm:// form.
	result, err := client.LoadRecipe(ctx, "recipe.yaml", "")
	if err != nil {
		log.Print(err)
		return
	}
	fmt.Println(result.Name)
}

func (*Client) LoadSnapshot added in v0.20.0

func (c *Client) LoadSnapshot(ctx context.Context, path, kubeconfig string) (*Snapshot, error)

LoadSnapshot reads a previously captured snapshot and returns it in the facade shape, ready to hand to ValidateState, ResolveRecipeFromSnapshot, or EmitRecipeEvidence.

It is the counterpart to CollectSnapshot for the case an integrator hits far more often: the snapshot already exists. A pipeline captures cluster state in one stage and resolves or validates against it in another, or the snapshot is committed to a repository and replayed.

path accepts the same forms the CLI does — a local file, an HTTP(S) URL, or a cm://namespace/name ConfigMap URI. kubeconfig resolves the cm:// form; empty uses the standard KUBECONFIG, ~/.kube/config, then in-cluster discovery chain, and is ignored entirely for the other two.

Cluster access is therefore source-dependent: a local file or an HTTP(S) URL needs none, while a cm:// URI reads a ConfigMap through the Kubernetes API and needs working credentials for that cluster. Only CollectSnapshot needs a cluster unconditionally, since it deploys an agent Job.

The loader FAILS CLOSED on a document that is not a snapshot this build can consume: a wrong kind (an AICRConfig, say), or an apiVersion this binary does not understand. That matters because snapshot deserialization is non-strict — any YAML mapping would otherwise decode into a zero-value Snapshot, derive criteria(any), and silently produce a fallback recipe with exit 0. Empty kind and apiVersion are tolerated for snapshots that predate those fields.

Raw is not populated

Snapshot.Raw carries the exact bytes a collection agent emitted and is set only by CollectSnapshot. A loaded snapshot leaves it empty, because the source is already the durable artifact — re-exposing its bytes here would invite callers to round-trip a stored snapshot through the parsed type, which is what Raw exists to discourage.

A caller needing the bytes can read the source again, but note what that does and does not give you: re-reading returns the source's CURRENT contents, which for a URL or a ConfigMap (and for a file someone rewrote) need not be the bytes this call parsed. If byte-for-byte identity with the loaded snapshot matters — hashing what you validated, say — capture the source contents yourself and load from that capture, rather than reading the source a second time afterwards.

This method does not touch the Client's recipe catalog, so any open Client will do; it hangs off Client to keep the surface uniform and to give config-driven loading a home.

func (*Client) MakeBundle

func (c *Client) MakeBundle(ctx context.Context, recipe *RecipeResult, opts BundleOptions) (BundleArtifact, error)

MakeBundle generates the full deployer-mode bundle for a previously resolved or loaded RecipeResult, writing the bundle files under opts.OutputDir and returning a BundleArtifact summary. Unlike BundleComponents (which returns per-component Helm values + manifests in memory), MakeBundle produces the SAME complete artifact the CLI bundle command emits — README, deploy.sh, per-component directories, checksums — in the deployer layout selected by opts.Config.Deployer() (helm, argocd, argocd-helm, flux, helmfile).

When to call

Call AFTER Client.ResolveRecipe or Client.LoadRecipe; pass that call's *RecipeResult unchanged. MakeBundle bundles from recipe.Resolved() (the full pkg/recipe.RecipeResult), which carries this Client's own DataProvider — so provider-scoped lookups (values files, manifest files) resolve against the Client's recipe source rather than the package global.

Allowlist enforcement

When the Client was constructed WithAllowLists, MakeBundle validates the recipe's criteria against the allowlist before bundling — same fencing the resolve path and the REST /v1/bundle handler apply. A recipe whose criteria fall outside the allowlist is rejected with the allowlist's structured error. A recipe with nil Criteria (a loaded, already-hydrated or bare RecipeResult file) skips the check, matching the handler's `recipeResult.Criteria != nil` guard.

Synchronization

Read-locks Client.mu so a concurrent Close can't race the bundle, and registers in the inflight WaitGroup so Close drains before evicting caches — the same protocol as BundleComponents. A facade-level timeout is opt-in via opts.Timeout: when set (> 0) it bounds the run by the smaller of opts.Timeout and any tighter caller deadline; when unset (0) MakeBundle runs under the caller's context with NO added cap. The REST /v1/bundle handler sets opts.Timeout = defaults.BundleHandlerTimeout to keep its 60s request boundary; the CLI bundle command leaves it 0 so large bundles, --vendor-charts, and attestation/signing are uncapped.

Errors:

  • ErrCodeInvalidRequest when the Client, ctx, or recipe is nil, when recipe lacks internal state (constructed outside Resolve/Load), when the recipe was produced by a different Client, or when the Client has been Closed.
  • Allowlist and bundler errors propagate with their structured codes.

func (*Client) MergeReports added in v0.16.0

func (c *Client) MergeReports(results []*PhaseResult) *ctrf.Report

MergeReports merges the per-phase CTRF reports from a ValidateState run into a single combined report, stamped with the tool name "aicr" and this Client's version. Library and server callers use it to produce the same combined CTRF document the CLI writes, without reaching into pkg/validator/ctrf merge internals. Nil results and phases with a nil Report contribute nothing.

func (*Client) MirrorInventory added in v0.21.0

func (c *Client) MirrorInventory(
	ctx context.Context,
	rec *RecipeResult,
	opts ...MirrorInventoryOption,
) (*MirrorInventory, error)

MirrorInventory discovers every container image and Helm chart a recipe references, by rendering its charts and scanning the resulting manifests.

This performs real work: it renders every component's chart. Callers should pass a context with a deadline appropriate to the recipe's size.

Example

ExampleClient_MirrorInventory lists every image and chart a recipe needs, for staging into an air-gapped registry.

Rendering is the caller's job: the facade returns data, and formats such as Hauler or Zarf stay in the CLI so the SDK does not freeze third-party schemas as contract.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	result, err := client.ResolveRecipeFromCriteria(ctx, &aicr.Criteria{
		Service:     "eks",
		Accelerator: "h100",
		Intent:      "training",
	})
	if err != nil {
		log.Print(err)
		return
	}

	// Pass the same overrides you will bundle with. Disabling a sub-component
	// removes its images, so mirroring without them stages the wrong set.
	disabled := "false"
	inventory, err := client.MirrorInventory(ctx, result,
		aicr.WithMirrorValueOverrides([]aicr.MirrorValueOverride{
			{Component: "gpuoperator", Path: "driver.enabled", Value: &disabled},
		}))
	if err != nil {
		log.Print(err)
		return
	}

	for _, image := range inventory.Images {
		fmt.Println(image)
	}
	for _, chart := range inventory.Charts {
		fmt.Printf("%s %s from %s\n", chart.Chart, chart.Version, chart.Repository)
	}
}

func (*Client) PublishEvidence added in v0.20.0

func (c *Client) PublishEvidence(ctx context.Context, opts EvidencePublishOptions) error

PublishEvidence signs and pushes an already-emitted on-disk evidence bundle, then writes pointer.yaml beside it.

It is the off-network second leg of the workflow whose first leg is an evidence-emitting validation run that did not push: that step produces the unsigned on-disk bundle this one consumes. Splitting them lets the cluster-bound step run where the cluster is reachable and the Sigstore-bound step run where Fulcio and Rekor are. The result is content-identical to the one-shot path, because the predicate signed here is read verbatim from the bundle's statement.intoto.json.

Interactive keyless-signing disclosure is intentionally NOT performed here, matching EmitRecipeEvidence: prompting is a UI concern the caller owns, and this method must be able to run unattended from a server or library.

Example

ExampleClient_PublishEvidence signs a recipe-evidence bundle and pushes it to an OCI registry, the producing half of ExampleClient_VerifyEvidence.

package main

import (
	"context"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	if err = client.PublishEvidence(ctx, aicr.EvidencePublishOptions{
		BundleDir: "./evidence",
		Push:      "ghcr.io/example/aicr-evidence:v1",
	}); err != nil {
		log.Print(err)
		return
	}
}

func (*Client) RecipeDigest added in v0.20.0

func (c *Client) RecipeDigest(ctx context.Context, opts RecipeDigestOptions) (string, error)

RecipeDigest returns the canonical digest of a resolved recipe — the lowercase hex SHA-256 of its canonical YAML, byte-for-byte the value stored in predicate.recipe.digest by an evidence-emitting validation run.

It is the producer-side companion to VerifyEvidence: a CI gate verifies a committed evidence pointer, reads predicate.recipe.digest out of the verified bundle, and compares it against RecipeDigest of the recipe on the branch to detect evidence that has gone stale.

Hydration resolves against THIS Client's DataProvider, so the digest reflects the same catalog the Client would resolve and validate with.

Example

ExampleClient_RecipeDigest computes the canonical digest an evidence predicate records. A CI gate compares this against the digest inside a published evidence bundle to detect evidence that has gone stale relative to the recipe it claims to describe.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	digest, err := client.RecipeDigest(ctx, aicr.RecipeDigestOptions{
		Path: "recipe.yaml",
	})
	if err != nil {
		log.Print(err)
		return
	}
	fmt.Println(digest)
}

func (*Client) ResolveRecipe

func (c *Client) ResolveRecipe(ctx context.Context, req RecipeRequest) (*RecipeResult, error)

ResolveRecipe maps a RecipeRequest to a concrete validated recipe. It wraps pkg/recipe.Builder.BuildFromCriteria with a stable external request shape so AICR's internal Criteria type can evolve without breaking consumers.

Pinned recipe references (req.PinnedName / req.PinnedVersion) are not yet supported by the facade and return ErrCodeUnavailable. The field is reserved so callers can adopt it without API churn when the underlying builder gains pinning support.

func (*Client) ResolveRecipeFromCriteria

func (c *Client) ResolveRecipeFromCriteria(ctx context.Context, criteria *Criteria) (*RecipeResult, error)

ResolveRecipeFromCriteria resolves a facade Criteria into a facade RecipeResult. The Components projection mirrors ResolveRecipe; callers needing the full upstream recipe (constraints, deployment order, metadata) access it via the returned result's Resolved() helper.

Use this when the caller already speaks the facade Criteria type (e.g., a REST handler that parsed criteria from an HTTP request and translated via WrapCriteria) rather than the RecipeRequest shape ResolveRecipe takes.

Allowlist enforcement (WithAllowLists) applies here just as it does on the shared resolve path: criteria outside the configured allowlist are rejected before the recipe is built.

The same guards and synchronization as ResolveRecipe apply: nil receiver, nil context, and nil criteria are rejected with ErrCodeInvalidRequest; a closed Client is rejected; a facade-level timeout bounds the resolve.

func (*Client) ResolveRecipeFromCriteriaWithOptions added in v0.19.0

func (c *Client) ResolveRecipeFromCriteriaWithOptions(
	ctx context.Context,
	criteria *Criteria,
	opts ...RecipeResolveOption,
) (*RecipeResult, error)

ResolveRecipeFromCriteriaWithOptions is ResolveRecipeFromCriteria with optional per-resolution behavior such as profile selection and Slurm accounting ownership.

func (*Client) ResolveRecipeFromCriteriaWithProfile added in v0.19.0

func (c *Client) ResolveRecipeFromCriteriaWithProfile(
	ctx context.Context,
	criteria *Criteria,
	profile string,
) (*RecipeResult, error)

ResolveRecipeFromCriteriaWithProfile resolves criteria with an optional name=value profile selection.

func (*Client) ResolveRecipeFromSnapshot

func (c *Client) ResolveRecipeFromSnapshot(ctx context.Context, criteria *Criteria, snap *Snapshot) (*RecipeResult, error)

ResolveRecipeFromSnapshot resolves a recipe from explicit Criteria and evaluates its constraints against an observed cluster Snapshot, mirroring `aicr recipe --snapshot`. It returns the facade RecipeResult; callers needing the upstream recipe (ComponentRefs, deployment order, per- constraint evaluation results) access it via Resolved().

Unlike ResolveRecipeFromCriteria — which builds the recipe without observing the cluster — this variant threads a constraint evaluator that runs each resolution constraint against snap via pkg/constraints.Evaluate. The CLI's `recipe --snapshot` path does the same: it derives criteria from the snapshot fingerprint, then calls BuildFromCriteriaWithEvaluator so the resolved recipe records whether each constraint passed against the observed state.

Allowlist enforcement (WithAllowLists) applies here just as it does on the shared resolve path: criteria outside the configured allowlist are rejected before the recipe is built.

The criteria-coverage post-condition (issue #1542) is STRICT by default here: every stated criteria dimension must be honored by an applied overlay or resolution fails with ErrCodeInvalidRequest carrying details.uncovered.

To reproduce `aicr recipe --snapshot`, which additionally relaxes dimensions derived from the snapshot fingerprint and retries once, pass WithSnapshotCriteriaRelaxation and name the dimensions you received explicitly:

result, err := client.ResolveRecipeFromSnapshotWithOptions(ctx, criteria, snap,
    aicr.WithSnapshotCriteriaRelaxation(aicr.DimensionIntent))

Only the caller knows which dimensions a user stated versus which it derived, so the facade cannot infer that — but it does accept it as a parameter and applies the policy itself. Dimensions actually cleared are reported in RecipeResult.RelaxedDimensions.

The same guards and synchronization as ResolveRecipeFromCriteria apply: nil receiver, nil context, nil criteria, and nil snapshot are rejected with ErrCodeInvalidRequest; a closed Client is rejected; a facade-level timeout bounds the resolve. Builder errors propagate as-is (they already carry the appropriate pkg/errors code) rather than being re-wrapped.

func (*Client) ResolveRecipeFromSnapshotWithOptions added in v0.19.0

func (c *Client) ResolveRecipeFromSnapshotWithOptions(
	ctx context.Context,
	criteria *Criteria,
	snap *Snapshot,
	opts ...RecipeResolveOption,
) (*RecipeResult, error)

ResolveRecipeFromSnapshotWithOptions is ResolveRecipeFromSnapshot with optional per-resolution behavior such as profile selection and Slurm accounting ownership.

func (*Client) ResolveRecipeFromSnapshotWithProfile added in v0.19.0

func (c *Client) ResolveRecipeFromSnapshotWithProfile(
	ctx context.Context,
	criteria *Criteria,
	snap *Snapshot,
	profile string,
) (*RecipeResult, error)

ResolveRecipeFromSnapshotWithProfile is the snapshot-filtered profile resolution path.

func (*Client) SignCatalog added in v0.20.0

func (c *Client) SignCatalog(ctx context.Context, opts CatalogSignOptions) (*CatalogSignResult, error)

SignCatalog computes the deterministic digest over this Client's recipe catalog (registry.yaml plus validators/catalog.yaml), signs it via Sigstore keyless OIDC, and optionally writes the resulting bundle to opts.Output. VerifyCatalog is its counterpart.

As with VerifyCatalog, the digest is computed over THIS Client's DataProvider, so what gets signed is the catalog the Client resolves with.

Signing modes are constrained to what VerifyCatalog can verify

VerifyCatalog verifies against the public-good Sigstore root, requires a transparency-log entry, and pins the certificate to the GitHub Actions OIDC issuer. It exposes no key, no trust-root, and no offline option, because the recipe catalog is a release artifact NVIDIA signs — not something a consumer re-signs privately.

SignCatalog therefore REJECTS the four OIDCResolve settings that would produce a signature its own counterpart could not check:

  • SigningKey — a key-signed catalog has no verification path at all.
  • FulcioURL — a certificate from a private CA does not chain to the public-good root.
  • RekorURL — an entry in a private transparency log cannot be verified against the public-good root either. The flag can name a public-good v1 URL as well, which would verify, but the two are indistinguishable from the URL alone, so this fails closed.
  • DisableTLogUpload — verification requires a transparency-log entry.

Each is rejected with ErrCodeInvalidRequest before any signing work runs, so the failure is immediate and explains itself rather than surfacing later as an unverifiable artifact.

SigningConfigPath is checked rather than rejected. It passes through because the release path requires it — naming the public-good Rekor v2 target is its normal use — but a signing config can itself name a private Fulcio or Rekor, which would otherwise make the four rejections above bypassable by moving the same endpoints into a file. Every Fulcio, Rekor, OIDC-provider, and timestamp-authority URL in it must therefore be HTTPS under the sigstore.dev domain, matched on a label boundary so a lookalike host is rejected.

The config that passes that check is the config signed with: the parsed value is handed to the signing path rather than re-read from the path, so the file cannot change between the check and the use.

If private catalog signing is ever needed, both halves have to move together — widening this without widening VerifyCatalog is what this guard exists to prevent.

A signature that yields no bundle is treated as a failure rather than a silent success: it means the attester could not obtain an OIDC token.

Example

ExampleClient_SignCatalog signs a recipe catalog with keyless Sigstore.

This is a release-CI flow, not a local one

VerifyCatalog pins the certificate identity to this repository's tag-release workflow. A catalog signed anywhere else verifies against nothing, so this is only useful from that workflow, with ambient credentials supplied.

Leaving OIDCResolve zero-valued is the trap: SelectOIDCSource then falls through to the interactive BROWSER flow, which blocks on a human and mints a certificate issued by oauth2.sigstore.dev. SignCatalog succeeds and emits a bundle VerifyCatalog rejects — it fails the issuer pin before identity matching is even reached.

SignCatalog does reject settings that break verifiability — a signing key, a private Fulcio or Rekor, or a disabled transparency-log upload — but it does NOT police the identity SOURCE, which is the asymmetry an SDK caller is most likely to hit.

Signing is also deliberately not bounded by a facade timeout, because keyless OIDC can block on a human.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.FilesystemSource("/etc/aicr/recipes")))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	signed, err := client.SignCatalog(ctx, aicr.CatalogSignOptions{
		Output: "recipe-catalog.sigstore.json",
		OIDCResolve: aicr.OIDCResolveOptions{
			// Ambient workload credentials. Both must be set, or resolution
			// falls through to the browser flow described above.
			AmbientURL:   os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL"),
			AmbientToken: os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"),
		},
	})
	if err != nil {
		log.Print(err)
		return
	}
	fmt.Printf("signed catalog digest %s (%d bytes)\n", signed.Digest, len(signed.BundleJSON))
}

func (*Client) ValidateState

func (c *Client) ValidateState(
	ctx context.Context,
	recipe *RecipeResult,
	snap *Snapshot,
	opts ...ValidateOption,
) ([]*PhaseResult, error)

ValidateState evaluates a resolved recipe against an observed cluster snapshot, runs the selected validation phases (by default PhaseDeployment, PhaseConformance, PhasePerformance) in order, and returns one PhaseResult per phase run. Pass WithValidationPhases to restrict the run to a subset.

recipe must come from a prior Client.ResolveRecipe call on this Client — it carries the unexported internal recipe state needed to drive constraint evaluation. Passing a RecipeResult constructed by the caller (or one produced by a different Client whose internal has since been evicted) returns ErrCodeInvalidRequest.

snap is the Snapshot returned by Client.CollectSnapshot or by any other snapshotter source.

opts configure the validator run. Pass WithValidationNoCluster(true) from unit tests so no Kubernetes resources are created and every check reports as "skipped". WithValidationNamespace, WithValidationRunID, WithValidationCleanup, WithValidationTolerations, WithValidationNodeSelector, WithValidationKubeconfig, and WithValidationPhases cover the production-controller knobs. The validator catalog loads through this Client's own DataProvider, so a Client built from FilesystemSource validates against that recipe source rather than the package global.

Errors:

  • ErrCodeInvalidRequest when the Client, recipe, or snap is nil, when recipe lacks internal state, or when the Client has been Closed.
  • All validator errors propagate unwrapped — readiness-check failures surface as ErrCodeInvalidRequest, infrastructure failures as ErrCodeInternal.

All phases run by default and produce results regardless of earlier failures. Pass WithValidationFailFast(true) to stop after the first failed phase (useful for skipping expensive checks like inference-perf when deployment already failed). Callers wanting per-phase control can reach into pkg/validator.ValidatePhase directly.

Example

ExampleClient_ValidateState evaluates a resolved recipe against observed cluster state.

Validation comprises three phases, executed in order: deployment, conformance, performance. This example narrows to the first two with WithValidationPhases; omitting that option runs all three.

WithValidationNoCluster(true) keeps constraint evaluation but skips everything needing a cluster — the mode CI uses to check a recipe against a captured snapshot without provisioning hardware.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	recipe, err := client.LoadRecipe(ctx, "recipe.yaml", "")
	if err != nil {
		log.Print(err)
		return
	}
	snap, err := client.LoadSnapshot(ctx, "snapshot.yaml", "")
	if err != nil {
		log.Print(err)
		return
	}

	phases, err := client.ValidateState(ctx, recipe, snap,
		aicr.WithValidationNoCluster(true),
		aicr.WithValidationPhases(aicr.PhaseDeployment, aicr.PhaseConformance),
	)
	if err != nil {
		log.Print(err)
		return
	}
	for _, p := range phases {
		fmt.Printf("%s: %d passed, %d failed\n", p.Phase, p.Summary.Passed, p.Summary.Failed)
	}
}

func (*Client) VerifyBundle added in v0.20.0

func (c *Client) VerifyBundle(ctx context.Context, bundleDir string, opts BundleVerifyOptions) (*BundleVerification, error)

VerifyBundle verifies a deployment bundle's checksums and attestation chain, then evaluates the policy assertions carried in opts.

Verification is offline: the checksum and attestation chain resolve against the locally cached or embedded Sigstore trusted root. The one network path is a KMS URI in opts.Key, which makes a live GetPublicKey call to resolve the key.

A non-nil error means verification could not be performed — bad options, a missing or unreadable bundle directory, a malformed trust root. A bundle that verified but failed is reported through the returned value: Report.Errors for verification failures and PolicyFailure for policy ones.

This method does not touch the Client's recipe catalog. It hangs off Client so that a single configured Client is the one object a consumer needs, and so config-driven verification has a home. Any open Client will do — a hot-path caller can construct one against EmbeddedSource and reuse it, rather than building one per verification.

func (*Client) VerifyCatalog added in v0.20.0

func (c *Client) VerifyCatalog(ctx context.Context, bundlePath string, opts CatalogVerifyOptions) (*CatalogVerification, error)

VerifyCatalog recomputes the deterministic digest over this Client's recipe catalog (registry.yaml plus validators/catalog.yaml) and verifies it against the Sigstore bundle at bundlePath, pinning to NVIDIA CI identity. The bundle ships as the recipe-catalog.sigstore.json release asset alongside each tagged aicr binary.

The digest is computed over THIS Client's DataProvider, not the process-wide embedded catalog — the same binding ComputeHealth uses. A Client built on an EmbeddedSource verifies the catalog NVIDIA signed. A Client whose source layers external data over the embedded tree is verifying different content, so verification will not match the released signature; that is the correct answer to "is the catalog I am resolving against the signed one", not a bug.

A verification failure is returned as an error, not as a report: unlike bundle and evidence verification there is no partial verdict to render.

Example

ExampleClient_VerifyCatalog checks the Sigstore signature over this Client's recipe catalog — that the recipe data resolution is about to use was published by NVIDIA CI and has not been altered. The bundle ships as the recipe-catalog.sigstore.json release asset.

The digest is computed over THIS Client's DataProvider. A Client layering external data over the embedded tree is verifying different content, so it will not match the released signature — that is the correct answer to "is the catalog I am resolving against the signed one", not a failure to work around.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.FilesystemSource("/etc/aicr/recipes")))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	verification, err := client.VerifyCatalog(ctx, "recipe-catalog.sigstore.json", aicr.CatalogVerifyOptions{})
	if err != nil {
		log.Print(err)
		return
	}
	fmt.Printf("signed by %s over %s\n", verification.Identity, verification.Digest)
}

func (*Client) VerifyEvidence added in v0.20.0

func (c *Client) VerifyEvidence(ctx context.Context, opts EvidenceVerifyOptions) (*EvidenceVerification, error)

VerifyEvidence verifies a recipe-evidence bundle's signature (when present) and manifest hash chain, and surfaces the predicate it recovered.

A non-nil error means verification could not be attempted (bad options, closed Client). Everything else — including "this bundle is invalid" — comes back as a verdict on the returned value; read EvidenceVerification.Exit and compare against the EvidenceExit* constants.

Unlike bundle verification this can reach the network: a pointer or OCI input pulls the artifact from its registry.

That makes one interaction worth knowing. By default the call is capped by defaults.VerifyOperationTimeout, so a slow registry can trip it even when the caller's own context allowed longer. A cap breach returns an ERROR, not EvidenceExitIncomplete — so a gate that distinguishes "could not check this" from "checked it and it failed" must treat a context-deadline error as the former, alongside the Incomplete verdict.

Set EvidenceVerifyOptions.Timeout to a pointer to 0 to remove the facade cap and let the caller's context govern, which is the fix for a registry that is simply slow rather than broken. The caller's own deadline still applies; the option can only relax the facade's ceiling, never extend the context it was given.

The Client's recipe catalog is not consulted, so any open Client will do — including one a hot-path caller keeps around and reuses.

Example

ExampleClient_VerifyEvidence checks a recipe-evidence bundle's signature and hash chain. Input accepts a pointer file, a directory, or an OCI reference.

package main

import (
	"context"
	"fmt"
	"log"

	aicr "github.com/NVIDIA/aicr/pkg/client/v1"
)

func main() {
	ctx := context.Background()

	client, err := aicr.NewClient(aicr.WithRecipeSource(aicr.EmbeddedSource()))
	if err != nil {
		log.Print(err)
		return
	}
	defer func() { _ = client.Close() }()

	verification, err := client.VerifyEvidence(ctx, aicr.EvidenceVerifyOptions{
		Input: "evidence.json",
	})
	if err != nil {
		log.Print(err)
		return
	}

	switch verification.Exit {
	case aicr.EvidenceExitValidPassed:
		fmt.Println("valid, all phases passed")
	case aicr.EvidenceExitValidPhaseFailures:
		fmt.Println("valid, but phases failed")
	case aicr.EvidenceExitInvalid:
		fmt.Println("invalid")
	case aicr.EvidenceExitIncomplete:
		fmt.Println("incomplete")
	}

	fmt.Println(aicr.RenderEvidenceMarkdown(verification))
}

type ComponentBundle

type ComponentBundle struct {
	// Component is the matching ComponentRef from the recipe.
	Component ComponentRef

	// HelmValues are YAML-encoded Helm values, or nil for
	// non-Helm components.
	HelmValues []byte

	// Manifests are rendered manifest bytes. Non-nil for
	// Kustomize components, and also non-nil for Helm components
	// whose recipe attaches supplemental manifestFiles. nil when
	// the component has no manifest files of its own.
	Manifests []byte
}

ComponentBundle is the resolved deployable artifact for one recipe component. The slice returned by Client.BundleComponents mirrors RecipeResult.Components 1:1 — same order, same length — so callers can correlate by index when threading bundles back through their own state.

Component identity (Name, Kind, Version) duplicates the matching RecipeRef so callers passing bundles around without the original RecipeResult retain enough context to dispatch on kind.

HelmValues vs Manifests population — read carefully, the rule is per-Kind, not "exactly one":

  • Helm components: HelmValues holds YAML-encoded values that downstream consumers pass to `helm install --values`. Manifests MAY ALSO be non-nil when the recipe attaches supplemental manifest files to the Helm component (e.g., gpu-operator's overlay attaches a dcgm-exporter manifest; h100-gke-cos-training attaches gke-nccl-tcpxo manifests). Downstream consumers should apply Manifests alongside the Helm release. Skipping Manifests on a Helm component will silently drop those resources.
  • Kustomize / raw-manifest components: Manifests holds the rendered manifest bytes. HelmValues is nil.
  • Components with neither (rare — a recipe component with no valuesFile, no overrides, and no manifestFiles): both fields are nil; the component is still listed for ordering / status purposes.

type ComponentRef

type ComponentRef struct {
	// Name is the component identifier, e.g. "gpu-operator".
	Name string

	// Kind is the deployment kind, e.g. "Helm" or "Kustomize".
	Kind string

	// Version is the component chart/manifest version.
	Version string

	// Source is the upstream artifact location: a Helm chart
	// repository URL for Helm components (e.g.
	// "https://helm.ngc.nvidia.com/nvidia"), or a Kustomize source
	// repo for Kustomize components. Empty when the recipe
	// registry leaves it unset.
	Source string

	// Chart is the Helm chart name as it appears in the upstream
	// repository (e.g. "gpu-operator"). Empty for non-Helm
	// components. Defaults to Name when the registry leaves it
	// unset.
	Chart string

	// Namespace is the install namespace recommended by the recipe
	// (e.g. "gpu-operator"). Consumers SHOULD honor it so the
	// deployed layout matches what AICR validation expects to find.
	// Empty when the recipe leaves it unset.
	Namespace string
}

ComponentRef identifies a deployable recipe component.

The Name/Chart distinction matters: Name is AICR's identifier (e.g. "nfd"), while Chart is the Helm chart name (e.g. "node-feature-discovery"). Most components have Name == Chart, but the registry's helm.defaultChart override allows them to differ. Consumers building Helm Releases must use Chart, not Name, as spec.forProvider.chart.name.

type Config added in v0.20.0

type Config struct {
	// contains filtered or unexported fields
}

Config is a parsed AICRConfig document — the version-controlled file a team commits so their snapshot / recipe / bundle / validate / verify settings live beside the code they configure, rather than being retyped on each invocation.

Deriving options, not applying them

A Config does not attach to a Client and is never consulted implicitly. Instead each method below DERIVES a populated options value, which the caller may then override:

cfg, err := aicr.LoadConfig(ctx, "aicr-config.yaml")
opts, err := cfg.BundleVerifyOptions()
opts.MinTrustLevel = "verified"   // caller wins, visibly
v, err := client.VerifyBundle(ctx, dir, opts)

That shape is deliberate. The facade's options are plain structs, so a field left at its zero value is indistinguishable from one a caller set to the zero value on purpose — there is no equivalent of the CLI's cmd.IsSet. An implicit merge would therefore have to guess, and would silently hand back the config's value to a caller who deliberately cleared a setting. Deriving makes precedence one readable line at the call site instead of a merge rule the caller has to remember.

It also matches what the CLI does: build options from config, then let an explicitly-set flag win. The flag half necessarily stays in pkg/cli, which is the only layer that knows a flag was set.

Nil safety

Every method tolerates a nil Config and nil spec sections, returning zero values rather than erroring. A caller that did not supply a config can derive unconditionally and get "nothing configured", which is what the CLI does when --config is absent.

func LoadConfig added in v0.20.0

func LoadConfig(ctx context.Context, source string) (*Config, error)

LoadConfig reads and validates an AICRConfig from a file path or an HTTP(S) URL.

Errors keep the loader's structured codes — ErrCodeNotFound for a missing file, ErrCodeInvalidRequest for malformed input or a strict-decode rejection, ErrCodeUnavailable for an HTTP failure — rather than being flattened.

Criteria values are validated later, not here

Loading checks structure, not criteria MEMBERSHIP. Whether "eks" or some value your own catalog defines is legal depends on the CriteriaRegistry, which is per-DataProvider — and the provider named by spec.recipe.data does not exist yet at load time. Validating here could only check the embedded catalog, which would reject every externally-contributed value and make a config-driven external catalog unusable.

So membership is checked at RecipeCriteria, where a registry is in hand:

cfg, err := aicr.LoadConfig(ctx, path)          // structure
source, _ := cfg.RecipeSource()                 // spec.recipe.data
client, err := aicr.NewClient(aicr.WithRecipeSource(source))
err = client.LoadCatalog(ctx)                   // seeds the registry
criteria, err := cfg.RecipeCriteria(client.CriteriaRegistry())  // membership

A value in no catalog still fails — at that last step rather than the first.

func WrapConfig added in v0.20.0

func WrapConfig(c *appconfig.AICRConfig) *Config

WrapConfig lifts an AICRConfig parsed elsewhere into the facade type, for callers that already hold one from pkg/config. Returns nil for nil input, mirroring WrapSnapshot.

func (*Config) BundleInputOptions added in v0.21.0

func (c *Config) BundleInputOptions() (BundleInputOptions, error)

BundleInputOptions derives the caller-side spec.bundle settings: which recipe to bundle, which image-refs file to write to, where to push the finished bundle, and how to reach that registry. None of these reach MakeBundle — BundleOptions carries what the bundler itself reads.

Returns the zero value for a nil Config or an absent spec.bundle, and an error when the section is present but malformed.

func (*Config) BundleOptions added in v0.21.0

func (c *Config) BundleOptions() (BundleOptions, error)

BundleOptions derives Client.MakeBundle options from spec.bundle, as plain fields rather than a built *BundleConfig — so a caller (the CLI in particular) can read and override individual settings with its own flag precedence instead of reaching into an opaque built config.

Eighteen flat fields project the bundler settings the section configures — deployment (deployer, repo, value overrides, dynamic values, vendoring, app name), scheduling (system/accelerated selectors and tolerations, DRA eviction label, workload gate and selector, node count, storage classes), and the two attestation flags the bundler itself reads (attest, certificate identity regexp). OIDCResolve carries what reaches the attester rather than the bundler: the Attest gate, DeviceFlow, FulcioURL, RekorURL, SigningKey, and the derived UseTUFSigningConfig.

What is deliberately NOT projected here

RecipeInput, OutputTarget, OutputTargetRaw, ImageRefs, InsecureTLS and PlainHTTP are CALLER-side settings — which recipe to bundle, where to push the result, and how to reach that registry — not bundler settings, so they have no home on BundleOptions. BundleInputOptions carries them.

Zero values

PromptWriter is also left nil, because config cannot carry an io.Writer. A nil writer is treated as io.Discard, so a derived DeviceFlow discards the verification URL and user code and the lazy attester then blocks until the context deadline on first Attest(). That fails closed — no wrong signature — but the caller must set OIDCResolve.PromptWriter to use device flow at all. Erroring here instead would break derive-don't-apply: a caller may well supply their own Attester and never reach the device flow.

Attester, BinaryAttestation, OutputDir and Timeout are left at their zero values. None has a spec.bundle counterpart, and defaulting them here would hide which layer chose. A caller sets them after deriving, which is the same precedence the CLI applies to an explicitly-set flag.

Returns an error when spec.bundle is present but malformed.

func (*Config) BundleVerifyOptions added in v0.20.0

func (c *Config) BundleVerifyOptions() (BundleVerifyOptions, error)

BundleVerifyOptions derives Client.VerifyBundle options from spec.verify.

The mapping is one-to-one: spec.verify.trust supplies CertificateIdentityRegexp, Key, and TrustRoot, and spec.verify.policy supplies MinTrustLevel, RequireCreator, and CLIVersionConstraint. That alignment is not a coincidence — BundleVerifyOptions was shaped to mirror VerifySpec so this stayed a copy rather than a translation table.

IgnoreTLog has no config counterpart and is left false. It weakens the trust floor by dropping the transparency-log requirement, and keeping it command-line-only means a checked-in file can never silently disable that check.

An empty MinTrustLevel is preserved rather than defaulted here, so VerifyBundle applies its own "max" default. Setting it in this layer would hide which of the two chose the floor.

Returns an error when spec.verify is present but malformed.

func (*Config) CNCFEvidenceOptions added in v0.21.0

func (c *Config) CNCFEvidenceOptions() (CNCFEvidenceOptions, error)

CNCFEvidenceOptions derives spec.validate.evidence.cncf.

Returns the zero value (never an error for an absent section) when the document has no spec.validate or no evidence.cncf block, and an error when spec.validate is present but malformed.

func (*Config) EvidenceAttestationOptions added in v0.21.0

func (c *Config) EvidenceAttestationOptions() (EvidenceOptions, bool, error)

EvidenceAttestationOptions derives Client.EmitRecipeEvidence's options from spec.validate.evidence.attestation, and reports whether the document asked for a recipe-evidence bundle at all.

Out is the enable gate, matching the spec field's own contract: an empty out leaves the path off even when bom/push/plainHTTP/insecureTLS are populated, so a half-filled section does not start emitting evidence. False therefore means "not configured", not "misconfigured" — a malformed section is an error instead. That is why this returns a bool rather than a zero-value EvidenceOptions: EmitRecipeEvidence rejects an empty OutDir with ErrCodeInvalidRequest, so a zero value alone could not tell a caller whether the document declined the bundle or fumbled it.

Five fields project (out, bom, push, plainHTTP, insecureTLS). The rest of EvidenceOptions is deliberately caller-owned:

  • Commit has no spec counterpart. It selects the validator catalog the bundle's BOM is built against, and it is a property of the running binary rather than of the document. Set it after deriving.
  • OIDCResolve is excluded by the spec itself: a keyless-signing identity token is a short-lived secret and must not live in a version-controlled file. The caller resolves it at sign time.
  • NoSign and Full are command-line-only, for the same reason FailOnError and IgnoreTLog are. Both weaken the ARTIFACT: NoSign pushes an unsigned bundle, Full ships unredacted payloads. A checked-in file that can quietly turn off signing is a supply-chain downgrade no reviewer would see in a diff, so adding spec fields for them would close a "gap" that is actually a control.

Why plainHTTP and insecureTLS project anyway

They weaken a run too, so the NoSign rule above is not "config may never weaken anything" — stated that broadly it would be contradicted by the two fields three lines into the return below. The line is the ARTIFACT versus the HOP.

PlainHTTP and InsecureTLS configure the transport to a registry the same document already names in push. A document trusted to choose the push destination is trusted to describe how to reach it, which is why EvidenceOptions and SignOptions carry these at all while the bundler's own options do not — MakeBundle never reaches a registry.

Neither field changes what the bundle attests or whether it is signed, and that is structural rather than a promise: both reach only the OCI transport, never SignStatement's Fulcio/Rekor call and never predicate or redaction construction.

How the subject digest is pinned differs by path, and neither path reads it back from the weakened hop. Emit-and-push binds the digest computed locally while packaging, before any push begins. Signing an already-pushed artifact resolves the digest at pull time instead, but the pull is content-addressed and the materialized digest is checked for equality against the value the original packaging run recorded, failing closed on mismatch.

So a tampered hop can corrupt or break the transfer; it cannot make the signature vouch for content that was never packaged.

That is a narrower claim than "harmless". A committed plainHTTP or insecureTLS does weaken that hop, and it widens the threat model rather than just restating it: redirecting push needs a malicious document, whereas downgrading TLS on a destination the operator believes is protected only needs someone on the network path. It is accepted here because the destination is already the document's call. Treat it as a reviewable transport decision, not as evidence that excluding NoSign and Full is arbitrary.

spec.validate.evidence.cncf is projected separately

The evidence section carries two kinds; this method covers one. CNCFEvidenceOptions covers the other — it is a separate method, not folded in here, because the two target different consumers: this one feeds Client.EmitRecipeEvidence, while CNCF AI Conformance markdown has no Client.Emit* counterpart and is consumed directly by the caller (the CLI's validateFlagCombinations, cncf.New and runCNCFSubmission). Reading that half through Unwrap() is no longer necessary.

Returns (zero, false, nil) for a nil Config, an absent spec.validate, or an absent evidence.attestation. An empty out also returns ok=false, but unlike those three absent cases the other four fields (BOMPath, Push, PlainHTTP, InsecureTLS) still populate from the section — only OutDir stays empty. That split matters because out can come from elsewhere: the CLI's --emit-attestation flag can supply out itself while bom/push are configured (buildRecipeEvidenceConfig reads att.BOMPath/att.Push independently of att.OutDir). Zeroing the whole struct whenever out is empty would silently drop that half of the configuration on every run where out arrives some other way. Returns an error when the section is present but malformed.

func (*Config) IsCriteriaStrict added in v0.20.0

func (c *Config) IsCriteriaStrict() bool

IsCriteriaStrict reports spec.recipe.criteriaStrict, which rejects criteria values outside the embedded catalog — hiding registry entries contributed by a --data overlay.

Exposed as a plain read rather than applied inside RecipeCriteria on purpose: strictness is a property of the CriteriaRegistry, which is shared per-DataProvider, so a derivation method that set it would mutate state the caller shares with every other operation on that Client. The caller applies it deliberately, or not at all.

func (*Config) RecipeAccountingMode added in v0.20.0

func (c *Config) RecipeAccountingMode() (string, bool, error)

RecipeAccountingMode returns the Slurm accounting mode from spec.recipe.configuration.slurm.accounting.mode, and reports whether the document set one. Same raw-accessor rationale as RecipeProfile.

Returns an error when the configured value is not a valid accounting mode.

func (*Config) RecipeCriteria added in v0.20.0

func (c *Config) RecipeCriteria(reg *CriteriaRegistry) (*Criteria, error)

RecipeCriteria derives resolve criteria from spec.recipe.criteria, parsed against the supplied registry so a value contributed by a --data overlay validates against the same DataProvider the Client resolves with. Pass Client.CriteriaRegistry(); a nil registry falls back to the embedded catalog.

Returns an empty (non-nil) Criteria when the document states none, so the result is always safe to hand to a resolve call or to overwrite field by field.

func (*Config) RecipeOutputOptions added in v0.21.0

func (c *Config) RecipeOutputOptions() RecipeOutputOptions

RecipeOutputOptions derives spec.recipe.output. Returns the zero value for a nil Config or an absent section — never an error, because the underlying accessors are nil-safe and perform no parsing.

func (*Config) RecipeProfile added in v0.20.0

func (c *Config) RecipeProfile() string

RecipeProfile returns spec.recipe.profile, the configuration-profile selection in name=value form. Empty when unset.

RecipeResolveOptions already folds this into a ready-to-use option; this raw accessor exists for callers that must apply their own precedence first, which is exactly what the CLI does when overlaying an explicitly-set --profile flag. Reach for the options form unless you need the raw value.

func (*Config) RecipeResolveOptions added in v0.20.0

func (c *Config) RecipeResolveOptions() ([]RecipeResolveOption, error)

RecipeResolveOptions derives the resolve options spec.recipe carries: the configuration profile selection (spec.recipe.profile) and the Slurm accounting mode (spec.recipe.configuration.slurm.accounting.mode).

Returns a nil slice when the document sets neither, so it can be appended to a caller's own options unconditionally:

opts, err := cfg.RecipeResolveOptions()
opts = append(opts, aicr.WithProfile(flagProfile))  // caller wins: later option overwrites

func (*Config) RecipeRuntimeInventoryMode added in v0.20.0

func (c *Config) RecipeRuntimeInventoryMode() (string, bool, error)

RecipeRuntimeInventoryMode returns spec.recipe.configuration.runtimeInventory.mode and whether the document set one. Same raw-accessor rationale as RecipeAccountingMode.

Returns an error when the configured value is not a valid mode.

func (*Config) RecipeSource added in v0.20.0

func (c *Config) RecipeSource() (RecipeSourceOption, bool)

RecipeSource derives the Client recipe source from spec.recipe.data, and reports whether the document configured one.

This is the piece that lets a committed config stand up a Client at all: a non-empty data directory yields a FilesystemSource layered over the embedded recipe data, matching `aicr recipe --data`. When false is returned the caller supplies its own source, normally EmbeddedSource.

Deliberately NOT folded into a Client option. Recipe source is fixed at construction — a Client owns its DataProvider for its whole lifetime — so this belongs in the NewClient call rather than in a per-operation options value.

func (*Config) SnapshotAgentConfig added in v0.21.0

func (c *Config) SnapshotAgentConfig() (*AgentConfig, bool, error)

SnapshotAgentConfig derives Client.CollectSnapshot's AgentConfig from spec.snapshot.

These settings map onto the agent Job: namespace, image, image pull secrets, job name, service account, node selector, tolerations, require-GPU, runtime class, OS, max nodes per entry, resource requests and limits, timeout, cleanup and privileged.

OS is parsed through the criteria registry rather than copied, matching what the CLI does with --os. That keeps undocumented values from reaching the agent, and it matters for exact matches: an unparsed "Talos" misses the agent's "talos" check and selects incompatible host mounts.

AgentConfig's fields are exported, so a caller overrides any of them after deriving — the same derive-don't-apply precedence the other methods use, but without needing an options slice, because the type is a plain struct.

Three mappings that are not pass-throughs

NoCleanup is INVERTED against Cleanup, the same shape spec.validate has.

Privileged defaults to TRUE when config says nothing. The resolved field is a pointer precisely so "unset" stays distinct from an explicit false, and the CLI applies derefBoolOr(resolved.Privileged, true). Dereferencing a nil pointer to false here would silently drop privileges the collector needs, and the failure would surface as missing data rather than an error.

Requests and Limits resolve as raw "name=quantity,..." strings — Resolve deliberately does not parse them — so they are parsed here and a malformed value is an error rather than a silently empty ResourceList.

What is deliberately NOT projected

The whole spec.snapshot.output section is un-projected, and that is not an omission. Output describes DELIVERY; AgentConfig describes the collection Job, and the two are different concerns:

  • output.format is applied at delivery. The Job always stages YAML in a ConfigMap, so a format routed through AgentConfig would be silently ignored (#2398).
  • output.path and output.template are not AgentConfig.Output and .TemplatePath. Per AgentConfig.Output's own godoc, any value that is not a cm:// URI stages to an internal ConfigMap and delivery becomes the caller's job. Projecting a file path there would look configured and write nothing.

Callers deliver with snapshotter.DeliverSnapshot, passing Snapshot.Raw.

Kubeconfig, Debug, ClusterConfigPath, AKSGPUPoolsPath, DiscoverNetwork, RunID and NameBase are left at their zero values. None has a spec.snapshot counterpart — they are per-invocation or caller-owned. NameBase in particular carries the "aicr" default prefix that lets an unset job name stay empty while deployed objects keep their released names, which is a decision the caller makes, not the document.

Returns a zero-value AgentConfig (never nil) when the document has no spec.snapshot, and an error when the section is present but malformed.

A zero value is not a working configuration: Privileged is false, which the collector generally needs true. That is deliberate. Defaults apply when the section EXISTS and is silent about a field; a document with no spec.snapshot at all made no snapshot decisions, so the facade does not invent them. A caller in that position supplies its own defaults, as the CLI does from its flag defaults.

The bool

The second return reports whether spec.snapshot is present — true when the section exists (even if silent about every field), false for a nil Config, a nil internal document, or a document that omits the section. It exists for the same reason EvidenceAttestationOptions returns one: a caller deriving unconditionally (before it knows whether --config was even given) otherwise cannot tell "the document made no snapshot decisions, supply your own defaults" from "the document decided every field, apply them as-is" — both produce a populated *AgentConfig, and only one of them is safe to treat as-is. A caller that skips this bool and always applies the returned value silently drops privileges: an absent section returns Privileged: false, which the collector generally needs true.

func (*Config) SnapshotOutputOptions added in v0.21.0

func (c *Config) SnapshotOutputOptions() (SnapshotOutputOptions, error)

SnapshotOutputOptions derives snapshot DELIVERY settings from spec.snapshot.output.

Deliberately separate from SnapshotAgentConfig. That method describes the collection Job and projects nothing from spec.snapshot.output, because output describes delivery and the Job always stages YAML in a ConfigMap — a format routed through AgentConfig would be silently ignored (#2398). These three fields are what a caller needs AFTER CollectSnapshot returns, to write the snapshot where the document asked.

Returns the zero value (never an error for an absent section) when the document has no spec.snapshot or no output block.

func (*Config) SnapshotPath added in v0.20.0

func (c *Config) SnapshotPath() string

SnapshotPath returns spec.recipe.input.snapshot, the snapshot a committed config resolves against. Empty when unset; hand a non-empty value to Client.LoadSnapshot.

func (*Config) Unwrap added in v0.20.0

func (c *Config) Unwrap() *appconfig.AICRConfig

Unwrap returns the underlying AICRConfig, for callers that need a spec field this facade does not project. Returns nil for a nil Config.

Reaching for this is a signal worth acting on: it means the facade is missing a derivation someone needs. Prefer opening an issue over building on the raw document, since pkg/config carries no stability guarantee.

func (*Config) ValidateInputOptions added in v0.21.0

func (c *Config) ValidateInputOptions() (ValidateInputOptions, error)

ValidateInputOptions derives spec.validate.input and spec.validate.execution.failOnError — the three spec.validate fields a caller (not the validator) consumes, so a caller applying its own flag-over-config precedence does not need Unwrap() to read them.

Returns the zero value for a nil Config or an absent spec.validate, and an error when the section is present but malformed.

func (*Config) ValidateSettings added in v0.21.0

func (c *Config) ValidateSettings() (ValidateSettings, bool, error)

ValidateSettings derives settings from both spec.validate.agent and spec.validate.execution, as a plain value rather than an opaque option slice — so a caller (the CLI, in particular) can read and override individual fields with its own flag precedence instead of replaying a built option list.

Thirteen settings project: namespace, image, image pull secrets, job name, service account name, node selector, tolerations, require-GPU, phases, no-cluster, cleanup, fail-fast and timeout. Only nine of them reach Client.ValidateState — image, job name, service account name and require-GPU have no WithValidation* counterpart and are carried purely for the caller's own agent/Job construction. See the ValidateSettings type godoc for which is which.

Where the rest of spec.validate goes

The section is not served by this method alone, which the per-section table in docs/integrator/go-library.md records:

  • FailOnError, RecipePath and SnapshotPath are consumed by the CALLER, not by ValidateState, so a caller still needs them projected to apply its own flag-over-config precedence — just not on this type. ValidateInputOptions carries them.
  • EvidenceAttestation configures the recipe-evidence bundle. EvidenceAttestationOptions derives it; it is not folded in here because it targets Client.EmitRecipeEvidence rather than ValidateState.
  • EvidenceCNCF configures the CNCF AI Conformance markdown path, which has no facade emission method to receive it. See EvidenceAttestationOptions for why that half stays un-projected.

Two mappings that are not pass-throughs

NoCleanup is INVERTED: the config field says "do not clean up", the field says "clean up". Passing it straight through would delete artifacts a post-mortem asked to keep, silently and in either direction.

Phases are cast, not re-parsed, because Validation().Resolve() already rejects an unknown entry and names the spec field — on the WrapConfig path too, since that check lives in Resolve rather than in the loader.

A zero value is not a safe default

Cleanup: false is the opposite of the CLI's own default (clean up). A caller that cannot distinguish "no spec.validate at all" from "spec.validate is present but silent about cleanup" and always applies the returned value as-is will leave the cluster-admin ClusterRoleBinding and validator Jobs active on the plain, no-config invocation — silently, since nothing errors. This is the same hazard SnapshotAgentConfig documents for Privileged; see "# The bool" below for the signal that resolves it.

The bool

The second return reports whether spec.validate is present — true when the section exists (even if silent about every field), false for a nil Config, a nil internal document, or a document that omits the section — mirroring SnapshotAgentConfig's bool exactly, including why it exists: a caller deriving unconditionally (before it knows whether --config was even given) otherwise cannot tell "the document made no validate decisions, supply your own defaults" from "the document decided every field, apply them as-is", and only one of those is safe to treat as-is.

Returns an error when spec.validate is present but malformed.

type Criteria

type Criteria struct {
	Service     string
	Accelerator string
	Intent      string
	OS          string
	Platform    string
	Nodes       int
}

Criteria is the facade-owned, semver-stable shape of a recipe-resolution query. Mirrors pkg/recipe.Criteria field-for-field with the enum-typed pkg/recipe values projected to plain strings so the facade contract does not pin consumers to pkg/recipe's enum identifiers (an internal enum rename or addition stays internal). Construct one directly or wrap an upstream pkg/recipe.Criteria via WrapCriteria.

Field meanings match the pkg/recipe.Criteria documentation:

  • Service: Kubernetes service flavor (eks/gke/aks/oke/kind/lke/bcm).
  • Accelerator: GPU model identifier (h100/h200/b200/gb200/a100/l40/l40s/rtx-pro-6000).
  • Intent: workload intent (training/inference).
  • OS: worker-node OS (ubuntu/rhel/cos/amazonlinux/talos/ol).
  • Platform: framework overlay (dynamo/kubeflow/nim/runai/slurm).
  • Nodes: worker-node count hint (0 = unspecified).

Empty string is the "unspecified" sentinel for every field except Nodes, where 0 plays that role. A non-empty string that the registry does not recognize is rejected at resolve time with ErrCodeInvalidRequest.

func WrapCriteria

func WrapCriteria(c *recipe.Criteria) *Criteria

WrapCriteria projects a pkg/recipe.Criteria into the facade Criteria shape. Use this at the boundary where in-tree callers (CLI/API handlers) hand a parsed criteria — produced by recipe.ParseCriteriaFromRequest or recipe.BuildCriteriaWithRegistry — to facade methods such as Client.ResolveRecipeFromCriteria. Returns nil for nil input.

Round-trip: WrapCriteria(c) then toInternalCriteria projects back to the pkg/recipe.Criteria enum-typed shape; the round-trip is lossless because the facade carries plain strings for the same set of named enum fields (Service/Accelerator/Intent/OS/Platform) plus Nodes.

type CriteriaDimension added in v0.20.0

type CriteriaDimension string

CriteriaDimension names one criteria dimension subject to the criteria-coverage post-condition (issue #1542): the five dimensions an applied overlay can honor, and therefore the five a coverage failure can report as uncovered.

nodes is deliberately absent. No overlay gates on nodes, so it never participates in overlay selection or coverage — see pkg/recipe/coverage.go.

const (
	DimensionService     CriteriaDimension = "service"
	DimensionAccelerator CriteriaDimension = "accelerator"
	DimensionIntent      CriteriaDimension = "intent"
	DimensionOS          CriteriaDimension = "os"
	DimensionPlatform    CriteriaDimension = "platform"
)

The criteria dimensions subject to the coverage post-condition. The string values match pkg/recipe.CoverageDimensionNames exactly; a test asserts that equality, because a mismatch would silently unmark a stated dimension and let WithSnapshotCriteriaRelaxation clear something the caller stated.

func AllCriteriaDimensions added in v0.20.0

func AllCriteriaDimensions() []CriteriaDimension

AllCriteriaDimensions returns every dimension subject to the coverage post-condition, in canonical order. Useful for declaring that every dimension was caller-stated:

aicr.WithSnapshotCriteriaRelaxation(aicr.AllCriteriaDimensions()...)

which enables the policy but permits nothing to be relaxed — equivalent to strict resolution, stated explicitly.

type CriteriaRegistry

type CriteriaRegistry = recipe.CriteriaRegistry

CriteriaRegistry is the per-DataProvider set of valid criteria values, returned by Client.CriteriaRegistry so CLI/library callers parse and validate criteria against the SAME provider the Client resolves with.

Intentionally kept as a transparent alias of pkg/recipe.CriteriaRegistry rather than wrapped into a facade-owned type, for two reasons:

  1. The registry is behavior-rich (ParseService/ParseAccelerator/..., SetStrict, Values, AllAcceleratorTypes, etc.) — wrapping it would require translating every method through, with no semver win because these methods are already used to construct pkg/recipe.Criteria instances in CLI / API call paths.
  2. The registry carries mutable shared state (strict mode, registered values) keyed by per-Client DataProvider identity. A facade wrapper would either copy state (breaking the per-Client identity coupling) or hold a pointer (no isolation win over the alias).

External callers receive the same pkg/recipe.CriteriaRegistry the Client's resolve path uses. If the underlying API evolves, this alias is the single canary; the facade can absorb it by hand-writing a wrapper then.

type EvidenceOptions added in v0.16.0

type EvidenceOptions struct {
	// OutDir is the directory to write the recipe-evidence bundle to
	// (summary-bundle/, optionally logs-bundle/, and pointer.yaml). Required.
	OutDir string

	// BOMPath optionally embeds a CycloneDX BOM; when empty a recipe-bound
	// BOM is synthesized from the recipe's component refs and the validator
	// catalog images that ran.
	BOMPath string

	// Push, when set, is the OCI reference to push the (optionally signed)
	// summary bundle to.
	Push string

	// PlainHTTP / InsecureTLS control the OCI transport for Push (local /
	// self-signed registries).
	PlainHTTP   bool
	InsecureTLS bool

	// NoSign pushes an unsigned bundle and writes a pointer with an empty
	// signer block (requires Push); defers Fulcio/Rekor signing.
	NoSign bool

	// Full disables evidence minimization (ships the raw snapshot and CTRF
	// payloads instead of the redacted defaults).
	Full bool

	// Commit is the build commit used to resolve the validator catalog for
	// the bundle's BOM. The Client's version is used for the catalog version
	// and stamped as AICRVersion; commit has no Client-level home, so it is
	// supplied per call.
	Commit string

	// OIDCResolve carries keyless-signing token-resolution inputs, consumed
	// only when Push is set and NoSign is false.
	OIDCResolve OIDCResolveOptions
}

EvidenceOptions configures Client.EmitRecipeEvidence. It is the facade-owned mirror of the inputs the CLI used to assemble inline, minus the interactive signing-disclosure prompt, which is a UI concern the caller owns.

type EvidencePublishOptions added in v0.20.0

type EvidencePublishOptions struct {
	// BundleDir is the on-disk evidence directory: either the output
	// directory an evidence-emitting validation run wrote (which holds
	// summary-bundle/ and receives pointer.yaml) or the summary-bundle/
	// directory itself. Required.
	BundleDir string

	// Push is the OCI reference the summary bundle is pushed to. Required
	// — a publish with nothing to push is a no-op.
	Push string

	// PlainHTTP forces HTTP for registry traffic (local-registry tests).
	PlainHTTP bool

	// InsecureTLS disables registry TLS verification (self-signed certs).
	InsecureTLS bool

	// NoSign pushes the bundle unsigned and writes a pointer with an empty
	// signer block, deferring the Fulcio/Rekor leg. No OIDC flow runs, so
	// OIDCResolve is ignored.
	NoSign bool

	// OIDCResolve carries keyless-signing token-resolution inputs,
	// consumed only when NoSign is false. Resolution is deferred until
	// adjacent to signing so Fulcio's nonce-binding window is respected.
	OIDCResolve OIDCResolveOptions
}

EvidencePublishOptions configures Client.PublishEvidence.

type EvidenceVerification added in v0.20.0

type EvidenceVerification = evverifier.VerifyResult

EvidenceVerification is the outcome of recipe-evidence bundle verification: the verdict, the recovered predicate and pointer, the signer's claims, and the per-step results.

Deliberate transparent alias of pkg/evidence/verifier.VerifyResult. The result is a deep tree — it reaches into the evidence pointer, the in-toto predicate, the per-step records, and the signer claims — and every field is read-only output. A facade-owned copy would mean owning five more nested types whose shape is still evolving alongside the evidence predicate, for no consumer benefit: a caller reads the verdict and the predicate through `:=` and never names this type.

type EvidenceVerifyOptions added in v0.20.0

type EvidenceVerifyOptions struct {
	// Input selects the bundle, in any of three auto-detected forms: a
	// pointer file path (recipes/evidence/<recipe>/<source>/<digest>.yaml),
	// an OCI reference (with or without an oci:// prefix), or an unpacked
	// bundle directory. Required.
	Input string

	// BundleRef overrides the OCI reference when Input does not carry one
	// — a pointer file whose bundle.oci is empty.
	BundleRef string

	// ExpectedIssuer pins the OIDC issuer URL on the signing certificate.
	// Empty allows any issuer.
	ExpectedIssuer string

	// ExpectedIdentityRegexp pins the signer's SubjectAlternativeName via
	// regex. Empty allows any identity.
	ExpectedIdentityRegexp string

	// PlainHTTP forces HTTP for registry traffic (local-registry tests).
	PlainHTTP bool

	// InsecureTLS disables registry TLS verification (self-signed certs).
	InsecureTLS bool

	// AllowUnpinnedTag opts into accepting an OCI reference that resolves
	// to a tag rather than a digest. Off by default because a tag can be
	// rewritten by the registry, so "verify this artifact at this tag" is
	// not content-addressable.
	AllowUnpinnedTag bool

	// Timeout overrides the facade's operation cap for this call.
	//
	// Nil -- the zero value -- keeps defaults.VerifyOperationTimeout, so a
	// caller who never considered this gets today's behavior. A pointer to 0
	// imposes NO facade cap and runs under the caller's context unchanged. A
	// positive value sets an explicit cap.
	//
	// The pointer exists to make 0 mean "uncapped" rather than "default",
	// matching WithValidationTimeout(0). A plain duration cannot distinguish
	// unset from zero, so it would have had to spell uncapped some other way --
	// and a caller who learned 0-means-uncapped from ValidateState would then
	// get the opposite here, silently capped when they asked for unbounded.
	Timeout *time.Duration
}

EvidenceVerifyOptions configures Client.VerifyEvidence.

type MirrorChart added in v0.21.0

type MirrorChart struct {
	Name       string
	Repository string
	Chart      string
	Version    string
	Namespace  string
}

MirrorChart describes a Helm chart artifact a recipe needs.

type MirrorComponent added in v0.21.0

type MirrorComponent struct {
	Component string

	// Type is "helm" or "kustomize".
	Type string

	Images []string

	// Warnings records non-fatal problems found while discovering this
	// component, such as a chart that rendered with warnings. Discovery
	// succeeded; these are reported rather than raised so one noisy chart
	// does not fail an otherwise usable inventory.
	Warnings []string
}

MirrorComponent groups discovered images by the component referencing them.

type MirrorInventory added in v0.21.0

type MirrorInventory struct {
	// Images is the global sorted, deduplicated set of container images.
	Images []string

	// Charts lists the Helm charts the recipe references.
	Charts []MirrorChart

	// Components breaks the images down by the component that references
	// them, including any non-fatal discovery warnings.
	Components []MirrorComponent

	// RecipeVersion is the CLI version that generated the recipe.
	RecipeVersion string

	// Criteria is a human-readable summary of the recipe criteria.
	Criteria string
}

MirrorInventory is the set of artifacts a recipe references.

Facade-owned rather than an alias: pkg/mirror.MirrorList carries JSON and YAML struct tags that define the shape of the CLI's published output, so aliasing it would freeze that serialization as SDK contract.

type MirrorInventoryOption added in v0.21.0

type MirrorInventoryOption func(*mirrorInventoryOptions)

MirrorInventoryOption configures a mirror inventory request.

func WithMirrorKubeVersion added in v0.21.0

func WithMirrorKubeVersion(version string) MirrorInventoryOption

WithMirrorKubeVersion pins the Kubernetes version charts render against.

Charts branch on .Capabilities.KubeVersion, so an unset version can discover a different image set than the cluster will actually pull. When omitted, the version is derived from the recipe's own constraints.

func WithMirrorValueOverrides added in v0.21.0

func WithMirrorValueOverrides(overrides []MirrorValueOverride) MirrorInventoryOption

WithMirrorValueOverrides applies component value overrides before discovery.

Overrides matter here because they change the answer: disabling a sub-component removes its images from the inventory, so a caller mirroring for an air-gapped install must pass the same overrides they will bundle with or they will mirror the wrong set.

type MirrorValueOverride added in v0.21.0

type MirrorValueOverride struct {
	// Component is the value-override key for the component, matching what
	// `--set <component>:<path>=<value>` accepts.
	Component string

	// Path is the dotted path within that component's values.
	Path string

	// Value is the override. Nil marks the path dynamic (the `--dynamic`
	// form) rather than setting it, which is why this is a pointer: an empty
	// string is a legitimate value distinct from "no value".
	Value *string
}

MirrorValueOverride sets one component value before discovery.

Facade-owned rather than pkg/bundler/config.ComponentPath: exposing that type in a public signature would put an internal package in the frozen SDK surface, so every field it gains or loses becomes an SDK semver event for a package the SDK does not own.

type OIDCResolveOptions added in v0.16.0

type OIDCResolveOptions = bundleattest.ResolveOptions

OIDCResolveOptions configures keyless-signing OIDC token resolution for a pushed evidence bundle. Deliberate transparent alias of pkg/bundler/attestation.ResolveOptions, mirroring how BundleOptions exposes BundleAttester: the caller (CLI/server) builds the resolution inputs and the facade threads them through to attestation.Emit, which resolves the token adjacent to signing. The zero value is valid (no token sources → ambient or interactive flows handled by the caller before invoking the facade).

type Option

type Option func(*Client)

Option configures a Client.

func WithAllowLists

func WithAllowLists(al *AllowLists) Option

WithAllowLists fences which criteria values the Client's resolve path accepts. A resolve whose criteria fall outside the allowlist is rejected before the recipe is built. Pass nil (or omit the option) to allow all values. Construct an AllowLists directly or via ParseAllowListsFromEnv.

func WithOCISourceTempDir added in v0.20.0

func WithOCISourceTempDir(parent string) Option

WithOCISourceTempDir sets the existing writable parent directory beneath which an OCI recipe source creates its unique, private per-Client workspace. Client.Close removes only the child workspace; it never removes parent.

When omitted, the system temporary directory is used. Supplying this option with EmbeddedSource or FilesystemSource is invalid.

func WithRecipeSource

func WithRecipeSource(s RecipeSourceOption) Option

WithRecipeSource sets the recipe source on the Client. Construct the argument with EmbeddedSource, FilesystemSource, or OCISource.

func WithVersion

func WithVersion(version string) Option

WithVersion sets the version string stamped into resolved recipe metadata (RecipeResult.Metadata.Version). Threaded through to the underlying recipe.Builder via recipe.WithVersion. Typically the consuming binary's build version.

type Phase

type Phase string

Phase identifies a single validation phase. Facade-owned so the stable surface does not propagate pkg/validator type-shape changes. Values match pkg/validator/v1 constants verbatim for direct wire compatibility.

const (
	PhaseDeployment  Phase = "deployment"
	PhasePerformance Phase = "performance"
	PhaseConformance Phase = "conformance"
)

Validation phases — string values match pkg/validator/v1 so wire round-trips between facade and validator are byte-identical.

type PhaseResult

type PhaseResult struct {
	Phase     Phase
	Status    string
	Duration  time.Duration
	Summary   ReportSummary
	RawReport []byte
	Report    *ctrf.Report
}

PhaseResult is the outcome of running all validators in a single phase. Facade-owned. Summary holds the CTRF count breakdown for the common pass/fail check; RawReport carries the marshaled CTRF JSON for callers needing per-test detail; Report is the typed CTRF report retained for in-tree consumers that merge per-phase reports via ctrf.MergeReports.

type ProfileSummary added in v0.19.0

type ProfileSummary struct {
	Name        string   `json:"name" yaml:"name"`
	Description string   `json:"description,omitempty" yaml:"description,omitempty"`
	Default     string   `json:"default" yaml:"default"`
	Values      []string `json:"values" yaml:"values"`
}

ProfileSummary is the compact catalog discovery shape.

type RecipeDigestOptions added in v0.20.0

type RecipeDigestOptions struct {
	// Path is a recipe or overlay file, as a path, an HTTP(S) URL, or a
	// cm://namespace/name ConfigMap URI. Required.
	Path string

	// Kubeconfig resolves cm:// URIs. Empty uses the standard KUBECONFIG,
	// ~/.kube/config, then in-cluster discovery chain.
	Kubeconfig string

	// Profile is a name=value configuration-profile selection, with the
	// same semantics as `aicr recipe --profile`. It applies only to
	// overlay inputs: an already-hydrated recipe carries its own
	// metadata.selectedProfile, and combining the two is rejected.
	Profile string

	// Timeout overrides the facade's operation cap for this call.
	//
	// Nil -- the zero value -- keeps defaults.VerifyOperationTimeout, so a
	// caller who never considered this gets today's behavior. A pointer to 0
	// imposes NO facade cap and runs under the caller's context unchanged. A
	// positive value sets an explicit cap.
	//
	// The pointer exists to make 0 mean "uncapped" rather than "default",
	// matching WithValidationTimeout(0). A plain duration cannot distinguish
	// unset from zero, so it would have had to spell uncapped some other way --
	// and a caller who learned 0-means-uncapped from ValidateState would then
	// get the opposite here, silently capped when they asked for unbounded.
	Timeout *time.Duration
}

RecipeDigestOptions configures Client.RecipeDigest.

type RecipeOutputOptions added in v0.21.0

type RecipeOutputOptions struct {
	// Path is spec.recipe.output.path. Empty when unset.
	Path string

	// Format is spec.recipe.output.format. Empty when unset, leaving the
	// caller's own default in place.
	Format string
}

RecipeOutputOptions carries spec.recipe.output — where and in what format a generated recipe is written. Consumed by the caller after ResolveRecipe returns, not by the resolve itself.

type RecipeRequest

type RecipeRequest struct {
	// Service is the target Kubernetes service identifier, e.g.
	// "eks", "gke", "aks", "oke", "kind", "lke", or "any". Mapped
	// to pkg/recipe CriteriaService. Note that this is the K8s
	// FLAVOR (eks vs gke), not the cloud vendor (aws vs gcp);
	// callers that think in cloud-vendor terms must map first
	// (aws→eks, gcp→gke, etc.).
	Service string

	// Region is the cloud region. Informational only — not part of
	// pkg/recipe.Criteria today; captured on the request so consumers
	// can audit the call without a separate field.
	Region string

	// Accelerator is the GPU model identifier, e.g. "h100", "b200".
	Accelerator string

	// Nodes is the worker-node count hint. Mapped to CriteriaNodes.
	// Note that this is the NUMBER OF NODES, not the number of
	// accelerators — a 64-GPU cluster on 8-GPU nodes has Nodes=8.
	// Zero means "unspecified, AICR picks the default-sized recipe."
	// Negative values are rejected with ErrCodeInvalidRequest.
	Nodes int32

	// Intent is the workload intent. Mapped to CriteriaIntent.
	// Supported values are defined by pkg/recipe.GetCriteriaIntentTypes
	// — today "training" and "inference".
	Intent string

	// OS is the worker-node operating system. Mapped to CriteriaOS.
	// Supported values: "ubuntu", "rhel", "cos", "amazonlinux", "talos", "ol".
	// Empty means "unspecified" — recipe resolution will not select
	// OS-pinned leaf overlays (e.g., h100-eks-ubuntu-training,
	// h100-gke-cos-training) and will fall back to the OS-agnostic
	// ancestor. Set this when the cluster's OS is known so OS-specific
	// constraints and mixins (kernel version, driver tuning) are
	// included.
	//
	// Note: some service+accelerator combinations (e.g. OKE with L40S)
	// have no OS-agnostic recipe and require an explicit OS value;
	// omitting it returns ErrCodeInvalidRequest.
	OS string

	// Platform is the workload platform overlay. Mapped to
	// CriteriaPlatform. Supported values are defined by
	// pkg/recipe.GetCriteriaPlatformTypes — today "", "any", "dynamo",
	// "kubeflow", "nim".
	Platform string

	// Profile is an optional name=value configuration profile selection.
	// Empty applies the resolved declaration's mandatory default.
	Profile string

	// AccountingMode selects the Slurm accounting database ownership model.
	// It is valid only when Platform is "slurm". Empty defaults to "disabled"
	// for newly resolved Slurm recipes.
	AccountingMode string

	// PinnedName reserves space for future pinned-recipe support.
	// Currently rejected with ErrCodeUnavailable; set the criteria
	// fields above instead.
	PinnedName string

	// PinnedVersion reserves space for future pinned-recipe support.
	// Currently rejected with ErrCodeUnavailable.
	PinnedVersion string
}

RecipeRequest is the stable external request shape. The Client translates this into pkg/recipe.Criteria.

type RecipeResolveOption added in v0.19.0

type RecipeResolveOption func(*recipeResolveConfig)

RecipeResolveOption configures one recipe resolution request.

func WithAccountingMode added in v0.19.0

func WithAccountingMode(mode string) RecipeResolveOption

WithAccountingMode selects the Slurm accounting ownership model for a criteria- or snapshot-based resolve call. It is valid only when the resolved platform is Slurm. An empty or invalid mode is rejected when the resolve call runs; omit this option to keep the recipe default.

func WithProfile added in v0.19.0

func WithProfile(profile string) RecipeResolveOption

WithProfile selects a name=value configuration profile for a criteria- or snapshot-based resolve call. Empty applies the declaration default.

func WithRuntimeInventoryMode added in v0.20.0

func WithRuntimeInventoryMode(mode string) RecipeResolveOption

WithRuntimeInventoryMode selects whether the runtime AI inventory component is installed by a criteria- or snapshot-based resolve call. It is valid only when the resolved recipe declares that component; an empty or invalid mode is rejected when the resolve call runs. Omit this option to keep the recipe's own declaration.

Unlike a bundle-time value override, the selection is recorded in the emitted recipe and removes the component's health check along with the component, which is the contract ADR-019 requires for stock adoption.

func WithSnapshotCriteriaRelaxation added in v0.20.0

func WithSnapshotCriteriaRelaxation(stated ...CriteriaDimension) RecipeResolveOption

WithSnapshotCriteriaRelaxation enables the relax-and-retry policy that `aicr recipe --snapshot` applies on top of a snapshot resolve, and declares which criteria dimensions the caller received explicitly.

What it does

A snapshot resolve is STRICT by default: every stated criteria dimension must be honored by an applied overlay, or resolution fails with ErrCodeInvalidRequest carrying details.uncovered. That is the right default for criteria a user typed, but wrong for criteria DERIVED from a snapshot fingerprint — a Kind-style overlay tree can be deliberately OS-agnostic while the fingerprint still detects a concrete os on the node. No recipe content distinguishes that detected value, so failing on it would reject a legitimate query.

With this option, a coverage failure whose uncovered dimensions were ALL derived (i.e. absent from stated) clears those dimensions back to unstated and retries the resolve exactly once. The dimensions actually cleared are reported in RecipeResult.RelaxedDimensions.

What is never relaxed

Three cases propagate the original coverage error instead of retrying:

  • A dimension named in stated. Relaxing a value the caller explicitly asked for would silently resolve a different recipe than requested.
  • A CONSTRAINT-EXCLUDED dimension: an overlay carrying it exists, but the observed cluster failed its constraints. Relaxing there would turn "this cluster does not meet the overlay's requirements" into a broader recipe that resolves cleanly — discarding the finding the operator most needs.
  • A relaxation that would leave NO stated coverage dimension, whose resolve matches every overlay and emits the generic fallback recipe at exit 0 (the fail-open behind issue #1888). Note this is not the same as "criteria is empty": a fingerprint-derived Nodes value survives the clear and still renders in Criteria.String(), but no overlay gates on nodes, so it selects nothing.

Passing no dimensions is meaningful

Presence of the option enables the policy; the argument only narrows what may be relaxed. Calling it with no dimensions is the common case — every dimension came from the fingerprint and all are relaxable:

aicr.WithSnapshotCriteriaRelaxation()                    // relax anything uncovered
aicr.WithSnapshotCriteriaRelaxation(aicr.DimensionOS)    // relax anything but os

Omitting the option entirely preserves strict behavior. Note the difference from an option keyed on a non-empty argument list: the all-derived case is exactly the query the policy exists to serve, so it must not silently fall back to strict.

Scope

Valid only on the snapshot resolve path (ResolveRecipeFromSnapshot and friends). On ResolveRecipeFromCriteria there is no fingerprint — every dimension is caller-supplied — so the option is rejected with ErrCodeInvalidRequest rather than ignored: silently dropping it would leave a caller believing they had `--snapshot` semantics when they did not.

An unrecognized dimension name is rejected when the resolve call runs. It is not treated as an unknown-but-harmless label, because the failure mode is leaving a stated dimension unmarked and relaxing it.

type RecipeResult

type RecipeResult struct {
	// Name is a stable identifier derived from the resolved criteria.
	// Because AICR recipes are keyed by criteria (not by a standalone
	// name), this field is the criteria string representation rather
	// than an independent label.
	Name string

	// Version is the recipe metadata version (set by the CLI that
	// generated the recipe data).
	Version string

	// TranslatedAt is the wall-clock time the facade completed the
	// translation of the internal RecipeResult into this shape. This
	// is NOT the time the underlying recipe was built — AICR's
	// internal RecipeResult currently carries no build timestamp.
	TranslatedAt time.Time

	// Components lists the deployable components in the recipe — enabled
	// component refs only. Disabled refs are omitted; call Resolved() for
	// the full underlying ComponentRefs (enabled and disabled).
	Components []ComponentRef

	// SelectedProfile is present when the resolved composition declares a
	// configuration profile.
	SelectedProfile *SelectedProfile

	// RelaxedDimensions lists the criteria dimensions cleared by
	// WithSnapshotCriteriaRelaxation because no applied overlay
	// distinguished the derived value, in the order the coverage failure
	// reported them. A non-empty value means the resolved recipe is BROADER
	// than the criteria originally requested.
	//
	// It is non-empty only when the first attempt failed coverage on derived
	// dimensions AND the retry succeeded. Every other outcome — the option was
	// not passed, the first attempt succeeded, relaxation was refused, or the
	// retry itself failed — leaves it empty or returns no RecipeResult at all.
	// So this field reports what a successful resolve gave up; it is never how
	// a caller detects a failure, which is always the returned error.
	//
	// The CLI surfaces the same fact as a slog.Warn per dimension; this is
	// the programmatic form, for callers that need to branch on it or report
	// it to their own users.
	RelaxedDimensions []CriteriaDimension
	// contains filtered or unexported fields
}

RecipeResult is the stable external result shape.

func WrapResolved added in v0.19.0

func WrapResolved(r *recipe.RecipeResult) *RecipeResult

WrapResolved wraps an already-resolved pkg/recipe.RecipeResult — typically one obtained from RecipeResult.Resolved() and then projected by the caller — back into the facade shape so it can be handed to SelectFromRecipeWithContext. Returns nil for nil input.

The wrapped result is QUERYABLE ONLY. It carries no owning Client, so Client.MakeBundle, Client.BundleComponents, and Client.ValidateState reject it with ErrCodeInvalidRequest; use Client.AdoptRecipe for those. The caller-supplied result keeps whatever DataProvider it was already bound to, so hydration resolves against the same source that produced it — no deep copy, no re-validation, no provider rebinding.

The REST query handler is the motivating in-tree caller: it projects a legacy (/v1) view of a resolved recipe before selecting, and WrapResolved lets it run the facade's selector rather than an inlined hydrate+select pair that can drift.

func (*RecipeResult) Resolved

func (r *RecipeResult) Resolved() *recipe.RecipeResult

Resolved returns the complete underlying recipe (the full pkg/recipe.RecipeResult) that this result wraps. The facade RecipeResult exposes only Name/Version/TranslatedAt/Components/SelectedProfile; callers that need constraints, validation config, deployment order, or metadata (e.g. evidence emission) use this. Returns nil if the result was not produced by the Client.

Lifetime: the returned pointer is borrowed from the facade RecipeResult. Do not mutate; do not retain past the facade RecipeResult's lifetime. Marshal/serialize first if persistence is needed.

type RecipeSourceOption

type RecipeSourceOption struct {
	// contains filtered or unexported fields
}

RecipeSourceOption identifies where recipes are sourced from.

func EmbeddedSource

func EmbeddedSource() RecipeSourceOption

EmbeddedSource uses only AICR's built-in (embedded) recipe data, no overlay.

func FilesystemSource

func FilesystemSource(path string) RecipeSourceOption

FilesystemSource describes a local filesystem path containing AICR recipes.

func OCISource

func OCISource(repository, selector string) RecipeSourceOption

OCISource describes an OCI repository containing an AICR recipe tree rooted at registry.yaml. repository may optionally begin with "oci://", but must not contain a tag or digest: selector is the single source of truth for the artifact version.

selector is required and must be a sha256:<64-hex-character> manifest digest supplied by trusted configuration. Tags and implicit "latest" are rejected so mutable registry state cannot change the selected catalog.

type ReportSummary

type ReportSummary struct {
	Tests   int
	Passed  int
	Failed  int
	Skipped int
	Pending int
	Other   int
}

ReportSummary is the high-level pass/fail count breakdown of a validation phase's CTRF report. Facade-owned (not aliased to ctrf.Summary); fields mirror the CTRF spec summary contract.

type SelectedProfile added in v0.19.0

type SelectedProfile struct {
	// Name is the declaration this selection came from, e.g. "gpuStack".
	Name string

	// Value is the selected value within that declaration.
	Value string

	// Advertiser declares that a platform-managed component outside the
	// recipe advertises nvidia.com/gpu (ADR-015 GKE allocation-policy
	// amendment). It is "external" on selections whose advertiser is
	// platform-owned — e.g. GKE's managed device plugin on the gpuStack
	// gke-default value (the GKE default) — and empty when no external
	// advertiser is declared: the recipe's own components then determine
	// advertisement (the GPU operator's device plugin, or DRA
	// resources.gpus.enabled). "external" is the only non-empty value.
	Advertiser string

	// OwnedPaths maps each locked component to its sorted dotted value
	// paths, and is the union across every value of the declaration rather
	// than only the selected one. Every listed component carries the
	// synthetic "enabled" path recording that the profile owns its
	// presence. Overriding any listed path is rejected at bundle time
	// unless the override agrees with the selected value.
	OwnedPaths map[string][]string
}

SelectedProfile is the stable facade projection of a recipe profile. It is populated only for aicr.run/v1alpha3 results; an unprofiled composition leaves it nil.

type Snapshot

type Snapshot struct {
	APIVersion string
	Kind       string
	CapturedAt time.Time

	// Raw is the exact YAML document the collection agent emitted, set by
	// Client.CollectSnapshot and empty on snapshots obtained any other way
	// (WrapSnapshot, a hand-constructed Snapshot).
	//
	// Persist THESE bytes rather than re-serializing the parsed snapshot: a
	// newer agent image can emit fields this binary's Snapshot type does not
	// model, and a typed round trip silently drops them. `aicr snapshot`
	// writes Raw for exactly that reason.
	Raw []byte
	// contains filtered or unexported fields
}

Snapshot is the captured cluster-state artifact returned by Client.CollectSnapshot. Facade-owned so the stable surface does not propagate pkg/snapshotter type-shape changes. APIVersion / Kind / CapturedAt are the high-level identifying metadata; the full measurement payload is held in an unexported internal field for zero-copy round-trip through ValidateState. Consumers needing measurement-level inspection import pkg/snapshotter directly.

func WrapSnapshot

func WrapSnapshot(s *snapshotter.Snapshot) *Snapshot

WrapSnapshot wraps a pkg/snapshotter.Snapshot in the facade Snapshot type so callers that load snapshots externally (e.g., the CLI reading a YAML file) can pass them to facade methods. Returns nil for nil input.

func (*Snapshot) Unwrap added in v0.19.0

func (s *Snapshot) Unwrap() *snapshotter.Snapshot

Unwrap returns the underlying pkg/snapshotter.Snapshot — the inverse of WrapSnapshot, and the analog of RecipeResult.Resolved(). In-tree callers (the CLI's validate path) use it to reach measurement-level detail the facade's public fields intentionally do not project.

A Snapshot constructed outside the facade (no internal payload) yields a minimal pkg/snapshotter.Snapshot rebuilt from the public fields, so callers never have to nil-check the result of a non-nil receiver. Returns nil for a nil receiver. The returned pointer is the facade's own — treat it as read-only.

type SnapshotChange added in v0.20.0

type SnapshotChange struct {
	Kind     SnapshotChangeKind     `json:"kind" yaml:"kind"`
	Severity SnapshotChangeSeverity `json:"severity" yaml:"severity"`
	Path     string                 `json:"path" yaml:"path"`
	Baseline *string                `json:"baseline,omitempty" yaml:"baseline,omitempty"`
	Target   *string                `json:"target,omitempty" yaml:"target,omitempty"`
}

SnapshotChange is one field-level difference between two snapshots. Baseline and Target are pointers so an absent side remains distinguishable from a present value whose string representation is empty.

type SnapshotChangeKind added in v0.20.0

type SnapshotChangeKind string

SnapshotChangeKind describes how a snapshot value changed.

const (
	// SnapshotChangeAdded indicates a value exists only in the target snapshot.
	SnapshotChangeAdded SnapshotChangeKind = "added"
	// SnapshotChangeRemoved indicates a value exists only in the baseline snapshot.
	SnapshotChangeRemoved SnapshotChangeKind = "removed"
	// SnapshotChangeModified indicates a value differs between the snapshots.
	SnapshotChangeModified SnapshotChangeKind = "modified"
)

type SnapshotChangeSeverity added in v0.20.0

type SnapshotChangeSeverity string

SnapshotChangeSeverity classifies the impact of a snapshot change.

const (
	// SnapshotChangeSeverityInfo indicates an informational snapshot change.
	SnapshotChangeSeverityInfo SnapshotChangeSeverity = "info"
)

type SnapshotDiff added in v0.20.0

type SnapshotDiff struct {
	BaselineSource string              `json:"baselineSource,omitempty" yaml:"baselineSource,omitempty"`
	TargetSource   string              `json:"targetSource,omitempty" yaml:"targetSource,omitempty"`
	Changes        []SnapshotChange    `json:"changes" yaml:"changes"`
	Summary        SnapshotDiffSummary `json:"summary" yaml:"summary"`
}

SnapshotDiff contains the complete field-level comparison of two snapshots.

func (*SnapshotDiff) HasDrift added in v0.20.0

func (r *SnapshotDiff) HasDrift() bool

HasDrift reports whether the diff contains any field-level changes.

type SnapshotDiffOptions added in v0.20.0

type SnapshotDiffOptions struct {
	BaselineSource string
	TargetSource   string
}

SnapshotDiffOptions configures labels attached to a snapshot diff result. The labels identify the inputs in serialized output; they do not affect the comparison.

type SnapshotDiffSummary added in v0.20.0

type SnapshotDiffSummary struct {
	Added    int `json:"added" yaml:"added"`
	Removed  int `json:"removed" yaml:"removed"`
	Modified int `json:"modified" yaml:"modified"`
	Total    int `json:"total" yaml:"total"`
}

SnapshotDiffSummary contains aggregate snapshot change counts.

type SnapshotOutputOptions added in v0.21.0

type SnapshotOutputOptions struct {
	// Path is spec.snapshot.output.path, the file the snapshot is written to.
	Path string

	// Format is spec.snapshot.output.format (yaml, json, or table), validated
	// by the loader.
	Format string

	// Template is spec.snapshot.output.template, a Go template rendered
	// instead of the structured formats. Requires Format yaml.
	Template string
}

SnapshotOutputOptions carries spec.snapshot.output — where and how a collected snapshot is written. Consumed by the caller performing delivery (snapshotter.DeliverSnapshot), not by Client.CollectSnapshot.

type ValidateInputOptions added in v0.21.0

type ValidateInputOptions struct {
	RecipePath   string
	SnapshotPath string

	// FailOnError decides whether a failed check fails the CALLER. Pointer so
	// "config said nothing" stays distinct from an explicit false, letting the
	// caller's own default apply.
	FailOnError *bool
}

ValidateInputOptions carries the spec.validate fields the CALLER consumes rather than the validator: which recipe and snapshot to validate, and whether a failed check should fail the caller.

Separate from ValidateSettings on purpose. ValidateState takes an already-resolved recipe and snapshot, and it reports check results without acting on them — so these three on ValidateSettings would be surface the validator never reads. The CLI needs them to apply flag-over-config precedence, which is why they are derived at all.

type ValidateOption

type ValidateOption func(*validateConfig)

ValidateOption configures a validation run launched via Client.ValidateState. It is a facade-owned functional option type: each WithValidation* factory below captures its argument into an internal validateConfig, and Client.ValidateState translates the captured config into pkg/validator options at call time.

The wrap insulates the facade's semver contract from pkg/validator's own evolving Option signature. Adding a field to pkg/validator's Validator struct, renaming validator.WithXxx, or changing the validator.Option function signature can all be absorbed inside the translation function without breaking facade consumers.

func WithValidationCleanup

func WithValidationCleanup(cleanup bool) ValidateOption

WithValidationCleanup controls whether validator-emitted Jobs, ConfigMaps, and RBAC are deleted at the end of the run. Default: true. Set to false to leave artifacts behind for post-mortem inspection.

func WithValidationCommit

func WithValidationCommit(commit string) ValidateOption

WithValidationCommit sets the git commit SHA threaded into the validator (validator.WithCommit). Used to resolve dev-build validator images to SHA-tagged images. An empty string is the "unset" sentinel — no validator option is emitted, matching the validator's own behavior where an empty commit influences nothing.

func WithValidationFailFast added in v0.15.0

func WithValidationFailFast(failFast bool) ValidateOption

WithValidationFailFast controls whether ValidateState stops after the first phase that reports StatusFailed. Default: false (all phases run and produce results). Set true to restore stop-on-first-failure behavior.

func WithValidationImagePullSecrets

func WithValidationImagePullSecrets(secrets []string) ValidateOption

WithValidationImagePullSecrets sets imagePullSecrets on the validator pods. Use this when the validator images live in a private registry whose credentials live in a Secret in the validation namespace.

The input is defensively copied; a caller that mutates the slice after this returns won't race with ValidateState reading it on a goroutine. nil-in maps to nil stored (preserves the "unset" sentinel downstream), empty-in maps to an empty-non-nil copy.

func WithValidationImageRegistryOverride

func WithValidationImageRegistryOverride(registry string) ValidateOption

WithValidationImageRegistryOverride overrides the registry prefix on validator container images (validator.WithImageRegistryOverride), e.g. to point at a local registry mirror. Empty means "no override" — the validator keeps its default registry.

func WithValidationImageTagOverride

func WithValidationImageTagOverride(tag string) ValidateOption

WithValidationImageTagOverride overrides the tag on every validator container image (validator.WithImageTagOverride), intended for feature-branch dev builds whose commit SHA has no published image. Empty means "no override" — the validator keeps its resolved tag.

func WithValidationKubeconfig added in v0.18.0

func WithValidationKubeconfig(kubeconfig string) ValidateOption

WithValidationKubeconfig sets an explicit, run-scoped kubeconfig path for every Kubernetes API operation performed by Client.ValidateState, including namespace, RBAC, ConfigMap, validator Job, and result operations. The file is reloaded for each validation run. Empty uses the shared default Kubernetes client and its standard KUBECONFIG, ~/.kube/config, then in-cluster discovery chain.

func WithValidationNamespace

func WithValidationNamespace(namespace string) ValidateOption

WithValidationNamespace sets the Kubernetes namespace where validation Jobs run. Default: "aicr-validation".

func WithValidationNoCluster

func WithValidationNoCluster(noCluster bool) ValidateOption

WithValidationNoCluster enables dry-run mode: no Kubernetes resources are created, all checks report as "skipped - no-cluster mode (test mode)". Constraints are still evaluated inline (they don't need cluster access). Use this for unit tests that exercise the facade surface without a live cluster.

func WithValidationNodeSelector

func WithValidationNodeSelector(nodeSelector map[string]string) ValidateOption

WithValidationNodeSelector passes a node selector through to the validation workload pods. Use when GPU nodes carry non-standard labels and the platform-default selector wouldn't match. Does NOT affect the orchestrator Job itself.

The input is defensively copied; without this, a caller mutating the map after handing off would race with the validator's map iteration (potential "concurrent map iteration and map write" panic in serializeNodeSelector).

func WithValidationPhases

func WithValidationPhases(phases ...Phase) ValidateOption

WithValidationPhases restricts the run to the named phases, in the order given. Valid values are PhaseDeployment, PhasePerformance, and PhaseConformance. When omitted (or called with no phases), all phases run in their canonical order — the default behavior. ValidateState rejects any unrecognized phase value with ErrCodeInvalidRequest before touching the cluster, so a typo cannot silently produce an empty run.

The input is defensively copied so a caller mutating the slice after this returns won't race with ValidateState reading it.

func WithValidationRunID

func WithValidationRunID(runID string) ValidateOption

WithValidationRunID overrides the auto-generated identifier shared across the Jobs and resources produced by a single validation run. Use this to make repeated runs in the same namespace distinguishable (e.g., a controller's reconcile-key suffix).

func WithValidationTimeout

func WithValidationTimeout(d time.Duration) ValidateOption

WithValidationTimeout opts into a facade-level deadline for the ValidateState run. By default (option unset) ValidateState wraps the caller's context with defaults.ValidationOperationTimeout (75m), which suits controllers that pass an unbounded context. Pass a positive duration to set an explicit cap, or 0 to impose NO facade cap — the run then proceeds under the caller's context unchanged so per-validator timeouts (e.g. the 65m inference-perf check) govern. The CLI validate command passes 0 so an all-phase run isn't cut short by a fixed cap.

func WithValidationTolerations

func WithValidationTolerations(tolerations []corev1.Toleration) ValidateOption

WithValidationTolerations passes tolerations through to the validation workload pods (e.g. NCCL benchmark pods). Does NOT affect the orchestrator Job itself, which runs with snapshotter.DefaultTolerations.

The input is defensively copied; mutation after this returns won't race with downstream serialization on a validator goroutine.

type ValidateSettings added in v0.21.0

type ValidateSettings struct {
	Namespace string

	// Image, JobName, ServiceAccountName and RequireGPU have no
	// WithValidation* option — see the type godoc above. A caller feeds them
	// into its own agent/Job construction instead of ValidateState.
	Image              string
	ImagePullSecrets   []string
	JobName            string
	ServiceAccountName string
	NodeSelector       map[string]string
	Tolerations        []corev1.Toleration
	RequireGPU         bool
	Phases             []Phase
	NoCluster          bool

	// Cleanup is INVERTED against spec.validate.execution.noCleanup. The
	// config field says "do not clean up"; this says "clean up". Passing it
	// through straight would delete artifacts a post-mortem asked to keep.
	Cleanup bool

	// FailFast and Timeout stay pointers so "config said nothing" remains
	// distinct from an explicit false / 0s, letting the caller's own default
	// apply rather than being overridden by a zero value.
	FailFast *bool
	Timeout  *time.Duration
}

ValidateSettings carries settings from both spec.validate.agent and spec.validate.execution. Fields are exported so a caller derives, then overrides any of them before use — the same derive-don't-apply precedence the other derivations use.

Not every field reaches Client.ValidateState. Image, JobName, ServiceAccountName and RequireGPU configure the validator's own Kubernetes Job, but pkg/validator exposes no WithValidationImage, WithValidationJobName, WithValidationServiceAccountName or WithValidationRequireGPU option for ValidateState to accept them through — see options.go. They are carried here anyway so a caller (the CLI's parseValidateAgentConfig, in particular) can read spec.validate.agent with its own flag-over-config precedence instead of reaching for Unwrap(). The remaining nine fields (Namespace, ImagePullSecrets, NodeSelector, Tolerations, Phases, NoCluster, Cleanup, FailFast, Timeout) do reach ValidateState via a matching WithValidation* option.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL