diff --git a/README.md b/README.md index 7979d6b..9facd8e 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,10 @@ If you want to learn more about Quarkus, please visit its website: https://quark ## Building and deploying natively Build the native image with the Mandrel-based multi-stage Dockerfile and push it -directly to a remote Docker host via [unregistry](https://github.com/psviderski/unregistry) -(no external registry required; only missing layers are transferred over SSH): +to a remote Docker host over an SSH tunnel. The script starts a temporary +`registry:2` container on the remote (localhost only), forwards it to the build +machine, pushes the image through that tunnel, then pulls and retags it on the +remote Docker daemon. No external registry is required. ```shell script ./scripts/build-and-push.sh --remote deploy@prod.example.com @@ -16,7 +18,8 @@ directly to a remote Docker host via [unregistry](https://github.com/psviderski/ 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. See `./scripts/build-and-push.sh --help` for all flags. +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. 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/pom.xml b/pom.xml index 383d922..9d526f7 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.example test-quarkus - 1.0.0-SNAPSHOT + 1.0.1 jar Test Quarkus diff --git a/scripts/build-and-push.sh b/scripts/build-and-push.sh index eda4c5d..055a79a 100755 --- a/scripts/build-and-push.sh +++ b/scripts/build-and-push.sh @@ -1,20 +1,18 @@ #!/usr/bin/env bash # # Build the native Quarkus image and push it to a remote Docker host -# via docker-pussh (unregistry): https://github.com/psviderski/unregistry +# via a temporary registry:2 on the remote, reached through an SSH tunnel. # # - Image name/tag default to pom.xml properties (quarkus.application.*) # - Builds for linux/amd64 by default (cross-compile from arm64 hosts) -# - Transfers only missing layers over SSH (no external registry required) +# - Transfers layers over the tunnel (no external registry required) set -euo pipefail IFS=$'\n\t' readonly DOCKERFILE="src/main/docker/Dockerfile" readonly DEFAULT_PLATFORM="linux/amd64" -readonly UNREGISTRY_PLUGIN="https://raw.githubusercontent.com/psviderski/unregistry/main/docker-pussh" -readonly PLUGIN_DIR="${HOME}/.docker/cli-plugins" -readonly PLUGIN_PATH="${PLUGIN_DIR}/docker-pussh" +readonly TEMP_REGISTRY_IMAGE="registry:2" readonly SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)" REMOTE="" @@ -56,7 +54,7 @@ usage() { Usage: $(basename "$0") --remote USER@HOST[:PORT] [options] Build the native Quarkus image and push it to a remote Docker host using -unregistry (docker-pussh). +a temporary Docker registry on the remote, reached through an SSH tunnel. Required: --remote TARGET SSH destination. Either 'user@host[:port]' @@ -72,10 +70,10 @@ Working directory (defaults to the parent of the script's directory): (override with env PROJECT_DIR) Build options: - --skip-build Skip docker build, only pussh an existing image + --skip-build Skip docker build, only push an existing image (platform is fixed to ${DEFAULT_PLATFORM}) -SSH options forwarded to docker pussh: +SSH options: --ssh-key PATH Private key file (overrides SSH config) --ssh-config PATH SSH config file (default: ~/.ssh/config) @@ -178,6 +176,126 @@ resolve_ssh_alias() { printf '%s@%s:%s|%s\n' "$user" "$hostname" "$port" "$identity" } +random_port() { + echo $((55000 + RANDOM % 10536)) +} + +# REMOTE_TARGET is user@host:port; OpenSSH needs -p and user@host separately. +parse_ssh_target() { + if [[ "$REMOTE_TARGET" == *:* ]]; then + SSH_PORT="${REMOTE_TARGET##*:}" + SSH_HOST_USER="${REMOTE_TARGET%:*}" + else + SSH_PORT="22" + SSH_HOST_USER="$REMOTE_TARGET" + fi +} + +# Populates global ssh_args for ssh(1). +build_ssh_args() { + ssh_args=() + [[ -n "$SSH_KEY" ]] && ssh_args+=(-i "$SSH_KEY") + [[ -n "$SSH_CONFIG" ]] && ssh_args+=(-F "$SSH_CONFIG") + [[ "$SSH_PORT" != "22" ]] && ssh_args+=(-p "$SSH_PORT") + ssh_args+=("$SSH_HOST_USER") +} + +run_remote() { + ssh "${ssh_args[@]}" "$@" +} + +# Sets REMOTE_DOCKER to "docker" or "sudo -n docker". +detect_remote_docker() { + if run_remote "docker info" >/dev/null 2>&1; then + REMOTE_DOCKER="docker" + return 0 + fi + if run_remote "sudo -n docker info" >/dev/null 2>&1; then + REMOTE_DOCKER="sudo -n docker" + return 0 + fi + die "Cannot run docker on ${REMOTE_TARGET}. Ensure Docker is installed and the SSH user may run docker (or passwordless sudo docker)." +} + +REG_PUSH_CONTAINER="" +REG_REMOTE_PORT="" +REG_LOCAL_PORT="" +TUNNEL_PID="" +LOCAL_REGISTRY_REF="" + +cleanup_push_resources() { + [[ -n "${LOCAL_REGISTRY_REF:-}" ]] \ + && docker rmi "$LOCAL_REGISTRY_REF" >/dev/null 2>&1 || true + if [[ -n "${TUNNEL_PID:-}" ]]; then + kill "$TUNNEL_PID" >/dev/null 2>&1 || true + wait "$TUNNEL_PID" 2>/dev/null || true + fi + if [[ -n "${REG_PUSH_CONTAINER:-}" && -n "${REMOTE_DOCKER:-}" && -n "${REMOTE_TARGET:-}" ]]; then + parse_ssh_target + build_ssh_args + run_remote "${REMOTE_DOCKER} rm -f ${REG_PUSH_CONTAINER}" >/dev/null 2>&1 || true + fi +} + +start_remote_registry() { + local attempt output + run_remote "${REMOTE_DOCKER} image inspect ${TEMP_REGISTRY_IMAGE}" >/dev/null 2>&1 \ + || run_remote "${REMOTE_DOCKER} pull ${TEMP_REGISTRY_IMAGE}" + + for attempt in $(seq 1 10); do + REG_REMOTE_PORT=$(random_port) + REG_PUSH_CONTAINER="reg-push-$$-${REG_REMOTE_PORT}" + if output=$(run_remote "${REMOTE_DOCKER} run -d \ + --name ${REG_PUSH_CONTAINER} \ + -p 127.0.0.1:${REG_REMOTE_PORT}:5000 \ + ${TEMP_REGISTRY_IMAGE}" 2>&1); then + return 0 + fi + run_remote "${REMOTE_DOCKER} rm -f ${REG_PUSH_CONTAINER}" >/dev/null 2>&1 || true + if ! echo "$output" | grep -qi bind; then + die "Failed to start temporary registry on remote:\n${output}" + fi + done + die "Failed to start temporary registry on remote (port binding)." +} + +start_ssh_tunnel() { + local attempt + for attempt in $(seq 1 10); do + REG_LOCAL_PORT=$(random_port) + if command -v nc >/dev/null 2>&1 && nc -z 127.0.0.1 "$REG_LOCAL_PORT" 2>/dev/null; then + continue + fi + ssh "${ssh_args[@]}" \ + -o ExitOnForwardFailure=yes \ + -N \ + -L "127.0.0.1:${REG_LOCAL_PORT}:127.0.0.1:${REG_REMOTE_PORT}" & + TUNNEL_PID=$! + sleep 1 + if kill -0 "$TUNNEL_PID" 2>/dev/null; then + return 0 + fi + TUNNEL_PID="" + done + die "Failed to establish SSH tunnel to remote registry." +} + +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}" + + 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" + + log "Pulling image on remote and retagging as ${IMAGE_REF}…" + run_remote "${REMOTE_DOCKER} pull ${remote_registry_ref}" + run_remote "${REMOTE_DOCKER} tag ${remote_registry_ref} ${IMAGE_REF}" + run_remote "${REMOTE_DOCKER} rmi ${remote_registry_ref}" >/dev/null || true +} + # ---------- arg parsing ---------- while [[ $# -gt 0 ]]; do case "$1" in @@ -226,26 +344,17 @@ log "Dockerfile: ${DOCKERFILE}" log "Remote: ${REMOTE_TARGET}" require docker +require ssh docker info >/dev/null 2>&1 \ || die "Docker daemon is not reachable. Start Docker Desktop (or your daemon) and retry." -if ! docker pussh --help >/dev/null 2>&1; then - cat >&2 <