#!/usr/bin/env bash # Build spark-server on ECS via multi-stage Dockerfile.remote (golang container compile). # Never overwrites whole configs/config.yaml; only patches image/avatar max bytes if present. # Env: SPARK_RELEASE, SPARK_SRC_DIR, SPARK_DATA_DIR set -euo pipefail DATA="${SPARK_DATA_DIR:-/data/spark-server}" REMOTE="${SPARK_SRC_DIR:-/tmp/spark-image-limit-build}" RELEASE="${SPARK_RELEASE:-img150k-$(date +%Y%m%d%H%M%S)}" LOCK="${SPARK_DEPLOY_LOCK:-/tmp/spark-image-limit-build.lock}" COMPOSE_LOCK="${SPARK_COMPOSE_LOCK:-/tmp/spark-compose-mutate.lock}" cleanup() { rm -f "$LOCK" || true; } cleanup_all() { rm -f "$LOCK" "$COMPOSE_LOCK" || true; } trap cleanup EXIT if [[ -f "$COMPOSE_LOCK" ]]; then echo "compose mutate lock held: $COMPOSE_LOCK" >&2 cat "$COMPOSE_LOCK" >&2 || true exit 99 fi if [[ -f /tmp/sign-energy-deploy.lock ]]; then echo "sign-energy deploy in progress; abort to avoid conflict" >&2 cat /tmp/sign-energy-deploy.lock >&2 || true exit 99 fi echo $$ >"$LOCK" echo "==> release $RELEASE (src=$REMOTE)" cd "$REMOTE" if [[ -f spark-src.tgz ]]; then tar xzf spark-src.tgz fi test -f go.mod DF=Dockerfile.remote if [[ ! -f "$DF" && -f deploy/docker/Dockerfile.remote ]]; then DF=deploy/docker/Dockerfile.remote fi test -f "$DF" mkdir -p "$DATA/releases" "$DATA/configs/backup" "$DATA/images" # Stamp rollback BEFORE retagging :latest if [[ -f "$DATA/configs/config.yaml" ]]; then cp -a "$DATA/configs/config.yaml" "$DATA/configs/backup/config.yaml.$(date +%Y%m%d%H%M%S)" fi if docker image inspect spark-server:latest >/dev/null 2>&1; then docker tag spark-server:latest spark-server:rollback docker image inspect spark-server:latest --format '{{.Id}}' >"$DATA/releases/previous_image_id" date -Iseconds >"$DATA/releases/previous_stamped_at" fi # Patch media limits in-place only when keys already exist (never rewrite whole file). python3 - <<'PY' from pathlib import Path import re p = Path("/data/spark-server/configs/config.yaml") if not p.is_file(): print("NOTE: no config.yaml") raise SystemExit(0) t = p.read_text(encoding="utf-8") if "image_max_bytes:" not in t: print("NOTE: image_max_bytes missing; code DefaultOptions (150 KiB) will apply") raise SystemExit(0) def set_key(text, key, val): pat = re.compile(r"(^\s*" + key + r":\s*)\d+", re.M) if pat.search(text): return pat.sub(r"\g<1>" + str(val), text), True return text, False nt, c1 = set_key(t, "image_max_bytes", 153600) nt, c2 = set_key(nt, "avatar_max_bytes", 153600) if c1 or c2: p.write_text(nt, encoding="utf-8") print("patched media max bytes to 153600") else: print("media keys present; left as-is") PY echo "==> docker build via Dockerfile.remote (compile inside golang:1.26-bookworm; NO host go)" docker pull golang:1.26-bookworm # Fallback explicit container compile if someone only has runtime Dockerfile: # docker run --rm -v "$REMOTE":/src -w /src -e CGO_ENABLED=0 -e GOOS=linux -e GOARCH=amd64 \ # -e GOPROXY=https://proxy.golang.org,direct --entrypoint go golang:1.26-bookworm \ # build -trimpath -ldflags='-s -w -buildid=' -o bin/linux_amd64/spark-server ./cmd/server docker build --platform linux/amd64 \ -f "$DF" \ -t "spark-server:${RELEASE}" \ -t spark-server:latest \ "$REMOTE" echo "==> save image tar" docker save "spark-server:${RELEASE}" | gzip -c >"$DATA/images/spark-server-${RELEASE}.tar.gz" cp -f "$DATA/images/spark-server-${RELEASE}.tar.gz" "$DATA/images/spark-server.tar.gz" echo "img150k $RELEASE $(date -Iseconds) pid=$$" >"$COMPOSE_LOCK" trap cleanup_all EXIT cd "$DATA/compose" docker compose -f docker-compose.yaml up -d --force-recreate spark-server ok=0 for _ in $(seq 1 36); do if curl -fsS -m 3 http://127.0.0.1:18080/healthz >/dev/null 2>&1; then ok=1 break fi sleep 2 done if [[ "$ok" != "1" ]]; then echo "HEALTH_FAIL rolling back" >&2 docker tag spark-server:rollback spark-server:latest || true docker compose -f docker-compose.yaml up -d --force-recreate spark-server || true exit 2 fi docker exec spark-server /usr/local/bin/spark-migrate -config /app/configs/config.yaml -dir /app/migrations || true echo "==> verify" curl -fsS http://127.0.0.1:18080/healthz; echo curl -fsS http://127.0.0.1:18080/readyz; echo grep -E 'image_max_bytes|avatar_max_bytes' "$DATA/configs/config.yaml" || echo "config keys absent (DefaultOptions OK)" docker exec spark-server sh -c 'strings /usr/local/bin/spark-server | grep -F "All server-written images are capped" | head -1' || echo "string probe miss" docker ps --filter name=spark-server --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' printf '%s\n' "$RELEASE" >"$DATA/releases/current_release" IMG_ID=$(docker image inspect spark-server:latest --format '{{.Id}}') PREV=$(cat "$DATA/releases/previous_image_id" 2>/dev/null || true) NOW=$(date -Iseconds) cat >"$DATA/releases/manifest.json" <