diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7ce4f3f --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# Booth deploy env — copy to `.env` and fill in, then run ./scripts/booth.sh up +# (prod). Consumed by docker-compose.yml + the prod override via --env-file. +# See wiki/decisions/container-deployment.md. Do NOT commit the filled-in .env. + +# --- image source (prod pulls from the house Gitea registry) ------------------ +# The registry namespace; combined with the image name + TAG below. +REGISTRY=git.infra.msai.al/mca/parking_solution +# Moving branch tag to deploy: `main` for production, `dev` for staging. A push to +# that branch republishes this tag, so `booth.sh update` pulls the latest of it. +TAG=main + +# --- secrets (NO safe defaults — the server refuses to boot without a real one) - +# JWT signing secret. Generate yourself, never share it: openssl rand -hex 32 +# Must be 32+ chars and must NOT contain change-me / insecure / dev-only. +JWT_SECRET= + +# Ledger-signing key for the append-only signed event chain. Set a DISTINCT value +# in prod (don't reuse JWT_SECRET). openssl rand -hex 32 +EVENT_SIGNING_KEY= + +# --- booth LAN specifics ------------------------------------------------------ +# Auth cookie is HTTPS-only by default; the booth is plain HTTP behind Caddy on +# :80, so this MUST stay 0 or operators cannot log in. Set to 1 only behind TLS. +COOKIE_SECURE=0 + +# Remote origins the live WS feed must accept (same-origin always passes). Add any +# address admins hit the UI from beyond the booth itself, comma-separated, e.g. +# http://parksystems.msai.al (leave blank if only the local booth URL is used). +WS_ALLOWED_ORIGINS= + +# Vision/ANPR. Prod override already forces the fast_alpr engine; leave VISION_ENABLED=1 +# unless you are running without the camera. (Set 0 to disable the vision call entirely.) +VISION_ENABLED=1 diff --git a/scripts/booth.sh b/scripts/booth.sh new file mode 100755 index 0000000..0a8edf5 --- /dev/null +++ b/scripts/booth.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# +# booth.sh — operate the parking stack on the booth PC (Ubuntu). +# +# Wraps the three compose files (base + a dev/prod override) so the operator runs +# one command instead of a long `docker compose -f … -f … --env-file …` line. +# +# ./scripts/booth.sh up # start the stack (detached) +# ./scripts/booth.sh update # pull newer images + recreate (the "there are +# # new images" case) — see `update` below +# ./scripts/booth.sh down # stop the stack +# ./scripts/booth.sh restart # restart without pulling +# ./scripts/booth.sh status # what's running +# ./scripts/booth.sh logs # follow logs (Ctrl-C to stop) +# ./scripts/booth.sh ps|pull|config|exec … +# +# Environment is PROD by default (the booth runs prod: pull pinned registry images, +# Caddy on :80, fast_alpr). Override with ENV=dev for a local build/dev run: +# ENV=dev ./scripts/booth.sh up +# +# Config comes from an .env file next to the compose files (REGISTRY, TAG, +# JWT_SECRET, …). Copy .env.example → .env and fill it in. See +# wiki/decisions/container-deployment.md. + +set -euo pipefail + +# --- locate the repo (this script lives in /scripts) -------------------- +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_DIR" + +# --- environment selection (prod by default; the booth is prod) --------------- +ENV="${ENV:-prod}" +case "$ENV" in + prod|production) ENV=prod; OVERRIDE="docker-compose.prod.yml" ;; + dev|development) ENV=dev; OVERRIDE="docker-compose.dev.yml" ;; + *) echo "ERROR: ENV must be 'prod' or 'dev' (got '$ENV')." >&2; exit 2 ;; +esac + +BASE="docker-compose.yml" +ENV_FILE="${ENV_FILE:-.env}" + +# --- colours (only when attached to a terminal) ------------------------------- +if [ -t 1 ]; then + R="$(printf '\033[31m')"; G="$(printf '\033[32m')"; Y="$(printf '\033[33m')" + B="$(printf '\033[1m')"; N="$(printf '\033[0m')" +else + R=""; G=""; Y=""; B=""; N="" +fi +info() { printf '%s==>%s %s\n' "$B" "$N" "$*"; } +warn() { printf '%s!! %s%s\n' "$Y" "$*" "$N" >&2; } +die() { printf '%sERROR:%s %s\n' "$R" "$N" "$*" >&2; exit 1; } + +usage() { + sed -n '3,30p' "$0" | sed 's/^# \{0,1\}//' + exit "${1:-0}" +} + +# --- preflight (only for commands that actually talk to Docker) --------------- +# Deferred into a function so `help`/usage works with no Docker and no .env. +ENV_ARGS=() +DC=() +preflight() { + command -v docker >/dev/null 2>&1 || die "docker is not installed or not on PATH." + # Prefer the v2 plugin (`docker compose`); fall back to legacy `docker-compose`. + if docker compose version >/dev/null 2>&1; then + DC=(docker compose) + elif command -v docker-compose >/dev/null 2>&1; then + DC=(docker-compose) + else + die "Docker Compose v2 plugin not found ('docker compose'). Install docker-compose-plugin." + fi + + [ -f "$BASE" ] || die "missing $BASE in $REPO_DIR" + [ -f "$OVERRIDE" ] || die "missing $OVERRIDE in $REPO_DIR" + + # An .env is required for prod (JWT_SECRET et al. have no safe default); optional + # for dev (we inject a benign local secret below). Pass --env-file only when it + # exists so dev works without one. + if [ -f "$ENV_FILE" ]; then + ENV_ARGS=(--env-file "$ENV_FILE") + elif [ "$ENV" = "prod" ]; then + die "no $ENV_FILE found. Copy .env.example to $ENV_FILE and set JWT_SECRET/REGISTRY/TAG. (prod has no safe defaults.)" + else + # The BASE compose file makes JWT_SECRET shell-required (${JWT_SECRET:?}), which + # the dev override's service-level default can't satisfy. For a dev run with no + # .env, inject the same benign 32-char local secret the dev override documents so + # `up`/`config` work out of the box. NEVER do this for prod (the die above). + warn "no $ENV_FILE found — injecting the documented local-dev JWT_SECRET (dev only)." + : "${JWT_SECRET:=localdevsecret0123456789abcdef0123}" + export JWT_SECRET + fi +} + +# The assembled compose invocation every subcommand builds on (runs preflight once). +compose() { "${DC[@]}" -f "$BASE" -f "$OVERRIDE" "${ENV_ARGS[@]}" "$@"; } + +# --- subcommands -------------------------------------------------------------- +cmd="${1:-}"; [ "$#" -gt 0 ] && shift || true + +# Help/usage short-circuits before any Docker or .env requirement. +case "$cmd" in ""|-h|--help|help) usage 0 ;; esac + +# Reject an unknown command up front (before preflight) so a typo gets a clear +# "unknown command" rather than a confusing "no .env" from the prod env check. +case "$cmd" in + up|start|update|upgrade|down|stop|restart|pull|status|ps|logs|config|exec) ;; + *) warn "unknown command: $cmd"; usage 1 ;; +esac + +preflight + +case "$cmd" in + up|start) + info "Starting the parking stack ($B$ENV$N) …" + compose up -d "$@" + info "Up. ${G}$(compose ps --services 2>/dev/null | tr '\n' ' ')${N}" + info "Booth UI: prod → http:/// · dev → http://:3000/" + ;; + + update|upgrade) + # The "I know there are new images" path: pull the moving branch tag, then + # recreate only what changed. Compose recreates a service whose image digest + # moved; unchanged services (and the named volumes — the SQLite DB!) are left + # alone. Old image layers are pruned afterwards to reclaim disk. + [ "$ENV" = "prod" ] || warn "update on ENV=$ENV: dev builds locally, so 'pull' may be a no-op. Use 'up --build' to rebuild dev." + info "Pulling newer images for the ${B}$ENV_FILE${N} TAG …" + compose pull + info "Recreating changed services (volumes/DB preserved) …" + compose up -d --remove-orphans + info "Pruning dangling image layers …" + docker image prune -f >/dev/null || true + info "${G}Update complete.${N} Running:" + compose ps + ;; + + down|stop) + info "Stopping the parking stack ($ENV) …" + # NOTE: never pass -v here — that would delete the parking-data volume (the + # signed event ledger). Volumes are intentionally preserved across down/up. + compose down "$@" + ;; + + restart) + info "Restarting (no pull) …" + compose restart "$@" + ;; + + pull) + info "Pulling images only (no recreate) …" + compose pull "$@" + ;; + + status|ps) + compose ps "$@" + ;; + + logs) + # Follow by default; pass a service name to scope, e.g. `logs server`. + compose logs -f --tail=200 "$@" + ;; + + config) + # Render the merged, variable-substituted compose config (debugging). + compose config "$@" + ;; + + exec) + [ "$#" -ge 1 ] || die "usage: $0 exec [cmd…] (e.g. exec server sh)" + compose exec "$@" + ;; +esac