feat(install): --use-local-images so a bundle can carry an unreleased build

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 <noreply@anthropic.com>
This commit is contained in:
Ken Eucker 2026-08-15 20:19:27 -07:00
parent 8c91436aa4
commit d5d2301679
2 changed files with 68 additions and 6 deletions

View File

@ -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
```

View File

@ -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 .