Add bootstrap v2, preflight readiness and migration tooling
This commit is contained in:
Executable
+266
@@ -0,0 +1,266 @@
|
|||||||
|
#!/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 "$@"
|
||||||
Executable
+501
@@ -0,0 +1,501 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
APPFACTORY_ROOT="/opt/appfactory"
|
||||||
|
ENV_FILE="$APPFACTORY_ROOT/config/appfactory.env"
|
||||||
|
|
||||||
|
BACKUP_WARN_HOURS="${BACKUP_WARN_HOURS:-24}"
|
||||||
|
BACKUP_FAIL_HOURS="${BACKUP_FAIL_HOURS:-72}"
|
||||||
|
|
||||||
|
DISK_WARN_PERCENT="${DISK_WARN_PERCENT:-80}"
|
||||||
|
DISK_FAIL_PERCENT="${DISK_FAIL_PERCENT:-90}"
|
||||||
|
|
||||||
|
OK_COUNT=0
|
||||||
|
WARN_COUNT=0
|
||||||
|
FAIL_COUNT=0
|
||||||
|
|
||||||
|
ok(){ OK_COUNT=$((OK_COUNT+1)); echo "[OK] $1"; }
|
||||||
|
warn(){ WARN_COUNT=$((WARN_COUNT+1)); echo "[WARN] $1"; }
|
||||||
|
fail(){ FAIL_COUNT=$((FAIL_COUNT+1)); echo "[FAIL] $1"; }
|
||||||
|
section(){ echo; echo "== $1 =="; }
|
||||||
|
|
||||||
|
check_command() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 && ok "Command available: $1" || fail "Command missing: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_env() {
|
||||||
|
section "Environment"
|
||||||
|
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
fail "Env file missing: $ENV_FILE"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "Env file exists: $ENV_FILE"
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$ENV_FILE"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
for key in APPFACTORY_UID APPFACTORY_GID APPFACTORY_DOCKER_GID; do
|
||||||
|
[ -n "${!key:-}" ] && ok "$key=${!key}" || fail "$key is missing"
|
||||||
|
done
|
||||||
|
|
||||||
|
for key in APPFACTORY_DOMAIN APPFACTORY_PORTAL_DOMAIN APPFACTORY_GITEA_DOMAIN APPFACTORY_REGISTRY_DOMAIN; do
|
||||||
|
[ -n "${!key:-}" ] && ok "$key=${!key}" || warn "$key is empty"
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -n "${APPFACTORY_ENABLE_HTTPS:-}" ] && ok "APPFACTORY_ENABLE_HTTPS=$APPFACTORY_ENABLE_HTTPS" || warn "APPFACTORY_ENABLE_HTTPS is missing"
|
||||||
|
|
||||||
|
for key in SMTP_HOST SMTP_PORT SMTP_USERNAME SMTP_FROM SMTP_PASSWORD; do
|
||||||
|
[ -n "${!key:-}" ] && ok "$key configured" || warn "$key is empty"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "${GOOGLE_CLIENT_ID:-}" ] && [ -n "${GOOGLE_CLIENT_SECRET:-}" ]; then
|
||||||
|
ok "Google OAuth configured"
|
||||||
|
else
|
||||||
|
warn "Google OAuth not configured"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_directories() {
|
||||||
|
section "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
|
||||||
|
}
|
||||||
|
|
||||||
|
check_ownership() {
|
||||||
|
section "Ownership"
|
||||||
|
|
||||||
|
uid="${APPFACTORY_UID:-}"
|
||||||
|
gid="${APPFACTORY_GID:-}"
|
||||||
|
|
||||||
|
if [ -z "$uid" ] || [ -z "$gid" ]; then
|
||||||
|
fail "Cannot check ownership because APPFACTORY_UID/GID is missing"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
for path in "$APPFACTORY_ROOT" "$APPFACTORY_ROOT/data" "$APPFACTORY_ROOT/workspace" "$APPFACTORY_ROOT/backups"; do
|
||||||
|
if [ ! -e "$path" ]; then
|
||||||
|
fail "Path missing for ownership check: $path"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
actual_uid="$(stat -c '%u' "$path")"
|
||||||
|
actual_gid="$(stat -c '%g' "$path")"
|
||||||
|
|
||||||
|
if [ "$actual_uid" = "$uid" ] && [ "$actual_gid" = "$gid" ]; then
|
||||||
|
ok "Ownership OK: $path ($actual_uid:$actual_gid)"
|
||||||
|
else
|
||||||
|
fail "Ownership mismatch: $path is $actual_uid:$actual_gid, expected $uid:$gid"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
check_docker() {
|
||||||
|
section "Docker"
|
||||||
|
|
||||||
|
check_command docker
|
||||||
|
|
||||||
|
docker compose version >/dev/null 2>&1 && ok "docker compose available" || fail "docker compose not available"
|
||||||
|
|
||||||
|
[ -S /var/run/docker.sock ] && ok "Docker socket exists" || fail "Docker socket missing"
|
||||||
|
|
||||||
|
if getent group docker >/dev/null 2>&1; then
|
||||||
|
docker_group_gid="$(getent group docker | cut -d: -f3)"
|
||||||
|
ok "Docker group exists in current runtime with gid=$docker_group_gid"
|
||||||
|
else
|
||||||
|
warn "Docker group does not exist in current runtime"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -S /var/run/docker.sock ]; then
|
||||||
|
docker_sock_gid="$(stat -c '%g' /var/run/docker.sock)"
|
||||||
|
ok "Docker socket gid=$docker_sock_gid"
|
||||||
|
|
||||||
|
if [ -n "${APPFACTORY_DOCKER_GID:-}" ] && [ "$docker_sock_gid" = "$APPFACTORY_DOCKER_GID" ]; then
|
||||||
|
ok "APPFACTORY_DOCKER_GID matches docker socket gid"
|
||||||
|
else
|
||||||
|
fail "APPFACTORY_DOCKER_GID mismatch. env=${APPFACTORY_DOCKER_GID:-empty}, docker_sock_gid=$docker_sock_gid"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker ps >/dev/null 2>&1 && ok "docker ps works from host/runtime" || fail "docker ps failed from host/runtime"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_containers() {
|
||||||
|
section "Containers"
|
||||||
|
|
||||||
|
for name in appfactory-portal appfactory-worker appfactory-monitor appfactory-webhook appfactory-gitea appfactory-registry appfactory-caddy; do
|
||||||
|
if ! docker inspect "$name" >/dev/null 2>&1; then
|
||||||
|
warn "Container missing: $name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
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" ]; then
|
||||||
|
fail "Container not running: $name status=$state"
|
||||||
|
elif [ "$health" = "unhealthy" ]; then
|
||||||
|
fail "Container unhealthy: $name"
|
||||||
|
else
|
||||||
|
ok "Container running: $name health=$health"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
check_container_docker_access() {
|
||||||
|
section "Container Docker Access"
|
||||||
|
|
||||||
|
for name in appfactory-portal appfactory-worker appfactory-webhook; do
|
||||||
|
if ! docker inspect "$name" >/dev/null 2>&1; then
|
||||||
|
warn "Skipping docker access check, container missing: $name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker exec "$name" docker ps >/dev/null 2>&1; then
|
||||||
|
ok "Docker access from container works: $name"
|
||||||
|
else
|
||||||
|
fail "Docker access from container failed: $name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
find_portal_db() {
|
||||||
|
candidates="$(find "$APPFACTORY_ROOT" -maxdepth 7 -type f \( -name '*.db' -o -name '*.sqlite' -o -name '*.sqlite3' \) 2>/dev/null || true)"
|
||||||
|
|
||||||
|
for db in $candidates; do
|
||||||
|
has_apps="$(sqlite3 "$db" "select name from sqlite_master where type='table' and name='apps';" 2>/dev/null || true)"
|
||||||
|
has_jobs="$(sqlite3 "$db" "select name from sqlite_master where type='table' and name='jobs';" 2>/dev/null || true)"
|
||||||
|
has_templates="$(sqlite3 "$db" "select name from sqlite_master where type='table' and name='app_templates';" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [ "$has_apps" = "apps" ] && [ "$has_jobs" = "jobs" ] && [ "$has_templates" = "app_templates" ]; then
|
||||||
|
echo "$db"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_sqlite() {
|
||||||
|
section "SQLite"
|
||||||
|
|
||||||
|
check_command sqlite3
|
||||||
|
|
||||||
|
db_path="$(find_portal_db || true)"
|
||||||
|
|
||||||
|
if [ -z "$db_path" ]; then
|
||||||
|
fail "Portal SQLite DB not found. No DB contains apps + jobs + app_templates."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "Portal SQLite DB found: $db_path"
|
||||||
|
|
||||||
|
[ -r "$db_path" ] && ok "DB readable" || fail "DB not readable: $db_path"
|
||||||
|
[ -w "$db_path" ] && ok "DB writable" || fail "DB not writable: $db_path"
|
||||||
|
|
||||||
|
for table in apps app_templates jobs scheduled_scripts alert_rules alert_events; do
|
||||||
|
exists="$(sqlite3 "$db_path" "select name from sqlite_master where type='table' and name='$table';" 2>/dev/null || true)"
|
||||||
|
[ "$exists" = "$table" ] && ok "Table exists: $table" || fail "Table missing: $table"
|
||||||
|
done
|
||||||
|
|
||||||
|
template_count="$(sqlite3 "$db_path" "select count(*) from app_templates where is_enabled=1;" 2>/dev/null || echo 0)"
|
||||||
|
if [ "$template_count" -gt 0 ] 2>/dev/null; then
|
||||||
|
ok "Enabled app templates: $template_count"
|
||||||
|
sqlite3 "$db_path" "select '- ' || coalesce(name, template) || ' [' || coalesce(template, '') || ']' from app_templates where is_enabled=1 order by 1;" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
fail "No enabled app templates found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_alerting() {
|
||||||
|
section "Alerting"
|
||||||
|
|
||||||
|
alerts_dir="$APPFACTORY_ROOT/workspace/appfactory-tools/alerts"
|
||||||
|
|
||||||
|
[ -d "$alerts_dir" ] && ok "Alert scripts directory exists: $alerts_dir" || { fail "Alert scripts directory missing: $alerts_dir"; return; }
|
||||||
|
|
||||||
|
count="$(find "$alerts_dir" -maxdepth 1 -type f -name '*.sh' | wc -l | tr -d ' ')"
|
||||||
|
if [ "$count" -gt 0 ]; then
|
||||||
|
ok "Alert scripts found: $count"
|
||||||
|
find "$alerts_dir" -maxdepth 1 -type f -name '*.sh' -printf '%f\n' | sort | sed 's/^/- /'
|
||||||
|
else
|
||||||
|
warn "No alert scripts found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_scheduler() {
|
||||||
|
section "Scheduler"
|
||||||
|
|
||||||
|
maintenance_dir="$APPFACTORY_ROOT/workspace/appfactory-tools/maintenance"
|
||||||
|
|
||||||
|
[ -d "$maintenance_dir" ] && ok "Maintenance scripts directory exists: $maintenance_dir" || { fail "Maintenance scripts directory missing: $maintenance_dir"; return; }
|
||||||
|
|
||||||
|
count="$(find "$maintenance_dir" -maxdepth 1 -type f -name '*.sh' | wc -l | tr -d ' ')"
|
||||||
|
if [ "$count" -gt 0 ]; then
|
||||||
|
ok "Maintenance scripts found: $count"
|
||||||
|
find "$maintenance_dir" -maxdepth 1 -type f -name '*.sh' -printf '%f\n' | sort | sed 's/^/- /'
|
||||||
|
else
|
||||||
|
warn "No maintenance scripts found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_disk_space() {
|
||||||
|
section "Disk Space"
|
||||||
|
|
||||||
|
for path in "$APPFACTORY_ROOT" "$APPFACTORY_ROOT/data" "$APPFACTORY_ROOT/backups"; do
|
||||||
|
if [ ! -d "$path" ]; then
|
||||||
|
fail "Cannot check disk space, path missing: $path"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
used_percent="$(df -P "$path" | awk 'NR==2 {gsub("%","",$5); print $5}')"
|
||||||
|
available="$(df -hP "$path" | awk 'NR==2 {print $4}')"
|
||||||
|
mountpoint="$(df -P "$path" | awk 'NR==2 {print $6}')"
|
||||||
|
|
||||||
|
if [ -z "$used_percent" ]; then
|
||||||
|
warn "Could not read disk usage for $path"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$used_percent" -ge "$DISK_FAIL_PERCENT" ] 2>/dev/null; then
|
||||||
|
fail "Disk usage critical for $path: ${used_percent}% used, available=$available, mount=$mountpoint"
|
||||||
|
elif [ "$used_percent" -ge "$DISK_WARN_PERCENT" ] 2>/dev/null; then
|
||||||
|
warn "Disk usage high for $path: ${used_percent}% used, available=$available, mount=$mountpoint"
|
||||||
|
else
|
||||||
|
ok "Disk usage OK for $path: ${used_percent}% used, available=$available, mount=$mountpoint"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
check_backup_freshness() {
|
||||||
|
section "Backup Freshness"
|
||||||
|
|
||||||
|
backup_dir="$APPFACTORY_ROOT/backups"
|
||||||
|
|
||||||
|
if [ ! -d "$backup_dir" ]; then
|
||||||
|
fail "Backup directory missing: $backup_dir"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
latest_backup="$(find "$backup_dir" -type f \
|
||||||
|
\( -name '*.tar' -o -name '*.tar.gz' -o -name '*.tgz' -o -name '*.zip' -o -name '*.db' -o -name '*.sqlite' -o -name '*.sqlite3' \) \
|
||||||
|
-printf '%T@ %p\n' 2>/dev/null | sort -nr | head -n 1 | cut -d' ' -f2- || true)"
|
||||||
|
|
||||||
|
if [ -z "$latest_backup" ]; then
|
||||||
|
warn "No backup files found in $backup_dir"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
now_epoch="$(date +%s)"
|
||||||
|
backup_epoch="$(stat -c '%Y' "$latest_backup")"
|
||||||
|
age_seconds=$((now_epoch - backup_epoch))
|
||||||
|
age_hours=$((age_seconds / 3600))
|
||||||
|
|
||||||
|
backup_size="$(du -h "$latest_backup" 2>/dev/null | awk '{print $1}')"
|
||||||
|
backup_time="$(date -d "@$backup_epoch" '+%Y-%m-%d %H:%M:%S %z' 2>/dev/null || date -r "$backup_epoch" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$backup_epoch")"
|
||||||
|
|
||||||
|
if [ "$age_hours" -ge "$BACKUP_FAIL_HOURS" ]; then
|
||||||
|
warn "Latest backup is too old: ${age_hours}h, file=$latest_backup, size=$backup_size, time=$backup_time"
|
||||||
|
elif [ "$age_hours" -ge "$BACKUP_WARN_HOURS" ]; then
|
||||||
|
warn "Latest backup is older than warning threshold: ${age_hours}h, file=$latest_backup, size=$backup_size, time=$backup_time"
|
||||||
|
else
|
||||||
|
ok "Latest backup fresh: ${age_hours}h, file=$latest_backup, size=$backup_size, time=$backup_time"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_gitea_readiness() {
|
||||||
|
section "Gitea Readiness"
|
||||||
|
|
||||||
|
gitea_container="appfactory-gitea"
|
||||||
|
|
||||||
|
if docker inspect "$gitea_container" >/dev/null 2>&1; then
|
||||||
|
state="$(docker inspect -f '{{.State.Status}}' "$gitea_container" 2>/dev/null || true)"
|
||||||
|
[ "$state" = "running" ] && ok "Gitea container running" || fail "Gitea container not running: $state"
|
||||||
|
else
|
||||||
|
fail "Gitea container missing: $gitea_container"
|
||||||
|
fi
|
||||||
|
|
||||||
|
gitea_db_candidates=(
|
||||||
|
"$APPFACTORY_ROOT/data/gitea/gitea/gitea.db"
|
||||||
|
"$APPFACTORY_ROOT/data/gitea/gitea.db"
|
||||||
|
)
|
||||||
|
|
||||||
|
gitea_db=""
|
||||||
|
for candidate in "${gitea_db_candidates[@]}"; do
|
||||||
|
if [ -f "$candidate" ]; then
|
||||||
|
gitea_db="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$gitea_db" ]; then
|
||||||
|
ok "Gitea DB exists: $gitea_db"
|
||||||
|
[ -r "$gitea_db" ] && ok "Gitea DB readable" || fail "Gitea DB not readable: $gitea_db"
|
||||||
|
[ -w "$gitea_db" ] && ok "Gitea DB writable" || warn "Gitea DB not writable from current runtime: $gitea_db"
|
||||||
|
|
||||||
|
if command -v sqlite3 >/dev/null 2>&1; then
|
||||||
|
repo_table="$(sqlite3 "$gitea_db" "select name from sqlite_master where type='table' and name='repository';" 2>/dev/null || true)"
|
||||||
|
[ "$repo_table" = "repository" ] && ok "Gitea repository table exists" || warn "Gitea repository table not found"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Gitea DB not found in expected paths"
|
||||||
|
fi
|
||||||
|
|
||||||
|
repo_dirs=(
|
||||||
|
"$APPFACTORY_ROOT/data/gitea/git/repositories"
|
||||||
|
"$APPFACTORY_ROOT/data/gitea/gitea-repositories"
|
||||||
|
"$APPFACTORY_ROOT/data/gitea/repositories"
|
||||||
|
)
|
||||||
|
|
||||||
|
repo_dir_found=""
|
||||||
|
for candidate in "${repo_dirs[@]}"; do
|
||||||
|
if [ -d "$candidate" ]; then
|
||||||
|
repo_dir_found="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$repo_dir_found" ]; then
|
||||||
|
ok "Gitea repository storage exists: $repo_dir_found"
|
||||||
|
else
|
||||||
|
warn "Gitea repository storage not found in expected paths"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_registry_readiness() {
|
||||||
|
section "Registry Readiness"
|
||||||
|
|
||||||
|
registry_container="appfactory-registry"
|
||||||
|
|
||||||
|
if docker inspect "$registry_container" >/dev/null 2>&1; then
|
||||||
|
state="$(docker inspect -f '{{.State.Status}}' "$registry_container" 2>/dev/null || true)"
|
||||||
|
[ "$state" = "running" ] && ok "Registry container running" || fail "Registry container not running: $state"
|
||||||
|
else
|
||||||
|
fail "Registry container missing: $registry_container"
|
||||||
|
fi
|
||||||
|
|
||||||
|
registry_dirs=(
|
||||||
|
"$APPFACTORY_ROOT/data/registry"
|
||||||
|
"$APPFACTORY_ROOT/data/docker-registry"
|
||||||
|
"$APPFACTORY_ROOT/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
registry_dir_found=""
|
||||||
|
for candidate in "${registry_dirs[@]}"; do
|
||||||
|
if [ -d "$candidate" ]; then
|
||||||
|
registry_dir_found="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$registry_dir_found" ]; then
|
||||||
|
ok "Registry storage exists: $registry_dir_found"
|
||||||
|
[ -r "$registry_dir_found" ] && ok "Registry storage readable" || fail "Registry storage not readable: $registry_dir_found"
|
||||||
|
[ -w "$registry_dir_found" ] && ok "Registry storage writable" || warn "Registry storage not writable from current runtime: $registry_dir_found"
|
||||||
|
else
|
||||||
|
warn "Registry storage not found in expected paths"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_caddy_readiness() {
|
||||||
|
section "Caddy Readiness"
|
||||||
|
|
||||||
|
caddyfile="$APPFACTORY_ROOT/gateway/Caddyfile"
|
||||||
|
|
||||||
|
if [ ! -f "$caddyfile" ]; then
|
||||||
|
fail "Caddyfile missing: $caddyfile"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "Caddyfile exists: $caddyfile"
|
||||||
|
|
||||||
|
if docker inspect appfactory-caddy >/dev/null 2>&1; then
|
||||||
|
state="$(docker inspect -f '{{.State.Status}}' appfactory-caddy 2>/dev/null || true)"
|
||||||
|
[ "$state" = "running" ] && ok "Caddy container running" || fail "Caddy container not running: $state"
|
||||||
|
|
||||||
|
if docker exec appfactory-caddy caddy validate --config /etc/caddy/Caddyfile >/tmp/appfactory-caddy-validate.log 2>&1; then
|
||||||
|
ok "Caddyfile validates inside container"
|
||||||
|
else
|
||||||
|
fail "Caddyfile validation failed"
|
||||||
|
sed 's/^/[caddy] /' /tmp/appfactory-caddy-validate.log || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "Caddy container missing: appfactory-caddy"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${APPFACTORY_ENABLE_HTTPS:-false}" = "true" ]; then
|
||||||
|
if [ -n "${APPFACTORY_PORTAL_DOMAIN:-}" ] || [ -n "${APPFACTORY_GITEA_DOMAIN:-}" ] || [ -n "${APPFACTORY_REGISTRY_DOMAIN:-}" ]; then
|
||||||
|
ok "HTTPS mode requested with at least one domain configured"
|
||||||
|
else
|
||||||
|
fail "HTTPS mode enabled but no domains are configured"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q '^:80[[:space:]]*{' "$caddyfile"; then
|
||||||
|
fail "HTTPS mode enabled but Caddyfile still uses :80 catch-all block"
|
||||||
|
else
|
||||||
|
ok "Caddyfile is not using :80-only mode"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if grep -q '^:80[[:space:]]*{' "$caddyfile"; then
|
||||||
|
ok "HTTP-only Caddyfile mode active"
|
||||||
|
else
|
||||||
|
warn "HTTPS disabled but Caddyfile does not contain :80 catch-all block"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_readiness_flags() {
|
||||||
|
section "Readiness Flags"
|
||||||
|
|
||||||
|
[ "${APPFACTORY_ENABLE_HTTPS:-false}" = "true" ] && ok "HTTPS enabled" || warn "HTTPS disabled"
|
||||||
|
|
||||||
|
[ -n "${APPFACTORY_DOMAIN:-}" ] && ok "Base domain configured: $APPFACTORY_DOMAIN" || warn "Base domain not configured"
|
||||||
|
[ -n "${APPFACTORY_PORTAL_DOMAIN:-}" ] && ok "Portal domain configured: $APPFACTORY_PORTAL_DOMAIN" || warn "Portal domain not configured"
|
||||||
|
[ -n "${APPFACTORY_GITEA_DOMAIN:-}" ] && ok "Gitea domain configured: $APPFACTORY_GITEA_DOMAIN" || warn "Gitea domain not configured"
|
||||||
|
[ -n "${APPFACTORY_REGISTRY_DOMAIN:-}" ] && ok "Registry domain configured: $APPFACTORY_REGISTRY_DOMAIN" || warn "Registry domain not configured"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
echo "APPFACTORY PREFLIGHT CHECK"
|
||||||
|
echo "Root: $APPFACTORY_ROOT"
|
||||||
|
|
||||||
|
load_env
|
||||||
|
check_directories
|
||||||
|
check_ownership
|
||||||
|
check_docker
|
||||||
|
check_containers
|
||||||
|
check_container_docker_access
|
||||||
|
check_sqlite
|
||||||
|
check_alerting
|
||||||
|
check_scheduler
|
||||||
|
check_disk_space
|
||||||
|
check_backup_freshness
|
||||||
|
check_gitea_readiness
|
||||||
|
check_registry_readiness
|
||||||
|
check_caddy_readiness
|
||||||
|
check_readiness_flags
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "== Summary =="
|
||||||
|
echo "OK: $OK_COUNT"
|
||||||
|
echo "WARN: $WARN_COUNT"
|
||||||
|
echo "FAIL: $FAIL_COUNT"
|
||||||
|
|
||||||
|
if [ "$FAIL_COUNT" -eq 0 ]; then
|
||||||
|
echo
|
||||||
|
echo "APPFACTORY PREFLIGHT: READY"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "APPFACTORY PREFLIGHT: NOT READY"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
+123
-24
@@ -2,34 +2,34 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
CONFIG_FILE="/opt/appfactory/config/appfactory.env"
|
CONFIG_FILE="/opt/appfactory/config/appfactory.env"
|
||||||
source "$CONFIG_FILE"
|
|
||||||
|
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
APPFACTORY_DIR="${APPFACTORY_DIR:-/opt/appfactory}"
|
||||||
CATALOG_FILE="$APPFACTORY_DIR/apps/catalog.yml"
|
CATALOG_FILE="$APPFACTORY_DIR/apps/catalog.yml"
|
||||||
CADDY_FILE="$APPFACTORY_DIR/gateway/Caddyfile"
|
CADDY_FILE="$APPFACTORY_DIR/gateway/Caddyfile"
|
||||||
|
|
||||||
|
APPFACTORY_ENABLE_HTTPS="${APPFACTORY_ENABLE_HTTPS:-false}"
|
||||||
|
APPFACTORY_PORTAL_DOMAIN="${APPFACTORY_PORTAL_DOMAIN:-}"
|
||||||
|
APPFACTORY_GITEA_DOMAIN="${APPFACTORY_GITEA_DOMAIN:-}"
|
||||||
|
APPFACTORY_REGISTRY_DOMAIN="${APPFACTORY_REGISTRY_DOMAIN:-}"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$CADDY_FILE")"
|
mkdir -p "$(dirname "$CADDY_FILE")"
|
||||||
|
mkdir -p "$APPFACTORY_DIR/gateway/static"
|
||||||
|
|
||||||
cat > "$CADDY_FILE" <<EOF_CADDY
|
write_app_routes() {
|
||||||
:80 {
|
local target_file="$1"
|
||||||
handle /health {
|
|
||||||
respond "AppFactory OK" 200
|
|
||||||
}
|
|
||||||
|
|
||||||
handle /portal* {
|
if [ ! -f "$CATALOG_FILE" ]; then
|
||||||
uri strip_prefix /portal
|
return 0
|
||||||
reverse_proxy appfactory-portal:9100
|
fi
|
||||||
}
|
|
||||||
|
|
||||||
handle /apps {
|
python3 - "$CATALOG_FILE" "$target_file" <<'PY'
|
||||||
root * /srv/static
|
|
||||||
rewrite * /apps.html
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
EOF_CADDY
|
|
||||||
|
|
||||||
if [ -f "$CATALOG_FILE" ]; then
|
|
||||||
python3 - "$CATALOG_FILE" "$CADDY_FILE" <<'PY'
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
catalog_file = sys.argv[1]
|
catalog_file = sys.argv[1]
|
||||||
@@ -45,7 +45,7 @@ with open(catalog_file, "r", encoding="utf-8") as f:
|
|||||||
if stripped.startswith("- id:"):
|
if stripped.startswith("- id:"):
|
||||||
if current:
|
if current:
|
||||||
apps.append(current)
|
apps.append(current)
|
||||||
current = {"id": stripped.split(":", 1)[1].strip()}
|
current = {"id": stripped.split(":", 1)[1].strip().strip('"')}
|
||||||
elif current and ":" in stripped:
|
elif current and ":" in stripped:
|
||||||
key, value = stripped.split(":", 1)
|
key, value = stripped.split(":", 1)
|
||||||
current[key.strip()] = value.strip().strip('"')
|
current[key.strip()] = value.strip().strip('"')
|
||||||
@@ -65,9 +65,32 @@ with open(caddy_file, "a", encoding="utf-8") as f:
|
|||||||
f.write(f" reverse_proxy {app_id}:{port}\n")
|
f.write(f" reverse_proxy {app_id}:{port}\n")
|
||||||
f.write(" }\n\n")
|
f.write(" }\n\n")
|
||||||
PY
|
PY
|
||||||
fi
|
}
|
||||||
|
|
||||||
cat >> "$CADDY_FILE" <<EOF_CADDY
|
write_common_path_routes() {
|
||||||
|
local target_file="$1"
|
||||||
|
|
||||||
|
cat >> "$target_file" <<'EOF_ROUTES'
|
||||||
|
handle /health {
|
||||||
|
respond "AppFactory OK" 200
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /portal* {
|
||||||
|
uri strip_prefix /portal
|
||||||
|
reverse_proxy appfactory-portal:9100
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /apps {
|
||||||
|
root * /srv/static
|
||||||
|
rewrite * /apps.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
EOF_ROUTES
|
||||||
|
|
||||||
|
write_app_routes "$target_file"
|
||||||
|
|
||||||
|
cat >> "$target_file" <<'EOF_ROUTES'
|
||||||
handle_path /webhook/* {
|
handle_path /webhook/* {
|
||||||
reverse_proxy appfactory-webhook:9000
|
reverse_proxy appfactory-webhook:9000
|
||||||
}
|
}
|
||||||
@@ -79,11 +102,87 @@ cat >> "$CADDY_FILE" <<EOF_CADDY
|
|||||||
handle {
|
handle {
|
||||||
respond "AppFactory gateway is running" 200
|
respond "AppFactory gateway is running" 200
|
||||||
}
|
}
|
||||||
|
EOF_ROUTES
|
||||||
|
}
|
||||||
|
|
||||||
|
write_http_only_caddyfile() {
|
||||||
|
cat > "$CADDY_FILE" <<'EOF_CADDY'
|
||||||
|
:80 {
|
||||||
|
EOF_CADDY
|
||||||
|
|
||||||
|
write_common_path_routes "$CADDY_FILE"
|
||||||
|
|
||||||
|
cat >> "$CADDY_FILE" <<'EOF_CADDY'
|
||||||
}
|
}
|
||||||
EOF_CADDY
|
EOF_CADDY
|
||||||
|
}
|
||||||
|
|
||||||
|
write_domain_caddyfile() {
|
||||||
|
: > "$CADDY_FILE"
|
||||||
|
|
||||||
|
if [ -n "$APPFACTORY_PORTAL_DOMAIN" ]; then
|
||||||
|
cat >> "$CADDY_FILE" <<EOF_CADDY
|
||||||
|
$APPFACTORY_PORTAL_DOMAIN {
|
||||||
|
EOF_CADDY
|
||||||
|
write_common_path_routes "$CADDY_FILE"
|
||||||
|
cat >> "$CADDY_FILE" <<'EOF_CADDY'
|
||||||
|
}
|
||||||
|
|
||||||
|
EOF_CADDY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$APPFACTORY_GITEA_DOMAIN" ]; then
|
||||||
|
cat >> "$CADDY_FILE" <<EOF_CADDY
|
||||||
|
$APPFACTORY_GITEA_DOMAIN {
|
||||||
|
handle /health {
|
||||||
|
respond "Gitea gateway OK" 200
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
reverse_proxy appfactory-gitea:3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EOF_CADDY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$APPFACTORY_REGISTRY_DOMAIN" ]; then
|
||||||
|
cat >> "$CADDY_FILE" <<EOF_CADDY
|
||||||
|
$APPFACTORY_REGISTRY_DOMAIN {
|
||||||
|
handle /v2/* {
|
||||||
|
reverse_proxy appfactory-registry:5000
|
||||||
|
}
|
||||||
|
|
||||||
|
handle /health {
|
||||||
|
respond "Registry gateway OK" 200
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
reverse_proxy appfactory-registry:5000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EOF_CADDY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -s "$CADDY_FILE" ]; then
|
||||||
|
write_http_only_caddyfile
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$APPFACTORY_ENABLE_HTTPS" = "true" ] && {
|
||||||
|
[ -n "$APPFACTORY_PORTAL_DOMAIN" ] || [ -n "$APPFACTORY_GITEA_DOMAIN" ] || [ -n "$APPFACTORY_REGISTRY_DOMAIN" ];
|
||||||
|
}; then
|
||||||
|
write_domain_caddyfile
|
||||||
|
else
|
||||||
|
write_http_only_caddyfile
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Generated: $CADDY_FILE"
|
echo "Generated: $CADDY_FILE"
|
||||||
|
|
||||||
|
if docker inspect appfactory-caddy >/dev/null 2>&1; then
|
||||||
docker exec appfactory-caddy caddy validate --config /etc/caddy/Caddyfile
|
docker exec appfactory-caddy caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
|
||||||
docker exec appfactory-caddy caddy reload --config /etc/caddy/Caddyfile
|
docker exec appfactory-caddy caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
else
|
||||||
|
echo "WARN: appfactory-caddy container not found, skipping validate/reload"
|
||||||
|
fi
|
||||||
|
|||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB_DIR="/opt/appfactory/data/appfactory"
|
||||||
|
DB_FILE="$DB_DIR/appfactory.db"
|
||||||
|
|
||||||
|
mkdir -p "$DB_DIR"
|
||||||
|
|
||||||
|
sqlite3 "$DB_FILE" <<'SQL'
|
||||||
|
PRAGMA journal_mode=WAL;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS apps (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
language TEXT NOT NULL,
|
||||||
|
version TEXT NOT NULL DEFAULT '1.0.0',
|
||||||
|
status TEXT NOT NULL DEFAULT 'created',
|
||||||
|
memory TEXT,
|
||||||
|
cpus TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS deployments (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
app_id TEXT NOT NULL,
|
||||||
|
kind TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
commit_sha TEXT,
|
||||||
|
started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
finished_at TEXT,
|
||||||
|
stdout TEXT,
|
||||||
|
stderr TEXT,
|
||||||
|
returncode INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deployments_app_id
|
||||||
|
ON deployments(app_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_deployments_started_at
|
||||||
|
ON deployments(started_at);
|
||||||
|
SQL
|
||||||
|
|
||||||
|
echo "Database initialized:"
|
||||||
|
echo "$DB_FILE"
|
||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB_FILE="/opt/appfactory/data/appfactory/appfactory.db"
|
||||||
|
CATALOG_FILE="/opt/appfactory/apps/catalog.yml"
|
||||||
|
|
||||||
|
python3 - "$DB_FILE" "$CATALOG_FILE" <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
db_file = Path(sys.argv[1])
|
||||||
|
catalog_file = Path(sys.argv[2])
|
||||||
|
|
||||||
|
if not db_file.exists():
|
||||||
|
raise SystemExit(f"Missing database: {db_file}")
|
||||||
|
|
||||||
|
if not catalog_file.exists():
|
||||||
|
raise SystemExit(f"Missing catalog: {catalog_file}")
|
||||||
|
|
||||||
|
apps = []
|
||||||
|
current = None
|
||||||
|
|
||||||
|
for raw_line in catalog_file.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = raw_line.strip()
|
||||||
|
|
||||||
|
if line.startswith("- id:"):
|
||||||
|
if current:
|
||||||
|
apps.append(current)
|
||||||
|
current = {"id": line.split(":", 1)[1].strip()}
|
||||||
|
elif current and ":" in line:
|
||||||
|
key, value = line.split(":", 1)
|
||||||
|
current[key.strip()] = value.strip().strip('"')
|
||||||
|
|
||||||
|
if current:
|
||||||
|
apps.append(current)
|
||||||
|
|
||||||
|
con = sqlite3.connect(db_file)
|
||||||
|
|
||||||
|
for item in apps:
|
||||||
|
app_id = item.get("id")
|
||||||
|
if not app_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
con.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO apps (
|
||||||
|
id, name, language, version, status, memory, cpus
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
name = excluded.name,
|
||||||
|
language = excluded.language,
|
||||||
|
version = excluded.version,
|
||||||
|
status = excluded.status,
|
||||||
|
memory = excluded.memory,
|
||||||
|
cpus = excluded.cpus,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
app_id,
|
||||||
|
item.get("name") or app_id,
|
||||||
|
item.get("language") or "python",
|
||||||
|
item.get("version") or "1.0.0",
|
||||||
|
item.get("status") or "deployed",
|
||||||
|
item.get("memory") or None,
|
||||||
|
item.get("cpus") or None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
con.commit()
|
||||||
|
|
||||||
|
rows = con.execute(
|
||||||
|
"SELECT id, language, status, COALESCE(memory, ''), COALESCE(cpus, '') FROM apps ORDER BY id"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
print("Migration completed.")
|
||||||
|
for row in rows:
|
||||||
|
print(" | ".join(row))
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
PY
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB_FILE="/opt/appfactory/data/appfactory/appfactory.db"
|
||||||
|
|
||||||
|
python3 - "$DB_FILE" <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
db = sqlite3.connect(sys.argv[1])
|
||||||
|
|
||||||
|
db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS service_health (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
service_id TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
http_status INTEGER,
|
||||||
|
response_time_ms INTEGER,
|
||||||
|
error_text TEXT,
|
||||||
|
checked_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
db.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_service_health_service
|
||||||
|
ON service_health(service_id, checked_at DESC)
|
||||||
|
""")
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
print("Health migration completed")
|
||||||
|
PY
|
||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB_FILE="/opt/appfactory/data/appfactory/appfactory.db"
|
||||||
|
|
||||||
|
if [ ! -f "$DB_FILE" ]; then
|
||||||
|
echo "Missing database: $DB_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "$DB_FILE" <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
db_file = sys.argv[1]
|
||||||
|
con = sqlite3.connect(db_file)
|
||||||
|
|
||||||
|
con.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS jobs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
target_type TEXT NOT NULL,
|
||||||
|
target_id TEXT NOT NULL,
|
||||||
|
payload_json TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'queued',
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
started_at TEXT,
|
||||||
|
finished_at TEXT,
|
||||||
|
created_by_user_id INTEGER,
|
||||||
|
created_by_username TEXT,
|
||||||
|
created_by_display_name TEXT,
|
||||||
|
source TEXT NOT NULL DEFAULT 'system',
|
||||||
|
worker_id TEXT,
|
||||||
|
result_json TEXT,
|
||||||
|
error_text TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
con.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS job_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
job_id INTEGER NOT NULL,
|
||||||
|
stream TEXT NOT NULL DEFAULT 'system',
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
con.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_jobs_status
|
||||||
|
ON jobs(status)
|
||||||
|
""")
|
||||||
|
|
||||||
|
con.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_jobs_target
|
||||||
|
ON jobs(target_type, target_id)
|
||||||
|
""")
|
||||||
|
|
||||||
|
con.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_jobs_created_at
|
||||||
|
ON jobs(created_at)
|
||||||
|
""")
|
||||||
|
|
||||||
|
con.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_job_logs_job_id
|
||||||
|
ON job_logs(job_id)
|
||||||
|
""")
|
||||||
|
|
||||||
|
columns = [row[1] for row in con.execute("PRAGMA table_info(deployments)").fetchall()]
|
||||||
|
if "job_id" not in columns:
|
||||||
|
con.execute("ALTER TABLE deployments ADD COLUMN job_id INTEGER")
|
||||||
|
|
||||||
|
con.commit()
|
||||||
|
|
||||||
|
print("Jobs migration completed.")
|
||||||
|
print("")
|
||||||
|
for row in con.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"):
|
||||||
|
print(f"- {row[0]}")
|
||||||
|
|
||||||
|
con.close()
|
||||||
|
PY
|
||||||
Reference in New Issue
Block a user