diff --git a/README.md b/README.md index 9facd8e..7a7b656 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,25 @@ remote Docker daemon. No external registry is required. ``` The script reads `artifactId` and `version` from `pom.xml` for the image name -and tag, builds for `linux/amd64`, and verifies the image is present on the -remote after pushing. The temporary registry container and SSH tunnel are removed -when the script finishes. See `./scripts/build-and-push.sh --help` for all flags. +and version tag, builds for `linux/amd64`, and verifies the image is present on +the remote after pushing. On the remote (and locally) that push is tagged three +ways, all pointing at the same image: + +- `:` — the version from `pom.xml` or `--tag` +- `:latest` — moved to this push on every successful deploy +- `:sha256-<12 hex chars>` — immutable tag taken from the registry digest + +The temporary registry container and SSH tunnel are removed when the script +finishes. See `./scripts/build-and-push.sh --help` for all flags. + +To remove leftover temporary registry containers and `localhost:/...` image +tags on the remote (for example after an interrupted push or manual tests): + +```shell script +./scripts/build-and-push.sh --remote deploy@prod.example.com --cleanup +``` + +This does not rebuild or push; it only needs SSH access to the remote host. It can be invoked from any directory: by default it resolves the project root from the script's own location. Override with `--project-dir PATH` or the diff --git a/scripts/build-and-push.sh b/scripts/build-and-push.sh index 055a79a..3f9a626 100755 --- a/scripts/build-and-push.sh +++ b/scripts/build-and-push.sh @@ -3,9 +3,10 @@ # Build the native Quarkus image and push it to a remote Docker host # via a temporary registry:2 on the remote, reached through an SSH tunnel. # -# - Image name/tag default to pom.xml properties (quarkus.application.*) +# - Image name/tag default to pom.xml properties (artifactId and version) # - Builds for linux/amd64 by default (cross-compile from arm64 hosts) # - Transfers layers over the tunnel (no external registry required) +# - On the remote, tags the image as , latest, and sha256- set -euo pipefail IFS=$'\n\t' @@ -23,6 +24,7 @@ SSH_CONFIG="" PROJECT_DIR="${PROJECT_DIR:-${SCRIPT_DIR}/..}" SKIP_BUILD=0 SKIP_VERIFY=0 +CLEANUP=0 # ---------- logging ---------- if [[ -t 1 ]]; then @@ -63,7 +65,8 @@ Required: Image identity (defaults read from pom.xml): --name NAME Image repository name - --tag TAG Image tag + --tag TAG Version tag. The remote also receives :latest + and a sha256- tag for this push. Working directory (defaults to the parent of the script's directory): --project-dir PATH Project root containing pom.xml @@ -80,6 +83,11 @@ SSH options: Post-push: --no-verify Skip the ssh/docker images verification step +Maintenance: + --cleanup Remove leftover temporary registry containers and + localhost:/... image tags on the remote + (no build, no push; Docker local not required) + Other: -h, --help Show this help @@ -94,6 +102,7 @@ Examples: $(basename "$0") --remote prod # uses 'Host prod' in ~/.ssh/config $(basename "$0") --remote ubuntu@1.2.3.4 --ssh-key ~/.ssh/deploy_ed25519 $(basename "$0") --remote deploy@prod --skip-build --tag 2.0.0 + $(basename "$0") --remote prod --cleanup /absolute/path/to/test-quarkus/scripts/$(basename "$0") --remote deploy@prod PROJECT_DIR=/srv/proj $(basename "$0") --remote deploy@prod --skip-build EOF @@ -217,13 +226,27 @@ detect_remote_docker() { die "Cannot run docker on ${REMOTE_TARGET}. Ensure Docker is installed and the SSH user may run docker (or passwordless sudo docker)." } +cleanup_remote_registry() { + log "Cleaning temporary registry leftovers on ${REMOTE_TARGET}…" + run_remote "${REMOTE_DOCKER} ps -aq --filter name=reg-push- | xargs -r ${REMOTE_DOCKER} rm -f" \ + >/dev/null 2>&1 || true + run_remote "${REMOTE_DOCKER} rm -f regtest" >/dev/null 2>&1 || true + run_remote "${REMOTE_DOCKER} images --format '{{.Repository}}:{{.Tag}}' \ + | grep -E '^localhost:[0-9]+/' | xargs -r ${REMOTE_DOCKER} rmi" \ + >/dev/null 2>&1 || true + ok "Remote cleanup complete." +} + REG_PUSH_CONTAINER="" REG_REMOTE_PORT="" REG_LOCAL_PORT="" TUNNEL_PID="" LOCAL_REGISTRY_REF="" +DIGEST_REF="" +PUSH_LOG="" cleanup_push_resources() { + [[ -n "${PUSH_LOG:-}" ]] && rm -f "$PUSH_LOG" [[ -n "${LOCAL_REGISTRY_REF:-}" ]] \ && docker rmi "$LOCAL_REGISTRY_REF" >/dev/null 2>&1 || true if [[ -n "${TUNNEL_PID:-}" ]]; then @@ -280,20 +303,47 @@ start_ssh_tunnel() { die "Failed to establish SSH tunnel to remote registry." } +digest_tag_from_push_log() { + local log_file="$1" + local digest hex + digest=$(sed -n 's/.*digest: \(sha256:[0-9a-f]\{64\}\).*/\1/p' "$log_file" | tail -1) + if [[ -z "$digest" ]]; then + digest=$(docker image inspect --format '{{index .RepoDigests 0}}' "$LOCAL_REGISTRY_REF" 2>/dev/null \ + | sed -n 's/.*@\(sha256:[0-9a-f]\{64\}\)/\1/p') + fi + [[ -n "$digest" ]] || die "Could not read the image digest after push." + hex="${digest#sha256:}" + printf 'sha256-%s' "${hex:0:12}" +} + push_via_registry_tunnel() { LOCAL_REGISTRY_REF="localhost:${REG_LOCAL_PORT}/${IMAGE_NAME}:${IMAGE_TAG}" local remote_registry_ref="localhost:${REG_REMOTE_PORT}/${IMAGE_NAME}:${IMAGE_TAG}" + local latest_ref="${IMAGE_NAME}:latest" + local digest_tag log "Tagging ${IMAGE_REF} as ${LOCAL_REGISTRY_REF}…" docker tag "$IMAGE_REF" "$LOCAL_REGISTRY_REF" log "Pushing ${LOCAL_REGISTRY_REF} through SSH tunnel…" - docker push "$LOCAL_REGISTRY_REF" + PUSH_LOG=$(mktemp) + docker push "$LOCAL_REGISTRY_REF" | tee "$PUSH_LOG" + digest_tag=$(digest_tag_from_push_log "$PUSH_LOG") + rm -f "$PUSH_LOG" + PUSH_LOG="" + DIGEST_REF="${IMAGE_NAME}:${digest_tag}" - log "Pulling image on remote and retagging as ${IMAGE_REF}…" + log "Tagging local image as ${latest_ref} and ${DIGEST_REF}…" + docker tag "$IMAGE_REF" "$latest_ref" + docker tag "$IMAGE_REF" "$DIGEST_REF" + + log "Pulling image on remote and tagging ${IMAGE_REF}, ${latest_ref}, ${DIGEST_REF}…" run_remote "${REMOTE_DOCKER} pull ${remote_registry_ref}" run_remote "${REMOTE_DOCKER} tag ${remote_registry_ref} ${IMAGE_REF}" + run_remote "${REMOTE_DOCKER} tag ${remote_registry_ref} ${latest_ref}" + run_remote "${REMOTE_DOCKER} tag ${remote_registry_ref} ${DIGEST_REF}" run_remote "${REMOTE_DOCKER} rmi ${remote_registry_ref}" >/dev/null || true + ok "Remote tags: ${IMAGE_REF}, ${latest_ref}, ${DIGEST_REF}." } # ---------- arg parsing ---------- @@ -307,23 +357,13 @@ while [[ $# -gt 0 ]]; do --project-dir) PROJECT_DIR="$2"; shift 2 ;; --skip-build) SKIP_BUILD=1; shift ;; --no-verify) SKIP_VERIFY=1; shift ;; + --cleanup) CLEANUP=1; shift ;; -h|--help) usage; exit 0 ;; *) die "Unknown argument: $1 (use --help)" ;; esac done -# ---------- resolve project root ---------- -if ! cd "${PROJECT_DIR}" 2>/dev/null; then - die "Invalid --project-dir: ${PROJECT_DIR}" -fi -PROJECT_DIR="$(pwd)" - -# ---------- defaults ---------- -[[ -z "$IMAGE_NAME" ]] && IMAGE_NAME=$(read_pom "/*[local-name()='project']/*[local-name()='artifactId']" "test-quarkus" "artifactId") -[[ -z "$IMAGE_TAG" ]] && IMAGE_TAG=$(read_pom "/*[local-name()='project']/*[local-name()='version']" "1.0.0-SNAPSHOT" "version") -[[ -z "$REMOTE" ]] && die "Missing --remote USER@HOST[:PORT] (use --help)." - -IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}" +[[ -z "$REMOTE" ]] && die "Missing --remote USER@HOST[:PORT] (use --help)." # ---------- resolve SSH target ---------- resolved=$(resolve_ssh_alias "$REMOTE") @@ -337,6 +377,29 @@ if [[ -z "$SSH_KEY" && -n "$REMOTE_IDENTITY" ]]; then log "Using IdentityFile from SSH config: ${SSH_KEY}" fi +# ---------- remote cleanup only ---------- +if [[ "$CLEANUP" -eq 1 ]]; then + log "Remote: ${REMOTE_TARGET}" + require ssh + parse_ssh_target + build_ssh_args + detect_remote_docker + cleanup_remote_registry + exit 0 +fi + +# ---------- resolve project root ---------- +if ! cd "${PROJECT_DIR}" 2>/dev/null; then + die "Invalid --project-dir: ${PROJECT_DIR}" +fi +PROJECT_DIR="$(pwd)" + +# ---------- defaults ---------- +[[ -z "$IMAGE_NAME" ]] && IMAGE_NAME=$(read_pom "/*[local-name()='project']/*[local-name()='artifactId']" "test-quarkus" "artifactId") +[[ -z "$IMAGE_TAG" ]] && IMAGE_TAG=$(read_pom "/*[local-name()='project']/*[local-name()='version']" "1.0.0-SNAPSHOT" "version") + +IMAGE_REF="${IMAGE_NAME}:${IMAGE_TAG}" + # ---------- pre-flight ---------- log "Image: ${IMAGE_REF}" log "Platform: ${DEFAULT_PLATFORM}" @@ -388,15 +451,20 @@ ok "Push complete." if [[ "$SKIP_VERIFY" -eq 1 ]]; then warn "Skipping post-push verification (--no-verify)." else - log "Verifying ${IMAGE_REF} on ${REMOTE_TARGET}…" - - if ! run_remote "${REMOTE_DOCKER} images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E '^${IMAGE_NAME}:${IMAGE_TAG} '"; then - die "Image ${IMAGE_REF} not found on ${REMOTE_TARGET} after push." - fi - ok "Image present on remote." + log "Verifying tags on ${REMOTE_TARGET}…" + remote_images=$(run_remote "${REMOTE_DOCKER} images --format '{{.Repository}}:{{.Tag}}'") + missing=0 + for ref in "${IMAGE_REF}" "${IMAGE_NAME}:latest" "${DIGEST_REF}"; do + if ! printf '%s\n' "$remote_images" | grep -Fxq "$ref"; then + err "Missing on remote: ${ref}" + missing=1 + fi + done + [[ "$missing" -eq 0 ]] || die "Expected tags were not found on ${REMOTE_TARGET} after push." + ok "Remote has ${IMAGE_REF}, ${IMAGE_NAME}:latest and ${DIGEST_REF}." fi # ---------- next step hint ---------- -printf '\n%s→ To run on the remote:%s\n' "$C_OK" "$C_RESET" -printf ' ssh %s "docker run -d --name %s --restart unless-stopped -p 8080:8080 %s"\n\n' \ - "$REMOTE_TARGET" "$IMAGE_NAME" "$IMAGE_REF" +printf '\n%s→ To run the latest image on the remote:%s\n' "$C_OK" "$C_RESET" +printf ' ssh %s "docker run -d --name %s --restart unless-stopped -p 8080:8080 %s:latest"\n\n' \ + "$REMOTE_TARGET" "$IMAGE_NAME" "$IMAGE_NAME"