#!/usr/bin/env bash # # Inspect a wbxprov-remote-agent deploy ZIP (or the tarball inside one) and # report the actual OS/architecture of the Docker image it contains — WITHOUT # needing docker installed. Reads the OCI/Docker image manifest directly out # of the tarball. # # Use this before shipping a ZIP to a remote host to confirm you built the # right platform. The main `package.sh` already runs `docker image inspect` # on the image it just built, but this script gives you a completely # independent check (no docker daemon involved at all) that also works on # machines that never had docker on them. # # Usage: # ./docker/remote-agent/inspect-bundle.sh # ./docker/remote-agent/inspect-bundle.sh # auto-detect newest ZIP in dist/ # # Exit codes: # 0 bundle looks well-formed; arch/os printed # 1 bad arguments / no bundle found # 2 bundle is malformed or arch could not be read # # Optional env: # EXPECTED_ARCH=amd64 die if the image's architecture does not match # EXPECTED_OS=linux die if the image's OS does not match set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" DIST_DIR="$SCRIPT_DIR/dist" RED=$'\033[0;31m' GRN=$'\033[0;32m' YLW=$'\033[1;33m' RST=$'\033[0m' log() { printf '%s[inspect]%s %s\n' "$GRN" "$RST" "$*"; } warn() { printf '%s[inspect]%s %s\n' "$YLW" "$RST" "$*"; } die() { printf '%s[inspect]%s %s\n' "$RED" "$RST" "$*" >&2; exit 2; } command -v python3 >/dev/null 2>&1 || die "python3 is required (ships with macOS; on Linux: 'apt install python3')." BUNDLE="${1:-}" if [[ -z "$BUNDLE" ]]; then # Auto-detect: newest ZIP in dist/ [[ -d "$DIST_DIR" ]] || { printf '%s\n' "usage: $0 "; exit 1; } BUNDLE="$(ls -t "$DIST_DIR"/*.zip 2>/dev/null | head -n 1 || true)" [[ -n "$BUNDLE" ]] || { printf '%s\n' "usage: $0 (no ZIPs in $DIST_DIR)"; exit 1; } log "Auto-detected newest bundle: $BUNDLE" fi [[ -e "$BUNDLE" ]] || die "File not found: $BUNDLE" # Resolve to the actual image tarball. If given a ZIP, extract to a tempdir # and locate the tar.gz inside; otherwise assume the arg IS the tarball. TMPDIR_INSPECT="" cleanup() { [[ -n "$TMPDIR_INSPECT" ]] && rm -rf "$TMPDIR_INSPECT"; } trap cleanup EXIT case "$BUNDLE" in *.zip) command -v unzip >/dev/null 2>&1 || die "unzip is required to inspect a ZIP." TMPDIR_INSPECT="$(mktemp -d)" log "Extracting ZIP into temp dir for inspection..." unzip -q "$BUNDLE" -d "$TMPDIR_INSPECT" TARBALL="$(find "$TMPDIR_INSPECT" -maxdepth 3 -name '*.tar.gz' -type f | head -n 1 || true)" [[ -n "$TARBALL" ]] || die "Could not find an image tarball (*.tar.gz) inside the ZIP." ;; *.tar.gz|*.tgz|*.tar) TARBALL="$BUNDLE" ;; *) die "Unrecognized bundle type: $BUNDLE (expected .zip, .tar.gz, .tgz, or .tar)" ;; esac log "Reading image manifest from: $(basename "$TARBALL")" # The image tarball is a standard docker/OCI save. It contains: # manifest.json -> lists image config path (per-tag entry) # .json OR blobs/sha256/ -> per-image config JSON # We read manifest.json to find the config blob, then read the config # blob's 'architecture' and 'os' fields. Everything happens in-memory via # `tar -xO`, so nothing is written to disk. DECOMPRESS="cat" case "$TARBALL" in *.gz|*.tgz) DECOMPRESS="gunzip -c" ;; esac MANIFEST_JSON="$($DECOMPRESS "$TARBALL" | tar -xO manifest.json 2>/dev/null || true)" [[ -n "$MANIFEST_JSON" ]] || die "No manifest.json in tarball — is this really a 'docker save' bundle?" CONFIG_PATH="$(printf '%s' "$MANIFEST_JSON" | python3 -c " import json, sys data = json.load(sys.stdin) if not data: sys.exit('empty manifest') entry = data[0] config = entry.get('Config') or entry.get('config') if not config: sys.exit('no Config in manifest entry') print(config) ")" CONFIG_JSON="$($DECOMPRESS "$TARBALL" | tar -xO "$CONFIG_PATH" 2>/dev/null || true)" [[ -n "$CONFIG_JSON" ]] || die "Could not read config blob '$CONFIG_PATH' from tarball." read -r ARCH OS <<<"$(printf '%s' "$CONFIG_JSON" | python3 -c " import json, sys d = json.load(sys.stdin) print(d.get('architecture','?'), d.get('os','?')) ")" log "Image tags: $(printf '%s' "$MANIFEST_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(', '.join(d[0].get('RepoTags') or ['(none)']))")" log "Image OS: ${OS}" log "Image arch: ${ARCH}" FAIL=0 if [[ -n "${EXPECTED_ARCH:-}" && "${ARCH}" != "${EXPECTED_ARCH}" ]]; then warn "Expected arch '${EXPECTED_ARCH}' but got '${ARCH}'." FAIL=1 fi if [[ -n "${EXPECTED_OS:-}" && "${OS}" != "${EXPECTED_OS}" ]]; then warn "Expected OS '${EXPECTED_OS}' but got '${OS}'." FAIL=1 fi if (( FAIL != 0 )); then die "Bundle does not match expected OS/arch. Rebuild with the correct --platform." fi log "OK — bundle looks well-formed."