diff --git a/maintenance/bootstrap-v2.sh b/maintenance/bootstrap-v2.sh index 2ac1ab9..9f9ffce 100755 --- a/maintenance/bootstrap-v2.sh +++ b/maintenance/bootstrap-v2.sh @@ -4,20 +4,21 @@ 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" -) +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] $*"; } @@ -29,11 +30,6 @@ Usage: bootstrap-v2.sh check bootstrap-v2.sh repair bootstrap-v2.sh deploy - -Modes: - check - read-only validation, does not change filesystem or deploy services - repair - creates directories, repairs ownership/chmod, then runs preflight - deploy - repair + deploys core services + waits for containers + runs preflight USAGE } @@ -42,24 +38,6 @@ require_file() { [ -f "$path" ] || fail "Required file missing: $path" } -load_env() { - require_file "$ENV_FILE" - - set -a - # shellcheck disable=SC1090 - source "$ENV_FILE" - set +a - - [ -n "${APPFACTORY_UID:-}" ] || fail "APPFACTORY_UID missing in $ENV_FILE" - [ -n "${APPFACTORY_GID:-}" ] || fail "APPFACTORY_GID missing in $ENV_FILE" - [ -n "${APPFACTORY_DOCKER_GID:-}" ] || fail "APPFACTORY_DOCKER_GID missing in $ENV_FILE" - - ok "Environment loaded" - ok "APPFACTORY_UID=$APPFACTORY_UID" - ok "APPFACTORY_GID=$APPFACTORY_GID" - ok "APPFACTORY_DOCKER_GID=$APPFACTORY_DOCKER_GID" -} - check_mode() { case "$MODE" in check|repair|deploy) @@ -72,128 +50,127 @@ check_mode() { esac } +load_env() { + require_file "$ENV_FILE" + + set -a + # shellcheck disable=SC1090 + source "$ENV_FILE" + set +a + + [ -n "${APPFACTORY_UID:-}" ] || fail "APPFACTORY_UID missing" + [ -n "${APPFACTORY_GID:-}" ] || fail "APPFACTORY_GID missing" + [ -n "${APPFACTORY_DOCKER_GID:-}" ] || fail "APPFACTORY_DOCKER_GID missing" + + ok "Environment loaded" + ok "APPFACTORY_UID=$APPFACTORY_UID" + ok "APPFACTORY_GID=$APPFACTORY_GID" + ok "APPFACTORY_DOCKER_GID=$APPFACTORY_DOCKER_GID" +} + check_docker() { - command -v docker >/dev/null 2>&1 || fail "docker command missing" - + command -v docker >/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 not available" + docker compose version >/dev/null 2>&1 || fail "docker compose missing" - [ -S /var/run/docker.sock ] || fail "Docker socket missing: /var/run/docker.sock" + [ -S /var/run/docker.sock ] || fail "Docker socket missing" - 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 + [ "$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" } -check_directories_exist() { +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 } -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 +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 for ownership check: $path" + [ -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 ($actual_uid:$actual_gid)" + ok "Ownership OK: $path" else - fail "Ownership mismatch: $path is $actual_uid:$actual_gid, expected $APPFACTORY_UID:$APPFACTORY_GID" + 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 + find "$APPFACTORY_ROOT/workspace/appfactory-tools" -type f -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true + ok "Scripts executable" } -deploy_core_services() { - require_file "$DEPLOY_CORE_SCRIPT" +build_image() { + local image="$1" + local dir="$2" - for service in "${CORE_SERVICES[@]}"; do - log "Deploying core service: $service" + [ -d "$dir" ] || fail "Missing build directory: $dir" + [ -f "$dir/Dockerfile" ] || fail "Missing Dockerfile: $dir/Dockerfile" - if "$DEPLOY_CORE_SCRIPT" "$service"; then - ok "Deploy requested: $service" - else - fail "Deploy failed for core service: $service" - fi - done + 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() { - 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 + for name in "${CORE_CONTAINERS[@]}"; do log "Waiting for container: $name" while true; do @@ -208,7 +185,7 @@ wait_for_containers() { fi if [ "$SECONDS" -ge "$deadline" ]; then - docker ps --format "table {{.Names}}\t{{.Status}}" || true + docker ps -a fail "Timeout waiting for container: $name" fi @@ -220,8 +197,6 @@ wait_for_containers() { run_preflight() { require_file "$PREFLIGHT_SCRIPT" chmod +x "$PREFLIGHT_SCRIPT" - - log "Running preflight" "$PREFLIGHT_SCRIPT" } @@ -233,10 +208,9 @@ main() { check_mode load_env check_docker - check_required_scripts if [ "$MODE" = "check" ]; then - check_directories_exist + check_directories check_ownership run_preflight echo @@ -249,14 +223,18 @@ main() { ensure_scripts_executable if [ "$MODE" = "repair" ]; then - run_preflight echo echo "APPFACTORY BOOTSTRAP REPAIR: READY" exit 0 fi - deploy_core_services + deploy_core_stack wait_for_containers + + if [ -f "$GENERATE_CADDY" ]; then + "$GENERATE_CADDY" + fi + run_preflight echo diff --git a/scripts/restore-appfactory.sh b/scripts/restore-appfactory.sh index 7b95828..0ca7cae 100755 --- a/scripts/restore-appfactory.sh +++ b/scripts/restore-appfactory.sh @@ -2,89 +2,62 @@ set -euo pipefail APPFACTORY_ROOT="/opt/appfactory" -BACKUP_FILE="${1:-}" +BACKUP_FILE_INPUT="${1:-}" TARGET_USER="${2:-${SUDO_USER:-$USER}}" -fail() { - echo "[restore][FAIL] $*" >&2 - exit 1 -} - -ok() { - echo "[restore][OK] $*" -} - -log() { - echo "[restore] $*" -} +log(){ echo "[restore] $*"; } +ok(){ echo "[restore][OK] $*"; } +warn(){ echo "[restore][WARN] $*"; } +fail(){ echo "[restore][FAIL] $*" >&2; exit 1; } usage() { cat </dev/null 2>&1; then - fail "Target user does not exist: $TARGET_USER" -fi +[ -n "$BACKUP_FILE_INPUT" ] || { usage; fail "Backup file argument is required"; } +[ "$(id -u)" -eq 0 ] || fail "Run as root via sudo" +[ -f "$BACKUP_FILE_INPUT" ] || fail "Backup file not found: $BACKUP_FILE_INPUT" +id "$TARGET_USER" >/dev/null 2>&1 || fail "Target user does not exist: $TARGET_USER" +command -v realpath >/dev/null 2>&1 || fail "realpath missing" command -v tar >/dev/null 2>&1 || fail "tar missing" command -v docker >/dev/null 2>&1 || fail "docker missing" +BACKUP_FILE="$(realpath "$BACKUP_FILE_INPUT")" + APPFACTORY_UID="$(id -u "$TARGET_USER")" APPFACTORY_GID="$(id -g "$TARGET_USER")" [ -S /var/run/docker.sock ] || fail "Docker socket missing: /var/run/docker.sock" - APPFACTORY_DOCKER_GID="$(stat -c '%g' /var/run/docker.sock)" +docker ps >/dev/null 2>&1 || fail "docker ps failed. Is $TARGET_USER in docker group and was session restarted?" + log "Backup: $BACKUP_FILE" log "Target user: $TARGET_USER" log "APPFACTORY_UID=$APPFACTORY_UID" log "APPFACTORY_GID=$APPFACTORY_GID" log "APPFACTORY_DOCKER_GID=$APPFACTORY_DOCKER_GID" -log "Stopping existing AppFactory containers if present" - +log "Removing existing AppFactory containers if present" mapfile -t APPFACTORY_CONTAINERS < <( docker ps -a --format '{{.Names}}' \ | grep -E '^(appfactory-|.*-service$|test-|microsoft-365-service)' \ || true ) -if [ "${#APPFACTORY_CONTAINERS[@]}" -eq 0 ]; then - ok "No existing AppFactory containers found" -else - for container in "${APPFACTORY_CONTAINERS[@]}"; do - [ -n "$container" ] || continue - docker rm -f "$container" >/dev/null 2>&1 || true - ok "Removed container: $container" - done -fi +for container in "${APPFACTORY_CONTAINERS[@]}"; do + [ -n "$container" ] || continue + docker rm -f "$container" >/dev/null 2>&1 || true + ok "Removed container: $container" +done log "Removing existing AppFactory root" rm -rf "$APPFACTORY_ROOT" -log "Recreating AppFactory root" -mkdir -p "$APPFACTORY_ROOT" - log "Extracting backup" cd / tar -xzf "$BACKUP_FILE" @@ -105,11 +78,8 @@ mkdir -p \ "$APPFACTORY_ROOT/workspace" ENV_FILE="$APPFACTORY_ROOT/config/appfactory.env" - [ -f "$ENV_FILE" ] || fail "Missing env file after restore: $ENV_FILE" -log "Updating runtime ids in appfactory.env" - set_env_value() { local key="$1" local value="$2" @@ -121,6 +91,10 @@ set_env_value() { fi } +log "Updating appfactory.env for target server" +set_env_value "APPFACTORY_DIR" "$APPFACTORY_ROOT" +set_env_value "WORKSPACE_DIR" "$APPFACTORY_ROOT/workspace" +set_env_value "BACKUP_DIR" "$APPFACTORY_ROOT/backups" set_env_value "APPFACTORY_UID" "$APPFACTORY_UID" set_env_value "APPFACTORY_GID" "$APPFACTORY_GID" set_env_value "APPFACTORY_DOCKER_GID" "$APPFACTORY_DOCKER_GID" @@ -134,32 +108,88 @@ grep -q '^GOOGLE_CLIENT_ID=' "$ENV_FILE" || echo 'GOOGLE_CLIENT_ID=' >> "$ENV_FI grep -q '^GOOGLE_CLIENT_SECRET=' "$ENV_FILE" || echo 'GOOGLE_CLIENT_SECRET=' >> "$ENV_FILE" grep -q '^GOOGLE_REDIRECT_URI=' "$ENV_FILE" || echo 'GOOGLE_REDIRECT_URI=' >> "$ENV_FILE" +DB_FILE="$APPFACTORY_ROOT/data/appfactory/appfactory.db" +[ -f "$DB_FILE" ] || fail "Missing AppFactory DB after restore: $DB_FILE" + +COMPOSE_FILE="$APPFACTORY_ROOT/deploy/docker-compose.yml" +[ -f "$COMPOSE_FILE" ] || fail "Missing compose file: $COMPOSE_FILE" + log "Fixing ownership" chown -R "$APPFACTORY_UID:$APPFACTORY_GID" "$APPFACTORY_ROOT" log "Fixing script permissions" find "$APPFACTORY_ROOT/workspace/appfactory-tools" -type f -name '*.sh' -exec chmod +x {} \; 2>/dev/null || true -BOOTSTRAP="$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance/bootstrap-v2.sh" -PREFLIGHT="$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance/preflight-check.sh" +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 from $dir" + sudo -u "$TARGET_USER" docker build -t "$image:latest" "$dir" +} + GENERATE_CADDY="$APPFACTORY_ROOT/workspace/appfactory-tools/scripts/generate-caddyfile.sh" - -[ -f "$BOOTSTRAP" ] || fail "Missing bootstrap script: $BOOTSTRAP" -[ -f "$PREFLIGHT" ] || fail "Missing preflight script: $PREFLIGHT" - -chmod +x "$BOOTSTRAP" "$PREFLIGHT" -[ -f "$GENERATE_CADDY" ] && chmod +x "$GENERATE_CADDY" - -log "Running bootstrap repair" -sudo -u "$TARGET_USER" "$BOOTSTRAP" repair - if [ -f "$GENERATE_CADDY" ]; then - log "Generating Caddyfile" + log "Generating Caddyfile before starting Caddy" sudo -u "$TARGET_USER" "$GENERATE_CADDY" || true fi -log "Running bootstrap deploy" -sudo -u "$TARGET_USER" "$BOOTSTRAP" deploy +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 core compose stack" +sudo -u "$TARGET_USER" docker compose \ + --env-file "$ENV_FILE" \ + -f "$COMPOSE_FILE" \ + up -d --force-recreate + +log "Waiting for core containers" +CORE_CONTAINERS=( + appfactory-caddy + appfactory-gitea + appfactory-registry + appfactory-portal + appfactory-worker + appfactory-monitor + appfactory-webhook +) + +deadline=$((SECONDS + 180)) + +for container in "${CORE_CONTAINERS[@]}"; do + while true; do + if docker inspect "$container" >/dev/null 2>&1; then + state="$(docker inspect -f '{{.State.Status}}' "$container" 2>/dev/null || true)" + health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container" 2>/dev/null || true)" + + if [ "$state" = "running" ] && [ "$health" != "unhealthy" ]; then + ok "Container ready: $container health=$health" + break + fi + fi + + if [ "$SECONDS" -ge "$deadline" ]; then + docker ps -a + fail "Timeout waiting for container: $container" + fi + + sleep 2 + done +done + +if [ -f "$GENERATE_CADDY" ]; then + log "Regenerating/reloading Caddyfile" + sudo -u "$TARGET_USER" "$GENERATE_CADDY" +fi + +PREFLIGHT="$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance/preflight-check.sh" +[ -f "$PREFLIGHT" ] || fail "Missing preflight script: $PREFLIGHT" +chmod +x "$PREFLIGHT" log "Running final preflight" sudo -u "$TARGET_USER" "$PREFLIGHT"