#!/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" DEPLOY_CORE_SCRIPT="$APPFACTORY_ROOT/workspace/appfactory-tools/scripts/deploy-core-service.sh" CORE_SERVICES=( "caddy" "gitea" "registry" "portal" "worker" "monitor" "webhook" ) MODE="${1:-}" 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 command missing" docker version >/dev/null 2>&1 || fail "docker daemon not available" docker compose version >/dev/null 2>&1 || fail "docker compose not available" [ -S /var/run/docker.sock ] || fail "Docker socket missing: /var/run/docker.sock" local sock_gid sock_gid="$(stat -c '%g' /var/run/docker.sock)" if [ "$sock_gid" != "$APPFACTORY_DOCKER_GID" ]; then fail "Docker socket GID mismatch. APPFACTORY_DOCKER_GID=$APPFACTORY_DOCKER_GID, /var/run/docker.sock gid=$sock_gid" fi docker ps >/dev/null 2>&1 || fail "docker ps failed" ok "Docker ready" } check_directories_exist() { 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 } ensure_directories() { for dir in apps backups config data deploy gateway services templates tools workspace; do path="$APPFACTORY_ROOT/$dir" mkdir -p "$path" ok "Directory ready: $path" done } check_required_scripts() { require_file "$PREFLIGHT_SCRIPT" if [ "$MODE" = "deploy" ]; then require_file "$DEPLOY_CORE_SCRIPT" else if [ -f "$DEPLOY_CORE_SCRIPT" ]; then ok "Deploy core script exists: $DEPLOY_CORE_SCRIPT" else warn "Deploy core script missing: $DEPLOY_CORE_SCRIPT" fi fi } check_ownership() { for path in "$APPFACTORY_ROOT" "$APPFACTORY_ROOT/data" "$APPFACTORY_ROOT/workspace" "$APPFACTORY_ROOT/backups"; do [ -e "$path" ] || fail "Path missing for ownership check: $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 ($actual_uid:$actual_gid)" else fail "Ownership mismatch: $path is $actual_uid:$actual_gid, expected $APPFACTORY_UID:$APPFACTORY_GID" fi done } repair_ownership() { log "Repairing ownership for $APPFACTORY_ROOT to $APPFACTORY_UID:$APPFACTORY_GID" chown -R "$APPFACTORY_UID:$APPFACTORY_GID" "$APPFACTORY_ROOT" ok "Ownership repaired" } ensure_scripts_executable() { if [ -d "$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance" ]; then find "$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance" -maxdepth 1 -type f -name "*.sh" -exec chmod +x {} \; ok "Maintenance scripts executable" else warn "Maintenance scripts directory missing" fi if [ -d "$APPFACTORY_ROOT/workspace/appfactory-tools/alerts" ]; then find "$APPFACTORY_ROOT/workspace/appfactory-tools/alerts" -maxdepth 1 -type f -name "*.sh" -exec chmod +x {} \; ok "Alert scripts executable" else warn "Alert scripts directory missing" fi if [ -f "$DEPLOY_CORE_SCRIPT" ]; then chmod +x "$DEPLOY_CORE_SCRIPT" ok "Deploy core script executable" fi } deploy_core_services() { require_file "$DEPLOY_CORE_SCRIPT" for service in "${CORE_SERVICES[@]}"; do log "Deploying core service: $service" if "$DEPLOY_CORE_SCRIPT" "$service"; then ok "Deploy requested: $service" else fail "Deploy failed for core service: $service" fi done } wait_for_containers() { log "Waiting for core containers" local container_names=( "appfactory-caddy" "appfactory-gitea" "appfactory-registry" "appfactory-portal" "appfactory-worker" "appfactory-monitor" "appfactory-webhook" ) local deadline deadline=$((SECONDS + 180)) for name in "${container_names[@]}"; 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 --format "table {{.Names}}\t{{.Status}}" || true fail "Timeout waiting for container: $name" fi sleep 2 done done } run_preflight() { require_file "$PREFLIGHT_SCRIPT" chmod +x "$PREFLIGHT_SCRIPT" log "Running preflight" "$PREFLIGHT_SCRIPT" } main() { echo "=================================" echo "APPFACTORY BOOTSTRAP V2" echo "=================================" check_mode load_env check_docker check_required_scripts if [ "$MODE" = "check" ]; then check_directories_exist check_ownership run_preflight echo echo "APPFACTORY BOOTSTRAP CHECK: READY" exit 0 fi ensure_directories repair_ownership ensure_scripts_executable if [ "$MODE" = "repair" ]; then run_preflight echo echo "APPFACTORY BOOTSTRAP REPAIR: READY" exit 0 fi deploy_core_services wait_for_containers run_preflight echo echo "APPFACTORY BOOTSTRAP DEPLOY: READY" } main "$@"