Skip to content

feat(mirror): add mirror command#967

Merged
sttts merged 10 commits into
NVIDIA:mainfrom
haarchri:feature/mirror
May 22, 2026
Merged

feat(mirror): add mirror command#967
sttts merged 10 commits into
NVIDIA:mainfrom
haarchri:feature/mirror

Conversation

@haarchri
Copy link
Copy Markdown
Contributor

Summary

Add aicr mirror list subcommand for air-gapped image and chart discovery, with Hauler and Zarf output formats and end-to-end documentation.

Motivation / Context

Air-gapped Kubernetes deployments need to know every container image and Helm chart a recipe references before they can mirror them into a private registry. This PR adds the discovery pipeline and integrates with the two most common air-gap tools (Hauler, Zarf).

Fixes: #949

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Build/CI/tooling

Component(s) Affected

  • CLI (cmd/aicr, pkg/cli)
  • API server (cmd/aicrd, pkg/api, pkg/server)
  • Recipe engine / data (pkg/recipe)
  • Bundlers (pkg/bundler, pkg/component/*)
  • Collectors / snapshotter (pkg/collector, pkg/snapshotter)
  • Validator (pkg/validator)
  • Core libraries (pkg/errors, pkg/k8s)
  • Docs/examples (docs/, examples/)
  • Other: pkg/mirror/ (new), pkg/helm/ (new), pkg/bom/ (image extraction), tools/bom/ (BOM generator now shares pkg/helm/), tools/mirror-e2e (new E2E script), .github/workflows/mirror-e2e.yaml (new CI workflow)

Implementation Notes

Testing

unset GITLAB_TOKEN && make qualify
unset GITLAB_TOKEN && make mirror-e2e 2>&1
  • skipping validate...
  • cleaning distribution directory
  • loading environment variables
  • getting and validating git state
    • ignoring errors because this is a snapshot     error=git doesn't contain any tags - either add a tag or use --snapshot
    • git state                                      commit=96589bc871e50e124153d201fd09476910c883fb branch=feature/mirror current_tag=v0.0.0 previous_tag=<unknown> dirty=false
    • pipe skipped or partially skipped              reason=disabled during snapshot mode
  • parsing tag
  • setting defaults
    • brews is being phased out in favor of homebrew_casks, check https://goreleaser.com/deprecations#brews for more info
  • partial
  • snapshotting
    • building snapshot...                           version=v0.0.0-next
  • ensuring distribution directory
  • setting up metadata
  • writing release metadata
  • loading go mod information
  • build prerequisites
  • building binaries
    • partial build                                  match=target=darwin_arm64_v8.0
    • partial build                                  match=target=darwin_arm64_v8.0
    • building                                       binary=dist/aicr_darwin_arm64_v8.0/aicr
    • running hook                                   hook=bash -c '[ -z "${SLSA_PREDICATE:-}" ] && exit 0; cosign attest-blob --predicate "${SLSA_PREDICATE}" --type https://slsa.dev/provenance/v1 --bundle "$(dirname "/Users/haarchri/Documents/aicr/dist/aicr_darwin_arm64_v8.0/aicr")/aicr-attestation.sigstore.json" --yes "/Users/haarchri/Documents/aicr/dist/aicr_darwin_arm64_v8.0/aicr"'
  • size reports
    • 60.41MiB                                       path=dist/aicr_darwin_arm64_v8.0/aicr
  • writing artifacts metadata
  • build succeeded after 6s
  • thanks for using GoReleaser!
Build completed, binaries are in ./dist
Running mirror list e2e tests...
[MSG] ==========================================
[MSG] Mirror E2E: Hauler + Zarf validation
[MSG] ==========================================
[MSG] Work directory: /tmp/aicr-mirror-e2e-4hxkmu
[MSG] hauler found:  __    __       ___       __    __   __       _______ .______
[MSG] zarf found: v0.76.0
[MSG] Locating aicr binary...
[MSG] Using binary: /Users/haarchri/Documents/aicr/dist/aicr_darwin_arm64_v8.0/aicr
[MSG] Ensuring Kind cluster + local registry...
registry.ctlptl.dev/ctlptl-registry created
Switched to context "kind-aicr".
cluster.ctlptl.dev/kind-aicr created
node/aicr-control-plane condition met
node/aicr-worker condition met
[MSG] Registry verified at localhost:5001
[MSG] Generating recipe...
[cli] data provider set: generation=1
[cli] CLI flag overriding snapshot-detected value: field=service detected=any override=kind
[cli] CLI flag overriding snapshot-detected value: field=accelerator detected=any override=h100
[cli] CLI flag overriding snapshot-detected value: field=intent detected=any override=training
[cli] CLI flag overriding snapshot-detected value: field=os detected=any override=ubuntu
[cli] building recipe from criteria: criteria=criteria(service=kind, accelerator=h100, intent=training, os=ubuntu)
[cli] recipe generation completed: output=/tmp/aicr-mirror-e2e-4hxkmu/recipe.yaml components=12 overlays=4
[MSG] Recipe generated at /tmp/aicr-mirror-e2e-4hxkmu/recipe.yaml
[MSG] Running mirror list in all formats...
[cli] data provider set: generation=1
[cli] loading recipe from file: path=/tmp/aicr-mirror-e2e-4hxkmu/recipe.yaml
[cli] discovering images and charts: components=12
[cli] discovery complete: images=41 charts=12 components=12
[MSG]   hauler: /tmp/aicr-mirror-e2e-4hxkmu/hauler-full.yaml (      90 lines)
[cli] data provider set: generation=1
[cli] loading recipe from file: path=/tmp/aicr-mirror-e2e-4hxkmu/recipe.yaml
[cli] discovering images and charts: components=12
[cli] discovery complete: images=41 charts=12 components=12
[MSG]   zarf:   /tmp/aicr-mirror-e2e-4hxkmu/zarf-full.yaml (     108 lines)
[cli] data provider set: generation=1
[cli] loading recipe from file: path=/tmp/aicr-mirror-e2e-4hxkmu/recipe.yaml
[cli] discovering images and charts: components=12
[cli] discovery complete: images=41 charts=12 components=12
[MSG]   json:   valid
[cli] data provider set: generation=1
[cli] loading recipe from file: path=/tmp/aicr-mirror-e2e-4hxkmu/recipe.yaml
[cli] discovering images and charts: components=12
[cli] discovery complete: images=41 charts=12 components=12
[MSG]   yaml:   valid
[MSG] Trimming manifests to public images (max 5)...
[MSG]   Found 5 public image(s) to test
[MSG]   Hauler trimmed: /tmp/aicr-mirror-e2e-4hxkmu/hauler-trimmed.yaml (5 images)
[MSG]   Zarf trimmed:   /tmp/aicr-mirror-e2e-4hxkmu/zarf-trimmed.yaml (5 images)
[MSG] Resetting local registry for Hauler test...
Creating registry "ctlptl-registry"...
registry.ctlptl.dev/ctlptl-registry created
Switched to context "kind-aicr".
cluster.ctlptl.dev/kind-aicr created
[MSG]   Registry reset and ready
[MSG] Testing Hauler: sync → local registry → verify...
[MSG]   hauler store sync (platform=linux/arm64)...
2026-05-19 16:41:07 INF processing manifest [/tmp/aicr-mirror-e2e-4hxkmu/hauler-trimmed.yaml] to store [/tmp/aicr-mirror-e2e-4hxkmu/hauler-store]
2026-05-19 16:41:07 INF syncing content [content.hauler.cattle.io/v1] with [kind=Images] to store [/tmp/aicr-mirror-e2e-4hxkmu/hauler-store]
2026-05-19 16:41:07 INF adding image [bitnami/kubectl:latest] to the store
2026-05-19 16:41:12 INF successfully added image [index.docker.io/bitnami/kubectl:latest]
2026-05-19 16:41:12 INF adding image [ghcr.io/jkroepke/kube-webhook-certgen:1.8.2] to the store
2026-05-19 16:42:05 INF successfully added image [ghcr.io/jkroepke/kube-webhook-certgen:1.8.2]
2026-05-19 16:42:05 INF adding image [ghcr.io/jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2] to the store
2026-05-19 16:42:45 INF successfully added image [ghcr.io/jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2]
2026-05-19 16:42:45 INF adding image [ghcr.io/kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1] to the store
2026-05-19 16:42:50 INF successfully added image [ghcr.io/kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1]
2026-05-19 16:42:50 INF adding image [ghcr.io/kai-scheduler/kai-scheduler/operator:v0.14.1] to the store
2026-05-19 16:42:55 INF successfully added image [ghcr.io/kai-scheduler/kai-scheduler/operator:v0.14.1]
2026-05-19 16:42:55 INF processing completed successfully
[MSG]   hauler store copy → localhost:5001...
2026-05-19 16:42:55 INF kai-scheduler/kai-scheduler/operator:v0.14.1
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:c3356bfe9b69648c778ef07c8115ab0cd0f8df66730a5e8e29baadc085e175a3
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:bfb59b82a9b65e47d485e53b3e815bca3b3e21a095bd0cb88ced9ac0b48062bf
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:44d654bc6e9919c7ea77a18a4b7c8cb114f0a82fee9f9a1a010be958a6b62ee0
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:017886f7e1764618ffad6fbd503c42a60076c63adc16355cac80f0f311cae4c9
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:7c12895b777bcaa8ccae0605b4de635b68fc32d60fa08f421dc3818bf55ee212
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:5664b15f108bf9436ce3312090a767300800edbbfd4511aa1a6d64357024d5dd
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:aa796dec1a378b52ab2d74d160eb49180e43db68bff18fb75b1201f9fa3fd9bb
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:18b3f9cfec78a7311e7f72f260d73d10d5352371330b02471e182aa1e470c6fc
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:4aa0ea1413d37a58615488592a0b827ea4b2e48fa5a77cf707d0e35f025e613f
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:da7816fa955ea24533c388143c78804c28682eef99b4ee3723b548c70148bba6
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:ddf74a63f7d8b7d157e5db1a45675a58e304b4c1d425b05c28c835b987623395
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:8c8e3ce1891bfe3277227ffc670256c61569fed6316deb641bbebc41eee867d3
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:6b7146ee8d8ed0a1bab3003785f83d1d4f383d8adceb98373c99831a558745e6
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:88d59a2387a1a041db96185febb689ba8e2810b0f2acb1e0f1f690441257cb84
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:00178db943e30e3c688e29745e5bccbae7143c30459912563e3528362b5aafbb
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:ea3773dba9215eb23212d3b3e22bc8e523eb58baf5b9378a0af6dbdbdb93ea86
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:6a592015026b4db2934b15f5c5a9c702004f6034a1cde17d0fd6c6baff323a9f
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:2ed6fe3802ca3dc776b36b886ec8356bbce743f16efbfdf69465705bdbd4767b
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:253e490c8ad89df7e7ee7ade78efe3fb354bcd52c95d9956950bb2725088dbf9
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:d1e6744d69ce57f99317a79ba26ebd096c1c6049cce8a46d253c0e800cecff47
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:11961621dcfa5262772e8b4348c8e4a09f417edffda6bd1db538de6e6bf58cb2
2026-05-19 16:42:55 INF 2026/05/19 16:42:55 pushed blob: sha256:4f2b3e16ec0f75cb7d0f42d30b2e4926d5885577ac453f9daec402cab889c5ea
2026-05-19 16:42:56 INF 2026/05/19 16:42:56 localhost:5001/kai-scheduler/kai-scheduler/operator:v0.14.1: digest: sha256:a061f2f4ce2804acf2154fdd45424f9b340b78607a18c70c90827f03cacc0c0e size: 4266
2026-05-19 16:42:56 INF bitnami/kubectl:latest
2026-05-19 16:42:56 INF 2026/05/19 16:42:56 pushed blob: sha256:f76e8dec2abc696039fd986712b570d130928a1be82850df97fd17e65e60145c
2026-05-19 16:42:56 INF 2026/05/19 16:42:56 pushed blob: sha256:284bb2a16e1eb24742c63d36751e5f2d4f3c6502cf6d20efd5d5747f982e0cad
2026-05-19 16:42:56 INF 2026/05/19 16:42:56 localhost:5001/bitnami/kubectl:latest: digest: sha256:e488310b30eb87166de14da3d38783f91ad63c5475b185cf0d3de719d9c97dae size: 429
2026-05-19 16:42:56 INF jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:62de241dac5fe19d5f8f4defe034289006ddaa0f2cca735db4718fe2a23e504e
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:bfb59b82a9b65e47d485e53b3e815bca3b3e21a095bd0cb88ced9ac0b48062bf
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:017886f7e1764618ffad6fbd503c42a60076c63adc16355cac80f0f311cae4c9
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:fd4aa3667332c2a4837e1b5f395b0555b3c4ec299e38166ae93ee84bad01befa
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:2780920e5dbfbe103d03a583ed75345306e572ec5a48cb10361f046767d9f29a
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:52630fc75a18675c530ed9eba5f55eca09b03e91bd5bc15307918bbc1a7e7296
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:7c12895b777bcaa8ccae0605b4de635b68fc32d60fa08f421dc3818bf55ee212
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:3214acf345c0cc6bbdb56b698a41ccdefc624a09d6beb0d38b5de0b2303ecaf4
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:dd64bf2dd177757451a98fcdc999a339c35dee5d9872d8f4dc69c8f3c4dd0112
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:dcaa5a89b0ccda4b283e16d0b4d0891cd93d5fe05c6798f7806781a6a2d84354
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:4aa0ea1413d37a58615488592a0b827ea4b2e48fa5a77cf707d0e35f025e613f
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:069d1e267530c2e681fbd4d481553b4d05f98082b18fafac86e7f12996dddd0b
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:06090f8d67d81f6fa8dbd58dd3354af44ee3cd00a3f875a37a415c5cfabd13c3
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:1f26b64abcbc76c768c77c1007c320e5d96333d4915208fc75df97e07667fb62
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 localhost:5001/jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2: digest: sha256:75f287503e5cb266f14e70a0a72c7ad760acc89f705c1583fc356d1a40c3c112 size: 2749
2026-05-19 16:42:57 INF kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:04e8e74f1570aae1f64b45695ff1022771e069226fcd5bdabc18f232bfa738f6
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:7a92589a89467930fa017429915d261e9433d7a4dbe8b50b66bace245a2ee1dc
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:f718062daca244ca10ced9019e18ec319be49be3156540d5b9a320ea5007b723
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:9a4aa1190df61701af821c34b6f0b448e52fd8e43cecf568ea43684baef23a04
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:2b5561e0c2cf64f0cdd719a8d9e2fafec62ca0d089092f26ba4fbf4a8293eff4
2026-05-19 16:42:57 INF 2026/05/19 16:42:57 pushed blob: sha256:2314100aa49446bc5d23b859b1d68f60e2ead6944b90a03e43582462d3c96fbc
2026-05-19 16:42:58 INF 2026/05/19 16:42:58 pushed blob: sha256:c57a97b2502dbf8d905aa782f6a2be8f57c8774e28f6d2ddf6a9a866fcc5fd8b
2026-05-19 16:42:58 INF 2026/05/19 16:42:58 localhost:5001/kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1: digest: sha256:a8b3fadbd9084c08208a783a0288b8065b80d89e968d9cf47d1b26cddadab56a size: 1431
2026-05-19 16:42:58 INF jkroepke/kube-webhook-certgen:1.8.2
2026-05-19 16:42:58 INF 2026/05/19 16:42:58 pushed blob: sha256:56d9825b4e93b15640742a0abb2db341045d64b1aacdcc39083cf3f9b895f331
2026-05-19 16:42:58 INF 2026/05/19 16:42:58 pushed blob: sha256:b4c9a9feea3032e3ced7b49bf274922e05437a8916c2f92bae045d21f317b925
2026-05-19 16:42:58 INF 2026/05/19 16:42:58 localhost:5001/jkroepke/kube-webhook-certgen:1.8.2: digest: sha256:62e81342a41eaccb3c995c09c823e52d70f02be4fb647d0c40bd199f7e50e792 size: 481
2026-05-19 16:42:58 INF copied artifacts to [localhost:5001]
[MSG]   Hauler sync complete
[MSG] Verifying images in local registry (Hauler)...
[MSG]   OK: bitnami/kubectl:latest
[MSG]   OK: jkroepke/kube-webhook-certgen:1.8.2
[MSG]   OK: jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2
[MSG]   OK: kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1
[MSG]   OK: kai-scheduler/kai-scheduler/operator:v0.14.1
[MSG]   Hauler registry verification: 5/5 images OK
[MSG] Resetting local registry for Zarf test...
Creating registry "ctlptl-registry"...
registry.ctlptl.dev/ctlptl-registry created
Switched to context "kind-aicr".
cluster.ctlptl.dev/kind-aicr created
[MSG]   Registry reset and ready
[MSG] Testing Zarf: create package → mirror to registry → verify...
[MSG]   zarf package create...
2026-05-19 16:43:00 INF assembling package path=.
2026-05-19 16:43:00 INF fetching info for images count=5 destination=/var/folders/ym/wqrhc4wd0mj6b1wbkc25s0_80000gn/T/zarf-2232923849/images
2026-05-19 16:43:10 INF pulling images count=5
2026-05-19 16:43:10 INF saving image name=docker.io/bitnami/kubectl:latest size=71.18 MBs
2026-05-19 16:43:11 INF saving image name=ghcr.io/jkroepke/kube-webhook-certgen:1.8.2 size=7.97 MBs
2026-05-19 16:43:11 INF saving image name=ghcr.io/jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2 size=17.27 MBs
2026-05-19 16:43:11 INF saving image name=ghcr.io/kai-scheduler/kai-scheduler/operator:v0.14.1 size=45.15 MBs
2026-05-19 16:43:12 INF saving image name=ghcr.io/kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1 size=54.18 MBs
2026-05-19 16:43:12 INF done pulling images count=5 duration=11.8s
2026-05-19 16:43:12 INF composed components successfully
2026-05-19 16:43:12 INF skipping package signing (no signing key material configured)
2026-05-19 16:43:12 INF writing package to disk path=/tmp/aicr-mirror-e2e-4hxkmu/zarf-pkg/zarf-package-aicr-arm64.tar.zst
[MSG]   Zarf package created: zarf-package-aicr-arm64.tar.zst (186M)
[MSG]   zarf package mirror-resources → localhost:5001...
2026-05-19 16:43:13 WRN package signature could not be verified: error=package is not signed - verification cannot be performed
2026-05-19 16:43:13 INF mirroring images images=5
2026-05-19 16:43:13 INF pushing image name=ghcr.io/kai-scheduler/kai-scheduler/operator:v0.14.1
2026-05-19 16:43:13 INF pushing image name=docker.io/bitnami/kubectl:latest
2026-05-19 16:43:14 INF pushing image name=ghcr.io/jkroepke/kube-webhook-certgen:1.8.2
2026-05-19 16:43:14 INF pushing image name=ghcr.io/jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2
2026-05-19 16:43:14 INF pushing image name=ghcr.io/kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1
2026-05-19 16:43:14 INF done pushing images count=5 duration=1.7s
[MSG]   Zarf mirror complete
[MSG] Verifying images in local registry (Zarf)...
[MSG]   OK: bitnami/kubectl:latest
[MSG]   OK: jkroepke/kube-webhook-certgen:1.8.2
[MSG]   OK: jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2
[MSG]   OK: kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1
[MSG]   OK: kai-scheduler/kai-scheduler/operator:v0.14.1
[MSG]   Zarf registry verification: 5/5 images OK
[MSG] ==========================================
[MSG] Mirror E2E: all tests passed
[MSG] ==========================================

Risk Assessment

  • Low — Isolated change, well-tested, easy to revert
  • Medium — Touches multiple components or has broader impact
  • High — Breaking change, affects critical paths, or complex rollout

Rollout notes:
New subcommand only (aicr mirror list). No changes to existing commands or APIs. pkg/helm/ extraction is an internal refactor — tools/bom/ produces identical output.

Checklist

  • Tests pass locally (make test with -race)
  • Linter passes (make lint)
  • I did not skip/disable tests to make CI green
  • I added/updated tests for new functionality
  • I updated docs if user-facing behavior changed
  • Changes follow existing patterns in the codebase
  • Commits are cryptographically signed (git commit -S) — GPG signing info

@haarchri haarchri requested review from a team as code owners May 19, 2026 15:07
@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented May 19, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@haarchri
Copy link
Copy Markdown
Contributor Author

some outputs of the new command:

aicr mirror list --recipe /tmp/recipe.yaml  
[cli] data provider set: generation=1
[cli] loading recipe from file: path=/tmp/recipe.yaml
[cli] discovering images and charts: components=12
[cli] discovery complete: images=41 charts=12 components=12
images:
  - bitnami/kubectl:latest@sha256:1bc359beb3ae3982591349df11db50b0917b0596e8bed8ab9cf0c8a84a3502d1
  - ghcr.io/jkroepke/kube-webhook-certgen:1.8.2
  - ghcr.io/jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2
  - ghcr.io/kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1
  - ghcr.io/kai-scheduler/kai-scheduler/operator:v0.14.1
  - ghcr.io/nvidia/nvsentinel/gpu-health-monitor:v1.3.0-dcgm-3.x
  - ghcr.io/nvidia/nvsentinel/gpu-health-monitor:v1.3.0-dcgm-4.x
  - ghcr.io/nvidia/nvsentinel/labeler:v1.3.0
  - ghcr.io/nvidia/nvsentinel/metadata-collector:v1.3.0
  - ghcr.io/nvidia/nvsentinel/platform-connectors:v1.3.0
  - ghcr.io/nvidia/nvsentinel/syslog-health-monitor:v1.3.0
  - nvcr.io/nvidia/cloud-native/dcgm:4.4.2-1-ubuntu22.04
  - nvcr.io/nvidia/cloud-native/gdrdrv:v2.5.1
  - nvcr.io/nvidia/cloud-native/k8s-cc-manager:v0.1.1
  - nvcr.io/nvidia/cloud-native/k8s-driver-manager:v0.9.1
  - nvcr.io/nvidia/cloud-native/k8s-kata-manager:v0.2.3
  - nvcr.io/nvidia/cloud-native/k8s-mig-manager:v0.13.1
  - nvcr.io/nvidia/cloud-native/network-operator:v26.1.1
  - nvcr.io/nvidia/cloud-native/nvidia-fs:2.26.6
  - nvcr.io/nvidia/cloud-native/vgpu-device-manager:v0.4.1
  - nvcr.io/nvidia/cuda:13.0.1-base-ubi9
  - nvcr.io/nvidia/driver:580.105.08
  - nvcr.io/nvidia/gpu-operator:v25.10.1
  - nvcr.io/nvidia/k8s-device-plugin:v0.18.1
  - nvcr.io/nvidia/k8s-dra-driver-gpu:v25.12.0
  - nvcr.io/nvidia/k8s/container-toolkit:v1.18.1
  - nvcr.io/nvidia/k8s/dcgm-exporter:4.4.2-4.7.0-distroless
  - nvcr.io/nvidia/kubevirt-gpu-device-plugin:v1.4.0
  - nvcr.io/nvidia/skyhook/operator:v0.15.0@sha256:09e4f71cca8757818515f9e7dd4b8f47d30c642dc3a7efe1329d5c19efea76b9
  - quay.io/brancz/kube-rbac-proxy:v0.15.0@sha256:2c7b120590cbe9f634f5099f2cbb91d0b668569023a81505ca124a5c437e7663
  - quay.io/jetstack/cert-manager-cainjector:v1.20.2
  - quay.io/jetstack/cert-manager-controller:v1.20.2
  - quay.io/jetstack/cert-manager-startupapicheck:v1.20.2
  - quay.io/jetstack/cert-manager-webhook:v1.20.2
  - quay.io/prometheus-operator/prometheus-operator:v0.90.1
  - quay.io/prometheus/alertmanager:v0.32.0
  - quay.io/prometheus/node-exporter:v1.11.1
  - quay.io/prometheus/prometheus:v3.11.3
  - registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.18.0
  - registry.k8s.io/nfd/node-feature-discovery:v0.18.3
  - registry.k8s.io/prometheus-adapter/prometheus-adapter:v0.12.0
charts:
  - name: cert-manager
    repository: https://charts.jetstack.io
    chart: cert-manager
    version: v1.20.2
    namespace: cert-manager
  - name: gpu-operator
    repository: https://helm.ngc.nvidia.com/nvidia
    chart: gpu-operator
    version: v25.10.1
    namespace: gpu-operator
  - name: k8s-ephemeral-storage-metrics
    repository: https://jmcgrath207.github.io/k8s-ephemeral-storage-metrics/chart
    chart: k8s-ephemeral-storage-metrics
    version: 1.19.2
    namespace: monitoring
  - name: kai-scheduler
    repository: oci://ghcr.io/kai-scheduler/kai-scheduler
    chart: kai-scheduler
    version: v0.14.1
    namespace: kai-scheduler
  - name: kube-prometheus-stack
    repository: https://prometheus-community.github.io/helm-charts
    chart: kube-prometheus-stack
    version: 84.4.0
    namespace: monitoring
  - name: network-operator
    repository: https://helm.ngc.nvidia.com/nvidia
    chart: network-operator
    version: 26.1.1
    namespace: nvidia-network-operator
  - name: nfd
    repository: https://kubernetes-sigs.github.io/node-feature-discovery/charts
    chart: node-feature-discovery
    version: 0.18.3
    namespace: node-feature-discovery
  - name: nodewright-operator
    repository: https://helm.ngc.nvidia.com/nvidia/skyhook
    chart: skyhook-operator
    version: v0.15.1
    namespace: skyhook
  - name: nvidia-dra-driver-gpu
    repository: https://helm.ngc.nvidia.com/nvidia
    chart: nvidia-dra-driver-gpu
    version: 25.12.0
    namespace: nvidia-dra-driver
  - name: nvsentinel
    repository: oci://ghcr.io/nvidia
    chart: nvsentinel
    version: v1.3.0
    namespace: nvsentinel
  - name: prometheus-adapter
    repository: https://prometheus-community.github.io/helm-charts
    chart: prometheus-adapter
    version: 5.3.0
    namespace: monitoring
  - name: prometheus-operator-crds
    repository: https://prometheus-community.github.io/helm-charts
    chart: prometheus-operator-crds
    version: 28.0.1
    namespace: monitoring
components:
  - component: cert-manager
    type: helm
    images:
      - quay.io/jetstack/cert-manager-cainjector:v1.20.2
      - quay.io/jetstack/cert-manager-controller:v1.20.2
      - quay.io/jetstack/cert-manager-startupapicheck:v1.20.2
      - quay.io/jetstack/cert-manager-webhook:v1.20.2
  - component: gpu-operator
    type: helm
    images:
      - nvcr.io/nvidia/cloud-native/dcgm:4.4.2-1-ubuntu22.04
      - nvcr.io/nvidia/cloud-native/gdrdrv:v2.5.1
      - nvcr.io/nvidia/cloud-native/k8s-cc-manager:v0.1.1
      - nvcr.io/nvidia/cloud-native/k8s-driver-manager:v0.9.1
      - nvcr.io/nvidia/cloud-native/k8s-kata-manager:v0.2.3
      - nvcr.io/nvidia/cloud-native/k8s-mig-manager:v0.13.1
      - nvcr.io/nvidia/cloud-native/nvidia-fs:2.26.6
      - nvcr.io/nvidia/cloud-native/vgpu-device-manager:v0.4.1
      - nvcr.io/nvidia/cuda:13.0.1-base-ubi9
      - nvcr.io/nvidia/driver:580.105.08
      - nvcr.io/nvidia/gpu-operator:v25.10.1
      - nvcr.io/nvidia/k8s-device-plugin:v0.18.1
      - nvcr.io/nvidia/k8s/container-toolkit:v1.18.1
      - nvcr.io/nvidia/k8s/dcgm-exporter:4.4.2-4.7.0-distroless
      - nvcr.io/nvidia/kubevirt-gpu-device-plugin:v1.4.0
  - component: k8s-ephemeral-storage-metrics
    type: helm
    images:
      - ghcr.io/jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2
  - component: kai-scheduler
    type: helm
    images:
      - ghcr.io/kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1
      - ghcr.io/kai-scheduler/kai-scheduler/operator:v0.14.1
  - component: kube-prometheus-stack
    type: helm
    images:
      - ghcr.io/jkroepke/kube-webhook-certgen:1.8.2
      - quay.io/prometheus-operator/prometheus-operator:v0.90.1
      - quay.io/prometheus/alertmanager:v0.32.0
      - quay.io/prometheus/node-exporter:v1.11.1
      - quay.io/prometheus/prometheus:v3.11.3
      - registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.18.0
  - component: network-operator
    type: helm
    images:
      - nvcr.io/nvidia/cloud-native/network-operator:v26.1.1
  - component: nfd
    type: helm
    images:
      - registry.k8s.io/nfd/node-feature-discovery:v0.18.3
  - component: nodewright-operator
    type: helm
    images:
      - bitnami/kubectl:latest@sha256:1bc359beb3ae3982591349df11db50b0917b0596e8bed8ab9cf0c8a84a3502d1
      - nvcr.io/nvidia/skyhook/operator:v0.15.0@sha256:09e4f71cca8757818515f9e7dd4b8f47d30c642dc3a7efe1329d5c19efea76b9
      - quay.io/brancz/kube-rbac-proxy:v0.15.0@sha256:2c7b120590cbe9f634f5099f2cbb91d0b668569023a81505ca124a5c437e7663
  - component: nvidia-dra-driver-gpu
    type: helm
    images:
      - nvcr.io/nvidia/k8s-dra-driver-gpu:v25.12.0
  - component: nvsentinel
    type: helm
    images:
      - ghcr.io/nvidia/nvsentinel/gpu-health-monitor:v1.3.0-dcgm-3.x
      - ghcr.io/nvidia/nvsentinel/gpu-health-monitor:v1.3.0-dcgm-4.x
      - ghcr.io/nvidia/nvsentinel/labeler:v1.3.0
      - ghcr.io/nvidia/nvsentinel/metadata-collector:v1.3.0
      - ghcr.io/nvidia/nvsentinel/platform-connectors:v1.3.0
      - ghcr.io/nvidia/nvsentinel/syslog-health-monitor:v1.3.0
  - component: prometheus-adapter
    type: helm
    images:
      - registry.k8s.io/prometheus-adapter/prometheus-adapter:v0.12.0
  - component: prometheus-operator-crds
    type: helm
    images: []
metadata:
  recipeVersion: v0.0.0-next
  criteria: criteria(service=kind, accelerator=h100, intent=training, os=ubuntu)
aicr mirror list --service kind --accelerator h100 --intent training --os ubuntu --format hauler 2>/dev/null | head -30

apiVersion: content.hauler.cattle.io/v1
kind: Images
metadata:
  name: aicr-images
spec:
  images:
    - name: bitnami/kubectl:latest@sha256:1bc359beb3ae3982591349df11db50b0917b0596e8bed8ab9cf0c8a84a3502d1
    - name: ghcr.io/jkroepke/kube-webhook-certgen:1.8.2
    - name: ghcr.io/jmcgrath207/k8s-ephemeral-storage-metrics:1.19.2
    - name: ghcr.io/kai-scheduler/kai-scheduler/crd-upgrader:v0.14.1
    - name: ghcr.io/kai-scheduler/kai-scheduler/operator:v0.14.1
    - name: ghcr.io/nvidia/nvsentinel/gpu-health-monitor:v1.3.0-dcgm-3.x
    - name: ghcr.io/nvidia/nvsentinel/gpu-health-monitor:v1.3.0-dcgm-4.x
    - name: ghcr.io/nvidia/nvsentinel/labeler:v1.3.0
    - name: ghcr.io/nvidia/nvsentinel/metadata-collector:v1.3.0
    - name: ghcr.io/nvidia/nvsentinel/platform-connectors:v1.3.0
    - name: ghcr.io/nvidia/nvsentinel/syslog-health-monitor:v1.3.0
    - name: nvcr.io/nvidia/cloud-native/dcgm:4.4.2-1-ubuntu22.04
    - name: nvcr.io/nvidia/cloud-native/gdrdrv:v2.5.1
    - name: nvcr.io/nvidia/cloud-native/k8s-cc-manager:v0.1.1
    - name: nvcr.io/nvidia/cloud-native/k8s-driver-manager:v0.9.1
    - name: nvcr.io/nvidia/cloud-native/k8s-kata-manager:v0.2.3
    - name: nvcr.io/nvidia/cloud-native/k8s-mig-manager:v0.13.1
    - name: nvcr.io/nvidia/cloud-native/network-operator:v26.1.1
    - name: nvcr.io/nvidia/cloud-native/nvidia-fs:2.26.6
    - name: nvcr.io/nvidia/cloud-native/vgpu-device-manager:v0.4.1
    - name: nvcr.io/nvidia/cuda:13.0.1-base-ubi9
    - name: nvcr.io/nvidia/driver:580.105.08
    - name: nvcr.io/nvidia/gpu-operator:v25.10.1
    - name: nvcr.io/nvidia/k8s-device-plugin:v0.18.1
aicr mirror list --service kind --accelerator h100 --intent training --os ubuntu --format zarf 2>/dev/null | head -30
components:
  - charts:
      - name: cert-manager
        namespace: cert-manager
        repoName: cert-manager
        url: https://charts.jetstack.io
        version: v1.20.2
      - name: gpu-operator
        namespace: gpu-operator
        repoName: gpu-operator
        url: https://helm.ngc.nvidia.com/nvidia
        version: v25.10.1
      - name: k8s-ephemeral-storage-metrics
        namespace: monitoring
        repoName: k8s-ephemeral-storage-metrics
        url: https://jmcgrath207.github.io/k8s-ephemeral-storage-metrics/chart
        version: 1.19.2
      - name: kai-scheduler
        namespace: kai-scheduler
        url: oci://ghcr.io/kai-scheduler/kai-scheduler/kai-scheduler
        version: v0.14.1
      - name: kube-prometheus-stack
        namespace: monitoring
        repoName: kube-prometheus-stack
        url: https://prometheus-community.github.io/helm-charts
        version: 84.4.0
      - name: network-operator
        namespace: nvidia-network-operator
        repoName: network-operator
        url: https://helm.ngc.nvidia.com/nvidia

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements the aicr mirror list command for discovering container images and Helm charts in recipes and outputting them in formats consumable by air-gap tools (Hauler, Zarf, JSON, YAML). It extracts Helm template rendering into a reusable pkg/helm.RenderChart utility, implements a concurrent discovery engine (mirror.Lister) that applies Helm value overrides and deduplicates results, adds format-specific output renderers with multi-document YAML support, wires the CLI command with flags and validation, includes an end-to-end testing script, updates CI workflows and build tooling, and provides comprehensive user documentation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

area/tests, enhancement

Suggested reviewers

  • mchmarny
  • lockwobr
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(mirror): add mirror command' clearly and concisely summarizes the main change—adding a new mirror command to the CLI. It is specific enough and follows conventional commit format.
Description check ✅ Passed The PR description is comprehensive and related to the changeset. It explains the motivation (air-gapped deployments), provides implementation notes, testing results, and risk assessment. It clearly describes the aicr mirror list subcommand and its objectives.
Linked Issues check ✅ Passed The PR implementation meets the core objectives from issue #949: adds aicr mirror list for image/chart discovery, supports Hauler and Zarf output formats plus JSON/YAML, provides E2E testing demonstrating valid manifest generation and mirroring workflows, and includes comprehensive documentation.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the PR objectives. The new pkg/mirror/ and pkg/helm/ packages, CLI changes, documentation, E2E tooling, and refactoring of tools/bom/ to use shared Helm utilities are all within scope of implementing the mirror discovery feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/actions/setup-build-tools/action.yml:
- Around line 107-126: The action currently hardcodes defaults for
hauler_version, zarf_version, and ctlptl_version which duplicates
.settings.yaml; remove those default values (set defaults to empty) and add
validation so when install_zarf or install_ctlptl (and any similar install
toggles) is true the action fails fast if the corresponding version input
(zarf_version, ctlptl_version, hauler_version) is empty - this ensures versions
must be injected by the load-versions step and preserves .settings.yaml as the
single source of truth.

In @.github/workflows/mirror-e2e.yaml:
- Around line 20-39: Update the mirror-e2e workflow's path filters: in the
mirror-e2e workflow's paths arrays (both the push and pull_request entries)
extend the existing patterns referenced under the paths key so they also include
the repo areas that affect mirror behavior such as mirror-list dependencies and
version-pin sources; modify the paths arrays under the pull_request and push
sections (the paths key in the mirror-e2e workflow) to add patterns that cover
shared rendering/loader code and version pin files so these changes will trigger
the E2E job.

In `@pkg/cli/mirror.go`:
- Around line 160-183: Validate the --format flag as soon as it's read from cmd
(via mirror.Format(cmd.String("format"))) and call isValidMirrorFormat before
calling resolveRecipeForMirror or parsing --set overrides; if invalid, return
the same errors.New(...) message referencing mirror.SupportedFormats()
immediately so you avoid running resolveRecipeForMirror(ctx, cmd, cfg) and
config.ParseValueOverrides(raw) when the format itself is wrong.

In `@pkg/helm/render.go`:
- Line 125: Add a default timeout fallback before calling exec.CommandContext so
the external helm I/O is always bounded: define a defaultTimeout (e.g. 30s),
check the incoming ctx's deadline via ctx.Deadline(); if there is no deadline,
create ctx, cancel := context.WithTimeout(ctx, defaultTimeout) and replace the
ctx passed to exec.CommandContext, if there is a parent deadline compute
remaining := time.Until(deadline) and only wrap with context.WithTimeout(ctx,
defaultTimeout) when remaining > defaultTimeout (otherwise keep the original
ctx) and ensure you call defer cancel() when you create a new cancel function;
update the site where cmd := exec.CommandContext(ctx, "helm", args...) is
invoked to use the possibly-wrapped ctx.
- Around line 62-65: Replace plain fmt.Errorf uses in RenderChart and its helper
code with pkg/errors methods: import "github.com/pkg/errors" and use
errors.Errorf(...) for standalone formatted errors (e.g., the validation check
in RenderChart when input.Chart == ""), and use errors.Wrapf(err, "...") or
errors.Wrap(err, "...") to wrap underlying errors returned from functions such
as temp-file handling, subprocess (helm) failures, or cleanup operations (the
other fmt.Errorf occurrences referenced). Ensure each error includes contextual
message text and wraps the original err where one exists.

In `@pkg/mirror/discover_test.go`:
- Around line 43-343: Add two regression test rows to TestDiscover: one that
verifies context cancellation returns an error by using a blocking
mockHelmRenderer (e.g., a renderer that waits on a channel) and calling
lister.Discover with a pre-cancelled or soon-to-be-cancelled context to assert
Discover returns an error; and another that verifies typed override semantics by
adding a component whose Overrides map sets "enabled": false (boolean) and
asserting the component is skipped (expect wantImages/wantCharts/wantComps = 0)
— use existing helpers/types like mockHelmRenderer, NewLister, WithHelmRenderer,
Discover, and recipe.ComponentRef to place these cases so they exercise
concurrent rendering and override mutation path.

In `@pkg/mirror/discover.go`:
- Around line 302-305: The overridesByKey function currently swallows errors
from recipe.GetComponentRegistry() and returns nil; change it to fail closed by
propagating the error instead of returning nil: when GetComponentRegistry()
returns a non-nil error or a nil registry, return that error (or wrap it with
context) up the call chain so callers know the registry lookup failed; update
overridesByKey’s signature and all callers as needed to accept/handle the error,
and ensure any logging includes context like "overridesByKey: failed to get
component registry" while referencing recipe.GetComponentRegistry() and
overridesByKey in the change.
- Around line 139-165: The Render error handling in the loop around
l.helmRenderer.Render should detect context cancellation/deadline errors (use
errors.Is(renderErr, context.Canceled) or errors.Is(renderErr,
context.DeadlineExceeded) or check gctx.Err()) and stop/propagate the error
instead of turning it into a warning; update the block where renderErr is
handled (around l.helmRenderer.Render, compRef, and ci.Warnings) to return the
context error immediately (or bubble it up) so discovery fails fast on
cancellation, and also add a gctx.Done() check before continuing loops (the
manifest-processing loops that call extractManifestImages) to avoid processing
after cancellation.
- Around line 266-279: The current setNestedValue always stores override values
as strings, causing booleans/numbers/null to become quoted strings; update
setNestedValue (used by applyValueOverrides) to parse the incoming string into
the correct scalar type before storing: detect "null" -> nil, try
strconv.ParseBool for booleans, then strconv.ParseInt/ParseFloat for
integers/floats, and fall back to the original string if none match, then assign
current[part] = parsedValue instead of the raw string so templates receive
proper scalar types.

In `@pkg/mirror/helm.go`:
- Around line 67-69: Replace the direct call to errors.Wrap in
pkg/mirror/helm.go with errors.PropagateOrWrap to preserve any existing
StructuredError codes: instead of returning errors.Wrap(errors.ErrCodeInternal,
err.Error(), err) return the error using errors.PropagateOrWrap(err,
errors.ErrCodeInternal, "contextual message describing the helm mirror operation
failed") so the original code is preserved when present and you avoid
duplicating err.Error(); update the return in the function that returns out and
err accordingly.
- Around line 57-63: The code calls helm.RenderChart with fields from ref
(ref.Name, ref.Chart, ref.Source, ref.Version, ref.Namespace) without checking
ref for nil; add a nil guard at the start of the Render function (or immediately
before the helm.RenderChart call) that returns a clear error if ref == nil, and
only proceed to build helm.ChartInput and call helm.RenderChart when ref is
non-nil.

In `@tools/mirror-e2e`:
- Around line 76-77: The check_tools() function currently calls has_tools with
make, kind, kubectl, crane, yq, helm, ctlptl, goreleaser but omits python3 and
docker; update check_tools() to include both python3 and docker in the has_tools
invocation so the script validates those prerequisites before proceeding (refer
to the check_tools() function and the has_tools call to locate where to add
"python3" and "docker").
- Around line 209-216: The current sed strips any `@sha256`:... suffix
unconditionally (in the public_images assignment and the similar block around
verify_registry()), corrupting digest-only refs; update the sed so it only
removes `@sha256`:... when there is a preceding tag (i.e. a ":" before the "@").
Replace the unconditional sed 's/@sha256:[a-f0-9]*//' with a regex that
preserves digest-only refs, for example use sed -E
's/\(:[^@]*\)`@sha256`:[a-f0-9]*/\1/' (apply the same fix to the other block) so
public_images remains correct and verify_registry() sees the original
digest-only refs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 7e70da8c-84ef-43bb-bdfa-70b8486c71f5

📥 Commits

Reviewing files that changed from the base of the PR and between cc08600 and 5e7a758.

📒 Files selected for processing (26)
  • .github/actions/load-versions/action.yml
  • .github/actions/setup-build-tools/action.yml
  • .github/workflows/mirror-e2e.yaml
  • .settings.yaml
  • Makefile
  • docs/index.yml
  • docs/user/air-gap-mirror.md
  • docs/user/cli-reference.md
  • pkg/cli/mirror.go
  • pkg/cli/mirror_test.go
  • pkg/cli/root.go
  • pkg/defaults/timeouts.go
  • pkg/helm/render.go
  • pkg/helm/render_test.go
  • pkg/mirror/discover.go
  • pkg/mirror/discover_test.go
  • pkg/mirror/format_hauler.go
  • pkg/mirror/format_hauler_test.go
  • pkg/mirror/format_test.go
  • pkg/mirror/format_zarf.go
  • pkg/mirror/format_zarf_test.go
  • pkg/mirror/helm.go
  • pkg/mirror/mirror.go
  • tools/bom/helm.go
  • tools/bom/registry.go
  • tools/mirror-e2e
💤 Files with no reviewable changes (1)
  • tools/bom/registry.go

Comment thread .github/actions/setup-build-tools/action.yml Outdated
Comment thread pkg/cli/mirror.go Outdated
Comment thread pkg/helm/render.go
Comment thread pkg/helm/render.go
Comment thread pkg/mirror/discover_test.go
Comment thread pkg/mirror/discover.go
Comment thread pkg/mirror/helm.go Outdated
Comment thread pkg/mirror/helm.go Outdated
Comment thread tools/mirror-e2e Outdated
Comment thread tools/mirror-e2e
@github-actions
Copy link
Copy Markdown
Contributor

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/setup-tools`:
- Around line 675-733: The optional installers for hauler and zarf can cause the
whole script to exit on failures (due to set -euo pipefail); modify the hauler
and zarf install blocks so that after verify_download_url succeeds the
subsequent steps (curl download, tar/extract, checksum verify, mv/chmod) are
executed inside guarded conditionals or a try-style sequence that catches any
error and logs a warning rather than letting the script exit. Concretely, wrap
the HAULER sequence (using HAULER_TMP, HAULER_TAR, HAULER_URL) in an if ... then
... else ... or use a subshell/command-group with || { log_warning "..."; rm -rf
"${HAULER_TMP}"; } to handle failures, and do the same for the ZARF sequence
(ZARF_TMP, ZARF_BIN, ZARF_BASE, verify_sha256) so failures trigger log_warning
and cleanup but do not abort the script.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: caab44cd-1f9c-4688-8d2e-7018b9bd908e

📥 Commits

Reviewing files that changed from the base of the PR and between 14e58ed and b9aa911.

📒 Files selected for processing (3)
  • docs/user/air-gap-mirror.md
  • tools/check-tools
  • tools/setup-tools

Comment thread tools/setup-tools Outdated
@haarchri haarchri force-pushed the feature/mirror branch 2 times, most recently from 9203cef to d47b792 Compare May 20, 2026 06:36
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (4)
.github/actions/setup-build-tools/action.yml (1)

372-446: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail fast when hauler_version, zarf_version, or ctlptl_version is empty.

These install steps should explicitly validate required version inputs before URL construction; otherwise failures are noisy and non-obvious.

Suggested patch
     - name: Install Hauler
@@
       run: |
         set -euo pipefail
         if [[ -x /usr/local/bin/hauler ]]; then echo "hauler already installed"; hauler version; exit 0; fi
+        if [[ -z "${HAULER_VERSION}" ]]; then
+          echo "::error::hauler_version is required when install_hauler=true"
+          exit 1
+        fi
         VERSION="${HAULER_VERSION#v}"
@@
     - name: Install Zarf
@@
       run: |
         set -euo pipefail
         if [[ -x /usr/local/bin/zarf ]]; then echo "zarf already installed"; zarf version; exit 0; fi
+        if [[ -z "${ZARF_VERSION}" ]]; then
+          echo "::error::zarf_version is required when install_zarf=true"
+          exit 1
+        fi
         VERSION="${ZARF_VERSION#v}"
@@
     - name: Install ctlptl
@@
       run: |
         set -euo pipefail
         if [[ -x /usr/local/bin/ctlptl ]]; then echo "ctlptl already installed"; ctlptl version; exit 0; fi
+        if [[ -z "${CTLPTL_VERSION}" ]]; then
+          echo "::error::ctlptl_version is required when install_ctlptl=true"
+          exit 1
+        fi
         BASE="https://github.com/tilt-dev/ctlptl/releases/download/v${CTLPTL_VERSION}"

As per coding guidelines, .settings.yaml is the single source of truth for tool versions and workflow tooling should enforce that contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/setup-build-tools/action.yml around lines 372 - 446, The
install steps for Hauler, Zarf and ctlptl use HAULER_VERSION, ZARF_VERSION and
CTLPTL_VERSION without validating them, causing unclear failures; add an
explicit non-empty check at the start of each install block (the "Install
Hauler", "Install Zarf", and "Install ctlptl" steps) and emit a clear error and
exit non-zero if the corresponding env var is empty (e.g., check HAULER_VERSION
before computing VERSION and BASE/TAR, ZARF_VERSION before building BIN, and
CTLPTL_VERSION before constructing BASE/TAR) so the workflow fails fast with a
descriptive message that points to the missing input.
pkg/mirror/discover.go (1)

173-182: ⚠️ Potential issue | 🟠 Major

Check gctx inside the manifest-file loops, not just once before them.

After the one-time guard on Line 174, Lines 177-182 will keep reading and parsing every manifest file even if discovery is canceled midway through the slice. Add a gctx.Err() check in each iteration, and ideally thread the context into extractManifestImages, so cancellation stops promptly.

As per coding guidelines: Check context in loops — Always check ctx.Done() in long-running operations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/mirror/discover.go` around lines 173 - 182, The loop currently checks
gctx.Err() only once before iterating manifest slices, so cancellation won't
stop mid-iteration; update both loops over compRef.ManifestFiles and
compRef.PreManifestFiles to check gctx.Err() inside each iteration (e.g. if
gctx.Err() != nil { return gctx.Err() }) and pass the context into
extractManifestImages by adding a ctx/gctx parameter to its signature and call
sites so extractManifestImages can also abort promptly on cancellation.
tools/setup-tools (1)

675-733: ⚠️ Potential issue | 🟠 Major

Don't make Hauler/Zarf install failures fatal.

Lines 679-699 and 708-732 still abort the whole setup flow even though this script advertises both tools as optional for mirror E2E. With set -euo pipefail, a failed download, checksum, extract, or install here blocks general developer setup instead of letting tools/mirror-e2e skip the corresponding test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/setup-tools` around lines 675 - 733, The Hauler and Zarf install blocks
(around the HAULER_* and ZARF_* code paths) currently call exit 1 on failures
and let commands like curl/tar/verify_sha256 abort under set -e, making optional
tools fatal; change both blocks so failures are non-fatal by catching/handling
errors: replace immediate exit 1 and fatal command behavior with guarded checks
and fallback logging (use log_warning/log_error but continue), ensure temporary
dirs (HAULER_TMP/ZARF_TMP) are cleaned on failure, wrap risky commands (curl,
tar, verify_sha256, sudo mv) to detect failure and skip installation without
exiting so tools/mirror-e2e can detect absence and skip tests, and keep
verify_download_url usage but do not call exit on its failure.
tools/mirror-e2e (1)

213-216: ⚠️ Potential issue | 🟠 Major

Verify digest-only refs without forcing :tag syntax.

Lines 213-216 now preserve digest-only refs, but Lines 392-395 still parse every image as name:tag. A public image like ghcr.io/org/app@sha256:... will therefore be checked as localhost:5001/org/app@sha256:<digest>, which is not a valid registry reference and will make the E2E fail on digest-only discoveries.

Also applies to: 392-395

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/mirror-e2e` around lines 213 - 216, The parsing logic that later
coerces every image into name:tag is breaking digest-only refs (e.g.,
ghcr.io/org/app@sha256:...), so update the image-normalization code (the block
that processes public_images and the later parsing at lines handling per-image
rewrite, referenced by the public_images variable and the image-parsing code
around the second block) to detect and preserve digest-only references
containing "`@sha256`:" instead of forcing a :tag form; if an image contains
"`@sha256`:" leave the `@sha256`:<digest> suffix intact when constructing the local
target (do not append or substitute a :tag), otherwise continue to normalize
tagless or tagged names as before. Ensure both the initial selection
(public_images) and the later rewriting/parsing logic (the block currently
treating every image as name:tag) apply the same digest-aware behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/mirror-e2e.yaml:
- Around line 20-47: The workflow’s path filters in
.github/workflows/mirror-e2e.yaml are missing the action used by the job; update
both the push and pull_request "paths" arrays to include the prep-kind-runner
action by adding the pattern '.github/actions/prep-kind-runner/**' so changes to
that action will trigger the Mirror E2E workflow; ensure the new entry is added
alongside the existing '.github/actions/load-versions/**' and
'.github/actions/setup-build-tools/**' entries in both sections.

In `@docs/user/cli-reference.md`:
- Around line 1931-1937: Add blank lines above and below the shell fenced block
containing "aicr mirror list [flags]" and also add a blank line before the Flags
table so the code fence and table are separated from surrounding text; ensure
the fenced block uses proper triple backticks and that the Flags table
(including the `--recipe` row) has a blank line before it to satisfy
MD031/MD058. Target the fenced block with text "aicr mirror list [flags]" and
the following "**Flags:**" table when making the edits.

In `@pkg/mirror/discover.go`:
- Around line 211-213: The code currently replaces any returned error from
g.Wait() (and similarly later) with errors.ErrCodeInternal, losing
structured/context error kinds; update the error handling to use
errors.PropagateOrWrap(err, errors.ErrCodeInternal, "component discovery
failed") instead of errors.Wrap so that existing structured errors (e.g., those
coming from pkg/recipe.GetComponentRegistry()) and cancellation are preserved or
propagated, and apply the same change at the second occurrence (the block around
the later g.Wait()); reference the g.Wait() call and the GetComponentRegistry()
call when making the replacement.

In `@pkg/mirror/mirror.go`:
- Around line 110-125: Render currently emits "null" for JSON/YAML when list ==
nil but other code paths treat nil as an error; update Render (function Render,
type MirrorList) to validate the input at the top (if list == nil) and return a
wrapped invalid-request error instead of proceeding (use the existing errors
package, e.g., errors.ErrCodeInvalid combined with a descriptive message) so
FormatJSON, FormatYAML and any other format branches behave consistently for nil
input.

In `@tools/mirror-e2e`:
- Around line 143-148: The recipe generation invocation using "$AICR_BIN" recipe
should be made hermetic by passing the test-safety flag --no-cluster; update the
command that calls "$AICR_BIN" recipe (the call that currently uses --service
kind --accelerator h100 --intent training --os ubuntu --output
"${WORK}/recipe.yaml") to include --no-cluster so the command does not consult
the host kubeconfig or external clusters during E2E runs.

---

Duplicate comments:
In @.github/actions/setup-build-tools/action.yml:
- Around line 372-446: The install steps for Hauler, Zarf and ctlptl use
HAULER_VERSION, ZARF_VERSION and CTLPTL_VERSION without validating them, causing
unclear failures; add an explicit non-empty check at the start of each install
block (the "Install Hauler", "Install Zarf", and "Install ctlptl" steps) and
emit a clear error and exit non-zero if the corresponding env var is empty
(e.g., check HAULER_VERSION before computing VERSION and BASE/TAR, ZARF_VERSION
before building BIN, and CTLPTL_VERSION before constructing BASE/TAR) so the
workflow fails fast with a descriptive message that points to the missing input.

In `@pkg/mirror/discover.go`:
- Around line 173-182: The loop currently checks gctx.Err() only once before
iterating manifest slices, so cancellation won't stop mid-iteration; update both
loops over compRef.ManifestFiles and compRef.PreManifestFiles to check
gctx.Err() inside each iteration (e.g. if gctx.Err() != nil { return gctx.Err()
}) and pass the context into extractManifestImages by adding a ctx/gctx
parameter to its signature and call sites so extractManifestImages can also
abort promptly on cancellation.

In `@tools/mirror-e2e`:
- Around line 213-216: The parsing logic that later coerces every image into
name:tag is breaking digest-only refs (e.g., ghcr.io/org/app@sha256:...), so
update the image-normalization code (the block that processes public_images and
the later parsing at lines handling per-image rewrite, referenced by the
public_images variable and the image-parsing code around the second block) to
detect and preserve digest-only references containing "`@sha256`:" instead of
forcing a :tag form; if an image contains "`@sha256`:" leave the `@sha256`:<digest>
suffix intact when constructing the local target (do not append or substitute a
:tag), otherwise continue to normalize tagless or tagged names as before. Ensure
both the initial selection (public_images) and the later rewriting/parsing logic
(the block currently treating every image as name:tag) apply the same
digest-aware behavior.

In `@tools/setup-tools`:
- Around line 675-733: The Hauler and Zarf install blocks (around the HAULER_*
and ZARF_* code paths) currently call exit 1 on failures and let commands like
curl/tar/verify_sha256 abort under set -e, making optional tools fatal; change
both blocks so failures are non-fatal by catching/handling errors: replace
immediate exit 1 and fatal command behavior with guarded checks and fallback
logging (use log_warning/log_error but continue), ensure temporary dirs
(HAULER_TMP/ZARF_TMP) are cleaned on failure, wrap risky commands (curl, tar,
verify_sha256, sudo mv) to detect failure and skip installation without exiting
so tools/mirror-e2e can detect absence and skip tests, and keep
verify_download_url usage but do not call exit on its failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 473c72ec-85de-4ff7-bb58-f760e8819760

📥 Commits

Reviewing files that changed from the base of the PR and between b9aa911 and d47b792.

📒 Files selected for processing (30)
  • .github/actions/load-versions/action.yml
  • .github/actions/setup-build-tools/action.yml
  • .github/workflows/mirror-e2e.yaml
  • .settings.yaml
  • Makefile
  • docs/index.yml
  • docs/user/air-gap-mirror.md
  • docs/user/cli-reference.md
  • pkg/cli/mirror.go
  • pkg/cli/mirror_test.go
  • pkg/cli/root.go
  • pkg/component/overrides.go
  • pkg/component/overrides_test.go
  • pkg/defaults/timeouts.go
  • pkg/helm/render.go
  • pkg/helm/render_test.go
  • pkg/mirror/discover.go
  • pkg/mirror/discover_test.go
  • pkg/mirror/format_hauler.go
  • pkg/mirror/format_hauler_test.go
  • pkg/mirror/format_test.go
  • pkg/mirror/format_zarf.go
  • pkg/mirror/format_zarf_test.go
  • pkg/mirror/helm.go
  • pkg/mirror/mirror.go
  • tools/bom/helm.go
  • tools/bom/registry.go
  • tools/check-tools
  • tools/mirror-e2e
  • tools/setup-tools
💤 Files with no reviewable changes (1)
  • tools/bom/registry.go

Comment thread .github/workflows/mirror-e2e.yaml
Comment thread docs/user/cli-reference.md
Comment thread pkg/mirror/discover.go
Comment thread pkg/mirror/mirror.go
Comment thread tools/mirror-e2e
Copy link
Copy Markdown
Contributor

@lockwobr lockwobr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two themes in this review:

  • Zarf output completenessapiVersion is omitted and the file's v1beta1 comment doesn't match upstream. Inline below.
  • Smaller code-level items — OCI URL drift, unbounded helm stdout, trust-model docs note, one nit. Inline below.

Separately, posted a design comment on #949 with two directions worth weighing for the --mirror-registry follow-up (containerd-level redirect vs. bundle emitting the air-gap artifacts directly).

Comment thread pkg/mirror/format_zarf.go Outdated
Comment thread pkg/mirror/format_zarf.go Outdated
Comment thread pkg/helm/render.go Outdated
Comment thread docs/user/air-gap-mirror.md
Comment thread pkg/mirror/discover.go
Copy link
Copy Markdown
Contributor

@lockwobr lockwobr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One additional refactor suggestion separated from the main review so it can be discussed independently — moving the HelmRenderer interface from pkg/mirror into pkg/helm. Details inline.

Comment thread pkg/mirror/helm.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/mirror/discover_test.go (1)

17-26: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use the shared default kube-version constant in these assertions.

The hard-coded "1.33.0" duplicates production config, so a legitimate change to defaults.MirrorDefaultKubeVersion will fail this test even when behavior is correct.

Suggested fix
 import (
 	"context"
 	"testing"
 	"time"
 
+	"github.com/NVIDIA/aicr/pkg/defaults"
 	"github.com/NVIDIA/aicr/pkg/errors"
 	"github.com/NVIDIA/aicr/pkg/helm"
 	"github.com/NVIDIA/aicr/pkg/helm/helmtest"
 	"github.com/NVIDIA/aicr/pkg/recipe"
 )
@@
 		{
 			name:        "no constraints returns default",
 			constraints: nil,
-			want:        "1.33.0",
+			want:        defaults.MirrorDefaultKubeVersion,
 		},
 		{
 			name: "no k8s constraint returns default",
 			constraints: []recipe.Constraint{
 				{Name: "worker-os", Value: "ubuntu"},
 			},
-			want: "1.33.0",
+			want: defaults.MirrorDefaultKubeVersion,
 		},

As per coding guidelines, “Use named constants from pkg/defaults instead of magic literals for timeouts, limits, and configuration values”.

Also applies to: 305-316

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/mirror/discover_test.go` around lines 17 - 26, The test uses a hard-coded
kube version string ("1.33.0") which duplicates production config; update the
assertions in discover_test.go to reference the shared constant
defaults.MirrorDefaultKubeVersion instead of the literal so tests follow the
coding guideline and will stay correct if the default changes; locate the
assertions around the test helper/verify calls (and the additional occurrences
noted later in the file) and replace the literal with
defaults.MirrorDefaultKubeVersion in each assertion or expected value.
♻️ Duplicate comments (2)
pkg/mirror/discover_test.go (1)

247-296: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add non-string override cases to TestSetNestedValue.

setNestedValue now depends on component.ConvertMapValue, but the table only asserts string leaves. A false and null case would lock in the behavior that matters for Helm conditionals and prevent regressing back to truthy strings.

Suggested fix
 	tests := []struct {
 		name     string
 		path     string
 		value    string
 		initial  map[string]any
 		expected any
 	}{
 		{
 			name:     "simple key",
 			path:     "key",
 			value:    "val",
 			initial:  map[string]any{},
 			expected: "val",
 		},
+		{
+			name:     "boolean value",
+			path:     "enabled",
+			value:    "false",
+			initial:  map[string]any{},
+			expected: false,
+		},
 		{
 			name:     "nested key",
 			path:     "a.b.c",
 			value:    "deep",
 			initial:  map[string]any{},
 			expected: "deep",
 		},
+		{
+			name:     "null value",
+			path:     "image.tag",
+			value:    "null",
+			initial:  map[string]any{},
+			expected: nil,
+		},
 		{
 			name:     "override existing",
 			path:     "driver.version",
 			value:    "new",
 			initial:  map[string]any{"driver": map[string]any{"version": "old"}},
 			expected: "new",
 		},
 	}

As per coding guidelines, “Test error cases: Test error conditions and edge cases explicitly”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/mirror/discover_test.go` around lines 247 - 296, The test
TestSetNestedValue only asserts string leaf values but setNestedValue now uses
component.ConvertMapValue so add non-string override cases (e.g., boolean false
and nil) to the tests: extend the tests slice in TestSetNestedValue with cases
like path "flag" value false and path "maybe" value nil (or interface{}(nil))
with appropriate initial maps and expected types/values, then ensure the
walking/assertion logic (using splitPath) checks the actual typed value (not
string equality) so the test will catch regressions where ConvertMapValue
returns a truthy string instead of the original boolean or nil; keep using
setNestedValue and splitPath to locate the leaf.
pkg/mirror/discover.go (1)

177-186: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-check cancellation inside both manifest loops.

After the pre-loop guard, discovery can still keep reading ManifestFiles and PreManifestFiles if gctx is canceled mid-scan. That does extra I/O and can still assemble partial results for a canceled command.

Suggested fix
 			if gctx.Err() != nil {
 				return gctx.Err()
 			}
 			for _, mPath := range compRef.ManifestFiles {
+				if err := gctx.Err(); err != nil {
+					return err
+				}
 				allImages = extractManifestImages(allImages, &ci, compRef.Name, mPath)
 			}
 			for _, mPath := range compRef.PreManifestFiles {
+				if err := gctx.Err(); err != nil {
+					return err
+				}
 				allImages = extractManifestImages(allImages, &ci, compRef.Name, mPath)
 			}

As per coding guidelines, “Check context in loops — Always check ctx.Done() in long-running operations”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/mirror/discover.go` around lines 177 - 186, The loop over
compRef.ManifestFiles and compRef.PreManifestFiles can continue after gctx is
canceled; update the loops in discover.go to check gctx.Err()/gctx.Done() inside
each iteration and break/return early when canceled so no further I/O or
extraction occurs; specifically, inside the loop that calls
extractManifestImages (for both ManifestFiles and PreManifestFiles) add a
context cancellation check (using gctx.Err() or select on gctx.Done()) before
calling extractManifestImages and stop iterating/return the context error if
canceled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/bom/main_test.go`:
- Around line 441-475: The test TestRenderHelmComponentWithValuesFile currently
only checks that output is non-empty; update it to assert the values file path
was passed into the renderer by inspecting the mock renderer's captured input
(use helmtest.MockRenderer's captured fields such as the last render request or
ValuesFiles slice) after calling renderHelmComponent; specifically assert that
the expected values file path filepath.Join(root, "recipes", "components",
"gpu-operator", "values.yaml") appears in the mock's recorded inputs (while
keeping the existing warnings and output checks around renderHelmComponent).

In `@tools/bom/main.go`:
- Around line 171-173: The current probe of values.yaml treats any os.Stat error
as “missing” by setting valuesPath = "", which hides permission/IO errors;
change the os.Stat handling so that if err != nil you check os.IsNotExist(err)
and only clear valuesPath in that case, otherwise surface the error
(log/fatal/return) so real IO/permission problems aren't suppressed — locate the
os.Stat(valuesPath) call and variable valuesPath in tools/bom/main.go and
replace the blanket suppression with an explicit os.IsNotExist check and error
propagation for non-missing errors.
- Around line 166-168: The function renderHelmComponent currently creates a new
root context with context.Background(), breaking cancellation chaining; change
its signature to accept a parent context (e.g., ctx context.Context) and derive
the timeout-bound context with context.WithTimeout(ctx, defaultHelmTimeout) and
defer cancel(), and update callers (including surveyComponent) to pass their
caller context through so cancellations propagate; ensure any internal uses of
context.Background() are removed and the same pattern is applied where
surveyComponent calls renderHelmComponent so the parent context is threaded
end-to-end.

---

Outside diff comments:
In `@pkg/mirror/discover_test.go`:
- Around line 17-26: The test uses a hard-coded kube version string ("1.33.0")
which duplicates production config; update the assertions in discover_test.go to
reference the shared constant defaults.MirrorDefaultKubeVersion instead of the
literal so tests follow the coding guideline and will stay correct if the
default changes; locate the assertions around the test helper/verify calls (and
the additional occurrences noted later in the file) and replace the literal with
defaults.MirrorDefaultKubeVersion in each assertion or expected value.

---

Duplicate comments:
In `@pkg/mirror/discover_test.go`:
- Around line 247-296: The test TestSetNestedValue only asserts string leaf
values but setNestedValue now uses component.ConvertMapValue so add non-string
override cases (e.g., boolean false and nil) to the tests: extend the tests
slice in TestSetNestedValue with cases like path "flag" value false and path
"maybe" value nil (or interface{}(nil)) with appropriate initial maps and
expected types/values, then ensure the walking/assertion logic (using splitPath)
checks the actual typed value (not string equality) so the test will catch
regressions where ConvertMapValue returns a truthy string instead of the
original boolean or nil; keep using setNestedValue and splitPath to locate the
leaf.

In `@pkg/mirror/discover.go`:
- Around line 177-186: The loop over compRef.ManifestFiles and
compRef.PreManifestFiles can continue after gctx is canceled; update the loops
in discover.go to check gctx.Err()/gctx.Done() inside each iteration and
break/return early when canceled so no further I/O or extraction occurs;
specifically, inside the loop that calls extractManifestImages (for both
ManifestFiles and PreManifestFiles) add a context cancellation check (using
gctx.Err() or select on gctx.Done()) before calling extractManifestImages and
stop iterating/return the context error if canceled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 0619b7f1-ce8a-4ec1-b2b0-c22384ca7d32

📥 Commits

Reviewing files that changed from the base of the PR and between d47b792 and 0d5bfcb.

📒 Files selected for processing (13)
  • docs/user/air-gap-mirror.md
  • pkg/helm/helmtest/mock.go
  • pkg/helm/render.go
  • pkg/mirror/discover.go
  • pkg/mirror/discover_test.go
  • pkg/mirror/format_hauler.go
  • pkg/mirror/format_hauler_test.go
  • pkg/mirror/format_zarf.go
  • pkg/mirror/format_zarf_test.go
  • pkg/mirror/mirror.go
  • tools/bom/helm.go
  • tools/bom/main.go
  • tools/bom/main_test.go
💤 Files with no reviewable changes (1)
  • tools/bom/helm.go

Comment thread tools/bom/main_test.go
Comment thread tools/bom/main.go Outdated
Comment thread tools/bom/main.go
@haarchri haarchri requested a review from lockwobr May 21, 2026 07:35
haarchri added 9 commits May 21, 2026 21:56
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
… recipes

Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Signed-off-by: Christopher Haar <christopher.haar@upbound.io>
Copy link
Copy Markdown
Contributor

@lockwobr lockwobr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@sttts sttts merged commit 095eb8d into NVIDIA:main May 22, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: aicr mirror list image and chart discovery with air-gap tool output formats

3 participants