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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions docs/integrator/go-library.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ than in yours.
| `Example_criteriaDimensions` | The coverage dimensions | yes |
| `Example_committedConfig` | `AICRConfig` → source → catalog → criteria, in the required order | no |
| `Example_resolveFromSnapshot` | `LoadSnapshot` plus snapshot criteria relaxation | no |
| `ExampleClient_DiffSnapshots` | In-memory drift detection between two loaded snapshots | no |
| `ExampleClient_LoadRecipe` | Reading a previously emitted recipe | no |
| `ExampleClient_CollectSnapshot` | Capturing cluster state via the snapshotter Job | no |
| `ExampleClient_ValidateState` | Selecting validation phases, and `--no-cluster` mode | no |
Expand Down Expand Up @@ -128,10 +129,10 @@ func main() {
## Snapshotting and validation

Beyond recipe resolution, the facade exposes the rest of the
Snapshot → Validate workflow. Both methods are stateless w.r.t. the
Client's recipe source; they are surfaced through the Client only to
keep the facade uniform and leave room for future per-Client
telemetry hooks.
Snapshot → Validate workflow, including comparison of two snapshots for
configuration drift. These operations are stateless w.r.t. the Client's recipe
source; they are surfaced through the Client to keep the facade uniform and
leave room for future per-Client telemetry hooks.

### Loading a snapshot you already have

Expand Down Expand Up @@ -174,6 +175,46 @@ identity with the loaded snapshot matters, such as hashing what you
validated, capture the source contents yourself and load from that
capture instead of re-reading afterwards.

### Comparing snapshots for drift

`DiffSnapshots` compares the measurement payloads already held by two facade
snapshots. The comparison is in memory: it does not read a cluster or revisit
the file, URL, or ConfigMap the snapshots came from.

```go
baseline, err := client.LoadSnapshot(ctx, "before.yaml", "")
if err != nil {
log.Fatalf("load baseline: %v", err)
}
target, err := client.LoadSnapshot(ctx, "after.yaml", "")
if err != nil {
log.Fatalf("load target: %v", err)
}

result, err := client.DiffSnapshots(ctx, baseline, target, aicr.SnapshotDiffOptions{
BaselineSource: "before.yaml",
TargetSource: "after.yaml",
})
if err != nil {
log.Fatalf("diff snapshots: %v", err)
}
if result.HasDrift() {
log.Printf("detected %d change(s)", result.Summary.Total)
}
```

Drift is returned as data, not as an error. `SnapshotDiff.Changes` preserves
added, removed, and modified values, while `Summary` provides aggregate counts.
The source labels are optional output metadata and do not affect comparison.
Use `aicr.WriteSnapshotDiffTable` for the same human-readable table format as
`aicr diff`; JSON and YAML serializers can consume the facade-owned result
directly.

Inputs must retain at least one typed measurement through `LoadSnapshot`,
`CollectSnapshot`, or `WrapSnapshot`. A hand-constructed `&aicr.Snapshot{}` or
a wrapped snapshot with no usable measurement is rejected instead of being
reported as no drift.

### Capturing a snapshot from a live cluster

```go
Expand Down Expand Up @@ -1075,6 +1116,8 @@ Per-operation caps:
load whatever the source: a local file read, an HTTP(S) fetch, or a
`cm://` ConfigMap read against the Kubernetes API. Distinct from
`SnapshotOperationTimeout` below, which bounds deploying an agent Job.
- `DiffSnapshots`: **no facade cap** — comparison is in memory and the caller's
context governs unchanged.
- `CollectSnapshot`: caller-controlled via `AgentConfig.Timeout` (falling
back to `defaults.SnapshotOperationTimeout` when unset), plus
`defaults.SnapshotOperationGrace`. The grace exists because
Expand Down
6 changes: 5 additions & 1 deletion docs/integrator/public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ in the [Go library integration guide](./go-library.md).
| `pkg/bom` | Internal | Bill-of-materials / image inventory generation. |
| `pkg/config` | Internal | Config-file loading and flag/spec resolution. |
| `pkg/corroborate` | Internal | Cross-source corroboration of observed state. |
| `pkg/diff` | Internal | Structural diff between two snapshots. |
| `pkg/diff` | Internal | Structural snapshot comparison implementation. External consumers use `Client.DiffSnapshots` and `aicr.WriteSnapshotDiffTable`. |
| `pkg/fingerprint` | Internal | Cluster/provider fingerprint detection. |
| `pkg/health` | Internal | Health-check orchestration. |
| `pkg/helm` | Internal | Helm chart rendering helpers. |
Expand Down Expand Up @@ -65,6 +65,10 @@ unrelated exports in their evolving packages remain free to change.
| Facade symbol | Translates to/from | Notes |
|---|---|---|
| `aicr.Snapshot` | `pkg/snapshotter.Snapshot` | **Facade-owned struct**. Public fields are identifying metadata; full measurement payload is preserved in an unexported field for round-trip through `ValidateState`. Obtain one from `Client.LoadSnapshot` (file, URL, or `cm://` ConfigMap) or `Client.CollectSnapshot` (live capture) — neither requires importing `pkg/snapshotter`. `aicr.WrapSnapshot` remains for the narrower case of lifting a `*snapshotter.Snapshot` you already hold from a direct `pkg/snapshotter` call. |
| `aicr.SnapshotDiff`, `aicr.SnapshotChange`, `aicr.SnapshotDiffSummary` | `pkg/diff` result shapes | **Facade-owned structs** returned by `Client.DiffSnapshots`. They preserve the CLI's JSON/YAML schema without exposing `pkg/diff` types. Drift is data (`SnapshotDiff.HasDrift`), while invalid or payload-less inputs and context cancellation are errors. |
| `aicr.SnapshotDiffOptions` | `Client.DiffSnapshots` input | **Facade-owned input struct** carrying optional baseline and target source labels. The labels are copied to output metadata and do not affect comparison semantics. |
| `aicr.SnapshotChangeKind` and its constants | `pkg/diff.ChangeKind` | **Facade-owned string enum** whose values describe added, removed, and modified readings. |
| `aicr.SnapshotChangeSeverity` and `aicr.SnapshotChangeSeverityInfo` | `pkg/diff.Severity` | **Facade-owned string enum** classifying change impact; informational is the currently defined severity. |
| `aicr.AgentConfig` | `pkg/snapshotter.AgentConfig` | **Facade-owned struct** covering the deployment-time agent fields. `Tolerations` keeps `k8s.io/api/core/v1.Toleration` since `k8s.io` is itself a stable contract. It does **not** mirror every `pkg/snapshotter.AgentConfig` field — the network-collector fields `ClusterConfigPath` and `DiscoverNetwork` are not surfaced on the facade type. `AKSGPUPoolsPath` **is** surfaced (controller-side pool projection input, required for AKS profile-qualified resolution from a collected snapshot). |
| `aicr.PhaseResult` | `pkg/validator.PhaseResult` | **Facade-owned struct**. Exposes `Summary` (CTRF counts) and `RawReport` (CTRF JSON bytes); `Report *ctrf.Report` is retained for in-tree consumers that merge per-phase reports. |
| `aicr.Phase`, `aicr.PhaseDeployment` / `PhasePerformance` / `PhaseConformance` | string consts | **Facade-owned**. Values match `pkg/validator/v1` constants verbatim for byte-identical wire round-trip. |
Expand Down
20 changes: 10 additions & 10 deletions pkg/cli/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import (

"github.com/urfave/cli/v3"

aicr "github.com/NVIDIA/aicr/pkg/client/v1"
"github.com/NVIDIA/aicr/pkg/defaults"
"github.com/NVIDIA/aicr/pkg/diff"
"github.com/NVIDIA/aicr/pkg/errors"
"github.com/NVIDIA/aicr/pkg/serializer"
)
Expand Down Expand Up @@ -122,13 +122,13 @@ func runDiffCmd(ctx context.Context, cmd *cli.Command) error {
return err
}

// Unwrap to reach pkg/diff, which still takes the internal shape. This is
// the last direct hop left in this command; exposing diff on the facade
// (#2025) is what removes it, and it was deliberately sequenced after
// LoadSnapshot so it can take facade snapshots rather than paths.
result := diff.Snapshots(baseline.Unwrap(), target.Unwrap())
result.BaselineSource = baselinePath
result.TargetSource = targetPath
result, err := client.DiffSnapshots(ctx, baseline, target, aicr.SnapshotDiffOptions{
BaselineSource: baselinePath,
TargetSource: targetPath,
})
if err != nil {
return err
}

slog.Info("snapshot diff complete",
slog.Int("added", result.Summary.Added),
Expand All @@ -154,7 +154,7 @@ func runDiffCmd(ctx context.Context, cmd *cli.Command) error {
//
// kubeconfig is propagated through to ConfigMap writers so multi-cluster
// workflows write back to the same cluster the snapshots were read from.
func writeDiffResult(ctx context.Context, cmd *cli.Command, outFormat serializer.Format, kubeconfig string, result *diff.Result) (err error) {
func writeDiffResult(ctx context.Context, cmd *cli.Command, outFormat serializer.Format, kubeconfig string, result *aicr.SnapshotDiff) (err error) {
output := cmd.String("output")

// Use custom table writer for human-readable output
Expand All @@ -176,7 +176,7 @@ func writeDiffResult(ctx context.Context, cmd *cli.Command, outFormat serializer
}()
w = f
}
return diff.WriteTable(w, result)
return aicr.WriteSnapshotDiffTable(w, result)
}

// JSON/YAML use standard serializer; thread kubeconfig so ConfigMap
Expand Down
16 changes: 8 additions & 8 deletions pkg/cli/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (

"github.com/urfave/cli/v3"

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

Expand Down Expand Up @@ -111,13 +111,13 @@ func TestWriteTable_ToFile(t *testing.T) {
tmpDir := t.TempDir()
outFile := filepath.Join(tmpDir, "out.txt")

result := &diff.Result{Changes: make([]diff.Change, 0)}
result := &aicr.SnapshotDiff{Changes: make([]aicr.SnapshotChange, 0)}
f, err := os.Create(outFile)
if err != nil {
t.Fatalf("failed to create output file: %v", err)
}

err = diff.WriteTable(f, result)
err = aicr.WriteSnapshotDiffTable(f, result)
if closeErr := f.Close(); closeErr != nil && err == nil {
err = closeErr
}
Expand All @@ -135,10 +135,10 @@ func TestWriteTable_ToFile(t *testing.T) {
}

func TestWriteTable_ToStdout(t *testing.T) {
result := &diff.Result{Changes: make([]diff.Change, 0)}
result := &aicr.SnapshotDiff{Changes: make([]aicr.SnapshotChange, 0)}

// WriteTable to stdout should not error.
err := diff.WriteTable(os.Stdout, result)
err := aicr.WriteSnapshotDiffTable(os.Stdout, result)
if err != nil {
t.Errorf("WriteTable to stdout failed: %v", err)
}
Expand Down Expand Up @@ -336,7 +336,7 @@ func TestWriteDiffResult_TableToFile(t *testing.T) {
outFile := filepath.Join(tmpDir, "out.txt")

cmd := buildDiffCommandWithOutput(t, outFile)
result := &diff.Result{Changes: make([]diff.Change, 0)}
result := &aicr.SnapshotDiff{Changes: make([]aicr.SnapshotChange, 0)}

if err := writeDiffResult(t.Context(), cmd, serializer.FormatTable, "", result); err != nil {
t.Fatalf("writeDiffResult failed: %v", err)
Expand All @@ -358,7 +358,7 @@ func TestWriteDiffResult_CreateFails(t *testing.T) {
bogusPath := filepath.Join(t.TempDir(), "does-not-exist", "out.txt")

cmd := buildDiffCommandWithOutput(t, bogusPath)
result := &diff.Result{Changes: make([]diff.Change, 0)}
result := &aicr.SnapshotDiff{Changes: make([]aicr.SnapshotChange, 0)}

err := writeDiffResult(t.Context(), cmd, serializer.FormatTable, "", result)
if err == nil {
Expand All @@ -380,7 +380,7 @@ func TestWriteDiffResult_KubeconfigPropagatesToConfigMap(t *testing.T) {
bogusKubeconfig := filepath.Join(tmpDir, "missing-kubeconfig.yaml")

cmd := buildDiffCommandWithOutput(t, "cm://aicr/test")
result := &diff.Result{Changes: make([]diff.Change, 0)}
result := &aicr.SnapshotDiff{Changes: make([]aicr.SnapshotChange, 0)}

err := writeDiffResult(t.Context(), cmd, serializer.FormatJSON, bogusKubeconfig, result)
if err == nil {
Expand Down
10 changes: 6 additions & 4 deletions pkg/client/v1/aicr.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
// - 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
Expand Down Expand Up @@ -62,10 +64,10 @@
// - VerifyBinaryAttestation — package-level; prove an aicr binary was
// built by NVIDIA CI.
//
// All facade types (Snapshot, AgentConfig, Criteria, RecipeRequest,
// RecipeResult, ComponentBundle, ComponentRef, PhaseResult, and AllowLists)
// are facade-owned structs translated to and from the upstream pkg/*
// shapes, so internal field renames don't churn external callers.
// 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,
Expand Down
Loading
Loading