#!/usr/bin/env bash set -euo pipefail APPFACTORY_ROOT="/opt/appfactory" ENV_FILE="$APPFACTORY_ROOT/config/appfactory.env" PREFLIGHT_SCRIPT="$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance/preflight-check.sh" GENERATE_CADDY="$APPFACTORY_ROOT/workspace/appfactory-tools/scripts/generate-caddyfile.sh" COMPOSE_FILE="$APPFACTORY_ROOT/deploy/docker-compose.yml" MODE="${1:-}" CORE_CONTAINERS=( "appfactory-caddy" "appfactory-gitea" "appfactory-registry" "appfactory-portal" "appfactory-worker" "appfactory-monitor" "appfactory-webhook" ) log(){ echo "[bootstrap-v2] $*"; } ok(){ echo "[bootstrap-v2][OK] $*"; } warn(){ echo "[bootstrap-v2][WARN] $*"; } fail(){ echo "[bootstrap-v2][FAIL] $*" >&2; exit 1; } usage() { cat </dev/null 2>&1 || fail "docker missing" docker version >/dev/null 2>&1 || fail "docker daemon not available" docker compose version >/dev/null 2>&1 || fail "docker compose missing" [ -S /var/run/docker.sock ] || fail "Docker socket missing" sock_gid="$(stat -c '%g' /var/run/docker.sock)" [ "$sock_gid" = "$APPFACTORY_DOCKER_GID" ] || fail "Docker socket GID mismatch. env=$APPFACTORY_DOCKER_GID sock=$sock_gid" docker ps >/dev/null 2>&1 || fail "docker ps failed" ok "Docker ready" } ensure_directories() { mkdir -p \ "$APPFACTORY_ROOT/apps" \ "$APPFACTORY_ROOT/backups" \ "$APPFACTORY_ROOT/config" \ "$APPFACTORY_ROOT/data" \ "$APPFACTORY_ROOT/deploy" \ "$APPFACTORY_ROOT/gateway" \ "$APPFACTORY_ROOT/services" \ "$APPFACTORY_ROOT/templates" \ "$APPFACTORY_ROOT/tools" \ "$APPFACTORY_ROOT/workspace" ok "Directories ready" } check_directories() { for dir in apps backups config data deploy gateway services templates tools workspace; do path="$APPFACTORY_ROOT/$dir" [ -d "$path" ] && ok "Directory exists: $path" || fail "Directory missing: $path" done } repair_ownership() { log "Repairing ownership" chown -R "$APPFACTORY_UID:$APPFACTORY_GID" "$APPFACTORY_ROOT" ok "Ownership repaired" } check_ownership() { for path in "$APPFACTORY_ROOT" "$APPFACTORY_ROOT/data" "$APPFACTORY_ROOT/workspace" "$APPFACTORY_ROOT/backups"; do [ -e "$path" ] || fail "Path missing: $path" actual_uid="$(stat -c '%u' "$path")" actual_gid="$(stat -c '%g' "$path")" if [ "$actual_uid" = "$APPFACTORY_UID" ] && [ "$actual_gid" = "$APPFACTORY_GID" ]; then ok "Ownership OK: $path" else fail "Ownership mismatch: $path is $actual_uid:$actual_gid expected $APPFACTORY_UID:$APPFACTORY_GID" fi done } ensure_scripts_executable() { find "$APPFACTORY_ROOT/workspace/appfactory-tools" -type f -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true ok "Scripts executable" } build_image() { local image="$1" local dir="$2" [ -d "$dir" ] || fail "Missing build directory: $dir" [ -f "$dir/Dockerfile" ] || fail "Missing Dockerfile: $dir/Dockerfile" log "Building image: $image" docker build -t "$image:latest" "$dir" } deploy_core_stack() { require_file "$COMPOSE_FILE" [ -f "$APPFACTORY_ROOT/data/appfactory/appfactory.db" ] || fail "Missing AppFactory DB" if [ -f "$GENERATE_CADDY" ]; then chmod +x "$GENERATE_CADDY" "$GENERATE_CADDY" || true fi build_image "appfactory-portal" "$APPFACTORY_ROOT/workspace/appfactory-portal" build_image "appfactory-worker" "$APPFACTORY_ROOT/workspace/appfactory-worker" build_image "appfactory-monitor" "$APPFACTORY_ROOT/workspace/appfactory-monitor" build_image "appfactory-webhook" "$APPFACTORY_ROOT/workspace/appfactory-webhook" log "Deploying compose stack" docker compose \ --env-file "$ENV_FILE" \ -f "$COMPOSE_FILE" \ up -d --force-recreate ok "Core compose stack deployed" } wait_for_containers() { deadline=$((SECONDS + 180)) for name in "${CORE_CONTAINERS[@]}"; do log "Waiting for container: $name" while true; do if docker inspect "$name" >/dev/null 2>&1; then state="$(docker inspect -f '{{.State.Status}}' "$name" 2>/dev/null || true)" health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$name" 2>/dev/null || true)" if [ "$state" = "running" ] && [ "$health" != "unhealthy" ]; then ok "Container ready: $name health=$health" break fi fi if [ "$SECONDS" -ge "$deadline" ]; then docker ps -a fail "Timeout waiting for container: $name" fi sleep 2 done done } run_preflight() { require_file "$PREFLIGHT_SCRIPT" chmod +x "$PREFLIGHT_SCRIPT" "$PREFLIGHT_SCRIPT" } main() { echo "=================================" echo "APPFACTORY BOOTSTRAP V2" echo "=================================" check_mode load_env check_docker if [ "$MODE" = "check" ]; then check_directories check_ownership run_preflight echo echo "APPFACTORY BOOTSTRAP CHECK: READY" exit 0 fi ensure_directories repair_ownership ensure_scripts_executable if [ "$MODE" = "repair" ]; then echo echo "APPFACTORY BOOTSTRAP REPAIR: READY" exit 0 fi deploy_core_stack wait_for_containers if [ -f "$GENERATE_CADDY" ]; then "$GENERATE_CADDY" fi run_preflight echo echo "APPFACTORY BOOTSTRAP DEPLOY: READY" } main "$@"