#!/bin/bash # Agnes VM startup script — templated by Terraform. # Idempotent — runs on every boot. set -euo pipefail exec > /var/log/agnes-startup.log 2>&1 chmod 640 /var/log/agnes-startup.log # defense in depth — not readable by non-root CUSTOMER_NAME="${customer_name}" IMAGE_REPO="${image_repo}" IMAGE_TAG="${image_tag}" UPGRADE_MODE="${upgrade_mode}" TLS_MODE="${tls_mode}" DOMAIN="${domain}" ACME_EMAIL="${acme_email}" DATA_SOURCE="${data_source}" KEBOOLA_STACK_URL="${keboola_stack_url}" SEED_ADMIN_EMAIL="${seed_admin_email}" ROLE="${role}" COMPOSE_REF="${compose_ref}" echo "=== [Agnes $CUSTOMER_NAME $ROLE] Startup at $(date) ===" # --- 1. Docker (install if missing) --- if ! command -v docker &>/dev/null; then curl -fsSL https://get.docker.com | sh fi if ! docker compose version &>/dev/null; then apt-get update && apt-get install -y docker-compose-plugin fi # --- 2. Persistent data disk mount --- DATA_DEV="/dev/disk/by-id/google-data" DATA_MNT="/data" if [ -b "$DATA_DEV" ]; then if ! blkid "$DATA_DEV" | grep -q ext4; then mkfs.ext4 -F "$DATA_DEV" fi mkdir -p "$DATA_MNT" mountpoint -q "$DATA_MNT" || mount -o discard,defaults "$DATA_DEV" "$DATA_MNT" grep -qF "$DATA_DEV" /etc/fstab || echo "$DATA_DEV $DATA_MNT ext4 discard,defaults,nofail 0 2" >> /etc/fstab mkdir -p "$DATA_MNT/state" "$DATA_MNT/analytics" "$DATA_MNT/extracts" fi # --- 3. App directory + docker-compose files from public repo --- APP_DIR="/opt/agnes" mkdir -p "$APP_DIR" cd "$APP_DIR" # Fetch docker-compose files pinned to $COMPOSE_REF (defaults to `main`; pin to a # stable-YYYY.MM.N tag for reproducibility across VM rebuilds). RAW_BASE="https://raw.githubusercontent.com/keboola/agnes-the-ai-analyst/$${COMPOSE_REF}" curl -fsSL "$${RAW_BASE}/docker-compose.yml" -o docker-compose.yml curl -fsSL "$${RAW_BASE}/docker-compose.prod.yml" -o docker-compose.prod.yml # Overlay which binds `data` volume to host /data (persistent disk mounted above) curl -fsSL "$${RAW_BASE}/docker-compose.host-mount.yml" -o docker-compose.host-mount.yml # TLS overlay (Caddy + Let's Encrypt) — fetch only when actually needed; surface failures if [ "$TLS_MODE" = "caddy" ] && [ -n "$DOMAIN" ]; then curl -fsSL "$${RAW_BASE}/Caddyfile" -o Caddyfile fi # --- 4. Fetch secrets from Secret Manager — fail loudly if missing --- KEBOOLA_TOKEN="" if [ "$DATA_SOURCE" = "keboola" ]; then # No `|| echo ""` fallback — if the token secret is missing, boot should fail # loudly rather than silently start an app that will fail sync cryptically later. KEBOOLA_TOKEN=$(gcloud secrets versions access latest --secret=keboola-storage-token) fi JWT_KEY=$(gcloud secrets versions access latest --secret=agnes-$${CUSTOMER_NAME}-jwt-secret) cat > "$APP_DIR/.env" < /usr/local/bin/agnes-auto-upgrade.sh <<'SCRIPTEOF' #!/bin/bash # Runs from cron — pulls new image if one is available, restarts containers. set -euo pipefail cd /opt/agnes # Source .env so AGNES_TAG reflects any operator edits since boot. # shellcheck disable=SC1091 set -a; . /opt/agnes/.env; set +a IMAGE="ghcr.io/keboola/agnes-the-ai-analyst:$${AGNES_TAG:-stable}" COMPOSE_FILES="-f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.host-mount.yml" BEFORE=$(docker images --no-trunc --format '{{.Digest}}' "$IMAGE" | head -1) docker compose $COMPOSE_FILES pull >/dev/null 2>&1 AFTER=$(docker images --no-trunc --format '{{.Digest}}' "$IMAGE" | head -1) if [ "$BEFORE" != "$AFTER" ]; then echo "$(date): new image digest for $IMAGE — recreating containers" docker compose $COMPOSE_FILES up -d docker image prune -f >/dev/null 2>&1 fi SCRIPTEOF chmod +x /usr/local/bin/agnes-auto-upgrade.sh # Install cron entry idempotently: remove any prior agnes-auto-upgrade line, then append ours. CRON_LINE="*/5 * * * * /usr/local/bin/agnes-auto-upgrade.sh >> /var/log/agnes-auto-upgrade.log 2>&1" (crontab -l 2>/dev/null | grep -v agnes-auto-upgrade || true; echo "$CRON_LINE") | crontab - fi echo "=== [Agnes $CUSTOMER_NAME $ROLE] Startup complete at $(date) ===" docker compose ps