From d5d2301679387659ef4a4f9cf21ee36248399119 Mon Sep 17 00:00:00 2001 From: Ken Eucker Date: Sat, 15 Aug 2026 20:19:27 -0700 Subject: [PATCH] feat(install): --use-local-images so a bundle can carry an unreleased build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit management_compose.yaml pins the published Command Center image and the builder pulls it unconditionally, so a bundle always carries the *released* admin application. That makes an admin-side change impossible to test on an air-gapped target until it ships in an image — including the Easy Setup offline fixes in this branch. With --use-local-images, an image already in the daemon is bundled as-is. Anything not present locally is still pulled, so the flag cannot quietly produce a bundle with a gap in it. It is opt-in because it deliberately does not verify that a local image resembles the tag it claims. Bundles built this way record USED_LOCAL_IMAGES=1 in the manifest and carry a "not for distribution" note in README.txt. The manifest is read key-by-key by the installer, so the added key needs no format bump. The docker wrapper forwards the flag through its existing catch-all, and `docker image inspect` inside the build container resolves against the host daemon via the mounted socket, so host-built images are visible. Co-Authored-By: Claude Opus 5 --- admin/docs/offline-install.md | 25 +++++++++++++++++ install/build_offline_bundle.sh | 49 +++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/admin/docs/offline-install.md b/admin/docs/offline-install.md index af49dc1..80faaaa 100644 --- a/admin/docs/offline-install.md +++ b/admin/docs/offline-install.md @@ -112,6 +112,7 @@ handing you a bundle that breaks in the field. | `--extra-image-archive FILE` | Include an existing `docker save` archive | | `--with-apps LIST` | Bundle Supply Depot app images (`default`, `all`, or a list) | | `--list-apps` | Print the installable app names and exit | +| `--use-local-images` | Bundle images already in the local Docker daemon instead of pulling them | | `--content-dir DIR` | Include pre-staged NOMAD storage content | | `--archive` | Also produce a `.tar.gz` of the finished bundle | @@ -141,6 +142,29 @@ removes them and excludes them from `SHA256SUMS`, and the installer ignores them when loading image archives. Without that, a `._core-images.tar` sidecar would both break checksum verification and be handed to `docker load`. +### Testing an unreleased Command Center + +`management_compose.yaml` pins the published +`ghcr.io/crosstalk-solutions/project-nomad:latest`, and the builder pulls it. A +bundle therefore carries the *released* Command Center, not whatever is in your +checkout — so a change to the admin application cannot be tested air-gapped +until it ships in an image. + +`--use-local-images` closes that loop. Build the image from your checkout, tag it +as the reference the compose file uses, then build the bundle: + +```bash +docker build -t ghcr.io/crosstalk-solutions/project-nomad:latest . +./install/build_offline_bundle_docker.sh --target ubuntu:26.04 --use-local-images +``` + +Any image already present in the daemon is taken as-is; anything missing is still +pulled, so the flag can never quietly produce a bundle with a gap in it. + +Bundles built this way record `USED_LOCAL_IMAGES=1` in the manifest and carry a +note in `README.txt`, because they may contain unreleased code and should not be +distributed. + ### Bundles are specific A bundle is valid for **one operating system, one version, and one @@ -250,6 +274,7 @@ TARGET_OS=ubuntu TARGET_VERSION=26.04 TARGET_ARCH=amd64 WITH_NVIDIA_TOOLKIT=1 +USED_LOCAL_IMAGES=0 CREATED_AT_UTC=2026-08-14T16:00:00Z ``` diff --git a/install/build_offline_bundle.sh b/install/build_offline_bundle.sh index 4ea4a8c..e8fef0c 100755 --- a/install/build_offline_bundle.sh +++ b/install/build_offline_bundle.sh @@ -54,6 +54,7 @@ EXTRA_IMAGE_ARCHIVE='' CONTENT_DIR='' WITH_APPS='' LIST_APPS='0' +USE_LOCAL_IMAGES='0' # Supply Depot apps whose images are worth carrying by default: broadly useful, # and modest in size compared with the AI/education stack. @@ -107,6 +108,12 @@ Options: useful starter set, or "all". Adds significant size — see --list-apps. --list-apps Print the installable app names and exit + --use-local-images Skip the registry pull for any image already + present in the local Docker daemon. Lets a bundle + carry an image built from this checkout (e.g. an + unreleased Command Center) instead of the + published tag. Images that are NOT local are + still pulled as usual. --without-nvidia-toolkit Omit the NVIDIA Container Toolkit packages --extra-image-list FILE Also pull and bundle the image references in FILE --extra-image-archive FILE Copy an existing docker-save archive into the @@ -174,6 +181,9 @@ parse_args() { CONTENT_DIR="$2" shift ;; + --use-local-images) + USE_LOCAL_IMAGES='1' + ;; --archive) CREATE_ARCHIVE='1' ;; @@ -415,6 +425,31 @@ CONTAINER_SCRIPT ok "Local APT repository resolves offline." } +# Make an image available locally so `docker save` can write it into the bundle. +# +# Normally that means pulling the published tag. With --use-local-images, an +# image already in the daemon is taken as-is: the point is to bundle a Command +# Center built from this checkout rather than the released +# ghcr.io/crosstalk-solutions/project-nomad:latest that management_compose.yaml +# pins, so unreleased changes can be tested on an air-gapped target. Images that +# are not present locally are still pulled, so the flag never silently produces +# a bundle with something missing. +# +# Deliberately narrow: it does not verify the local image resembles the tag it +# claims. A bundle built this way carries whatever you built, which is the whole +# point — and the reason it is opt-in rather than the default. +acquire_image() { + local image="$1" + + if [[ "${USE_LOCAL_IMAGES}" == '1' ]] && docker image inspect "${image}" >/dev/null 2>&1; then + log "Using local ${image} (not pulling)." + return 0 + fi + + log "Pulling ${image}..." + docker pull --platform "linux/${TARGET_ARCH}" "${image}" +} + discover_and_save_images() { local compose_file="${REPO_ROOT}/install/management_compose.yaml" local image_list="${BUNDLE_DIR}/images/core-images.txt" @@ -440,8 +475,7 @@ discover_and_save_images() { [[ ${#images[@]} -gt 0 ]] || die "No images were discovered from ${compose_file}." for image in "${images[@]}"; do - log "Pulling ${image}..." - docker pull --platform "linux/${TARGET_ARCH}" "${image}" || + acquire_image "${image}" || die "Failed to pull ${image}. The build machine needs internet access and registry availability." done @@ -550,9 +584,7 @@ bundle_app_images() { log "Bundling ${#images[@]} Supply Depot app image(s)..." for image in "${images[@]}"; do - log "Pulling ${image}" - docker pull --platform "linux/${TARGET_ARCH}" "${image}" || - die "Failed to pull ${image}." + acquire_image "${image}" || die "Failed to pull ${image}." done printf '%s\n' "${images[@]}" > "${BUNDLE_DIR}/images/app-images.txt" @@ -637,6 +669,7 @@ TARGET_OS=${TARGET_OS} TARGET_VERSION=${TARGET_VERSION} TARGET_ARCH=${TARGET_ARCH} WITH_NVIDIA_TOOLKIT=${WITH_NVIDIA} +USED_LOCAL_IMAGES=${USE_LOCAL_IMAGES} CREATED_AT_UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ) EOF @@ -646,7 +679,11 @@ Project NOMAD Offline Artifact Bundle NOMAD commit : ${NOMAD_COMMIT} Target : ${TARGET_OS} ${TARGET_VERSION} (${TARGET_ARCH}) - +$(if [[ "${USE_LOCAL_IMAGES}" == '1' ]]; then + printf '\nNOTE: Built with --use-local-images. One or more images came from the\n' + printf 'build machine rather than a registry, so this bundle may carry\n' + printf 'unreleased code. Not for distribution.\n' +fi) Install on a matching, disconnected target: sudo bash ./install_nomad.sh --artifacts .