#!/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 # # - 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) 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 SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)" REMOTE="" IMAGE_NAME="" IMAGE_TAG="" SSH_KEY="" SSH_CONFIG="" PROJECT_DIR="${PROJECT_DIR:-${SCRIPT_DIR}/..}" SKIP_BUILD=0 SKIP_VERIFY=0 # ---------- logging ---------- if [[ -t 1 ]]; then readonly C_INFO=$'\033[0;36m' readonly C_OK=$'\033[0;32m' readonly C_WARN=$'\033[0;33m' readonly C_ERR=$'\033[0;31m' readonly C_RESET=$'\033[0m' else readonly C_INFO="" C_OK="" C_WARN="" C_ERR="" C_RESET="" fi log() { printf '%s[INFO]%s %s\n' "$C_INFO" "$C_RESET" "$*"; } ok() { printf '%s[OK]%s %s\n' "$C_OK" "$C_RESET" "$*"; } warn() { printf '%s[WARN]%s %s\n' "$C_WARN" "$C_RESET" "$*" >&2; } err() { printf '%s[ERROR]%s %s\n' "$C_ERR" "$C_RESET" "$*" >&2; } die() { err "$*"; exit 1; } on_error() { local exit_code=$? err "Script aborted on line ${BASH_LINENO[0]} (exit ${exit_code})." exit "$exit_code" } trap on_error ERR # ---------- helpers ---------- usage() { cat </dev/null 2>&1; then value=$(xmllint --xpath "string($xpath)" pom.xml 2>/dev/null || true) fi if [[ -z "$value" && -n "$tag" ]]; then value=$(grep -E "<${tag}>" pom.xml | head -1 \ | sed -E "s:.*<${tag}>(.*).*:\1:") fi printf '%s' "${value:-$fallback}" } require() { command -v "$1" >/dev/null 2>&1 || die "Required command '$1' not found in PATH." } resolve_ssh_alias() { local alias="$1" local config="${SSH_CONFIG:-$HOME/.ssh/config}" if [[ "$alias" == *@* ]]; then printf '%s|%s\n' "$alias" "" return 0 fi [[ -f "$config" ]] \ || die "Cannot resolve '$alias': $config not found and value has no '@'." local in_block=0 hostname="" user="" port="" identity="" line first p local -a patterns # Force whitespace-aware splitting (the script's global IFS excludes spaces). local IFS=$' \t\n' # SSH config directives are case-insensitive (HostName == Hostname == HOSTNAME). # nocasematch is supported in bash 3.2 (unlike ${var,,} which needs 4+). shopt -s nocasematch while read -r line || [[ -n "$line" ]]; do # First whitespace-separated token (tabs and spaces collapse uniformly). read -r first _ <<< "$line" [[ -z "$first" || "$first" == "#" ]] && continue if [[ "$first" == "Host" ]]; then if (( in_block )); then break; fi read -r -a patterns <<< "$line" patterns=("${patterns[@]:1}") in_block=0 for p in "${patterns[@]}"; do if [[ "$p" == "$alias" ]]; then in_block=1 hostname=""; user=""; port=""; identity="" break fi done continue fi if (( in_block )); then case "$first" in HostName) read -r _ hostname <<< "$line" ;; User) read -r _ user <<< "$line" ;; Port) read -r _ port <<< "$line" ;; IdentityFile) read -r _ identity <<< "$line" ;; esac fi done < "$config" shopt -u nocasematch (( in_block )) \ || die "No matching Host entry in $config for alias '$alias'. Use user@host[:port] or check ~/.ssh/config." [[ -z "$hostname" ]] && die "Host '$alias' has no HostName in $config." [[ -z "$user" ]] && user="$(whoami)" [[ -z "$port" ]] && port="22" identity="${identity/#\~/$HOME}" printf '%s@%s:%s|%s\n' "$user" "$hostname" "$port" "$identity" } # ---------- arg parsing ---------- while [[ $# -gt 0 ]]; do case "$1" in --remote) REMOTE="$2"; shift 2 ;; --name) IMAGE_NAME="$2"; shift 2 ;; --tag) IMAGE_TAG="$2"; shift 2 ;; --ssh-key) SSH_KEY="$2"; shift 2 ;; --ssh-config) SSH_CONFIG="$2"; shift 2 ;; --project-dir) PROJECT_DIR="$2"; shift 2 ;; --skip-build) SKIP_BUILD=1; shift ;; --no-verify) SKIP_VERIFY=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}" # ---------- resolve SSH target ---------- resolved=$(resolve_ssh_alias "$REMOTE") REMOTE_TARGET="${resolved%|*}" REMOTE_IDENTITY="${resolved##*|}" if [[ "$REMOTE" != *@* ]]; then log "Resolved alias '${REMOTE}' → ${REMOTE_TARGET}" fi if [[ -z "$SSH_KEY" && -n "$REMOTE_IDENTITY" ]]; then SSH_KEY="$REMOTE_IDENTITY" log "Using IdentityFile from SSH config: ${SSH_KEY}" fi # ---------- pre-flight ---------- log "Image: ${IMAGE_REF}" log "Platform: ${DEFAULT_PLATFORM}" log "Dockerfile: ${DOCKERFILE}" log "Remote: ${REMOTE_TARGET}" require docker 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 </dev/null 2>&1 \ || die "Image ${IMAGE_REF} not present locally; remove --skip-build to build it." else log "Building ${IMAGE_REF} for ${DEFAULT_PLATFORM} (this may take several minutes)…" docker build \ --platform "$DEFAULT_PLATFORM" \ -f "$DOCKERFILE" \ -t "$IMAGE_REF" \ . ok "Build finished." fi # ---------- pussh ---------- log "Pusshing ${IMAGE_REF} to ${REMOTE_TARGET} via unregistry…" pussh_args=( "$REMOTE_TARGET" ) [[ -n "$SSH_KEY" ]] && pussh_args+=( -i "$SSH_KEY" ) [[ -n "$SSH_CONFIG" ]] && pussh_args+=( -F "$SSH_CONFIG" ) docker pussh "${IMAGE_REF}" "${pussh_args[@]}" ok "Pussh complete." # ---------- verify ---------- if [[ "$SKIP_VERIFY" -eq 1 ]]; then warn "Skipping post-push verification (--no-verify)." else log "Verifying ${IMAGE_REF} on ${REMOTE_TARGET}…" ssh_args=( "$REMOTE_TARGET" ) [[ -n "$SSH_KEY" ]] && ssh_args=( -i "$SSH_KEY" "${ssh_args[@]}" ) [[ -n "$SSH_CONFIG" ]] && ssh_args=( -F "$SSH_CONFIG" "${ssh_args[@]}" ) if ! ssh "${ssh_args[@]}" "docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E '^${IMAGE_NAME}:${IMAGE_TAG} '"; then die "Image ${IMAGE_REF} not found on ${REMOTE_TARGET} after pussh." fi ok "Image present on remote." 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"