Add bootstrap v2, preflight readiness and migration tooling

This commit is contained in:
2026-06-08 08:25:05 +02:00
parent c5c746ff97
commit 817e8ff862
7 changed files with 1133 additions and 26 deletions
+125 -26
View File
@@ -2,34 +2,34 @@
set -euo pipefail
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"
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 "$APPFACTORY_DIR/gateway/static"
cat > "$CADDY_FILE" <<EOF_CADDY
:80 {
handle /health {
respond "AppFactory OK" 200
}
write_app_routes() {
local target_file="$1"
handle /portal* {
uri strip_prefix /portal
reverse_proxy appfactory-portal:9100
}
if [ ! -f "$CATALOG_FILE" ]; then
return 0
fi
handle /apps {
root * /srv/static
rewrite * /apps.html
file_server
}
EOF_CADDY
if [ -f "$CATALOG_FILE" ]; then
python3 - "$CATALOG_FILE" "$CADDY_FILE" <<'PY'
python3 - "$CATALOG_FILE" "$target_file" <<'PY'
import sys
catalog_file = sys.argv[1]
@@ -45,7 +45,7 @@ with open(catalog_file, "r", encoding="utf-8") as f:
if stripped.startswith("- id:"):
if current:
apps.append(current)
current = {"id": stripped.split(":", 1)[1].strip()}
current = {"id": stripped.split(":", 1)[1].strip().strip('"')}
elif current and ":" in stripped:
key, value = stripped.split(":", 1)
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(" }\n\n")
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/* {
reverse_proxy appfactory-webhook:9000
}
@@ -79,11 +102,87 @@ cat >> "$CADDY_FILE" <<EOF_CADDY
handle {
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
}
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"
docker exec appfactory-caddy caddy validate --config /etc/caddy/Caddyfile
docker exec appfactory-caddy caddy reload --config /etc/caddy/Caddyfile
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 reload --config /etc/caddy/Caddyfile
else
echo "WARN: appfactory-caddy container not found, skipping validate/reload"
fi
+45
View File
@@ -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"
+82
View File
@@ -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
+33
View File
@@ -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
+81
View File
@@ -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