267 lines
6.7 KiB
Bash
Executable File
267 lines
6.7 KiB
Bash
Executable File
#!/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 <<USAGE
|
|
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
|
|
}
|
|
|
|
require_file() {
|
|
local path="$1"
|
|
[ -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)
|
|
ok "Mode: $MODE"
|
|
;;
|
|
*)
|
|
usage
|
|
fail "Mode is required: check, repair, or deploy"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
check_docker() {
|
|
command -v docker >/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 "$@"
|