Update scripts/generate-caddyfile.sh

This commit is contained in:
2026-06-15 09:22:19 +00:00
parent 9606f7df9e
commit 1b6c775e56
+201 -7
View File
@@ -13,6 +13,9 @@ fi
APPFACTORY_DIR="${APPFACTORY_DIR:-/opt/appfactory}" 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"
DB_FILE="$APPFACTORY_DIR/data/appfactory/appfactory.db"
STATIC_DIR="$APPFACTORY_DIR/gateway/static"
APPS_JSON_FILE="$STATIC_DIR/apps.json"
APPFACTORY_ENABLE_HTTPS="${APPFACTORY_ENABLE_HTTPS:-false}" APPFACTORY_ENABLE_HTTPS="${APPFACTORY_ENABLE_HTTPS:-false}"
APPFACTORY_PORTAL_DOMAIN="${APPFACTORY_PORTAL_DOMAIN:-}" APPFACTORY_PORTAL_DOMAIN="${APPFACTORY_PORTAL_DOMAIN:-}"
@@ -20,7 +23,7 @@ APPFACTORY_GITEA_DOMAIN="${APPFACTORY_GITEA_DOMAIN:-}"
APPFACTORY_REGISTRY_DOMAIN="${APPFACTORY_REGISTRY_DOMAIN:-}" APPFACTORY_REGISTRY_DOMAIN="${APPFACTORY_REGISTRY_DOMAIN:-}"
mkdir -p "$(dirname "$CADDY_FILE")" mkdir -p "$(dirname "$CADDY_FILE")"
mkdir -p "$APPFACTORY_DIR/gateway/static" mkdir -p "$STATIC_DIR"
write_site_header() { write_site_header() {
local site="$1" local site="$1"
@@ -36,18 +39,22 @@ EOF_CADDY
fi fi
} }
write_app_routes() { write_apps_json() {
local target_file="$1"
if [ ! -f "$CATALOG_FILE" ]; then if [ ! -f "$CATALOG_FILE" ]; then
cat > "$APPS_JSON_FILE" <<'EOF_JSON'
{
"apps": []
}
EOF_JSON
return 0 return 0
fi fi
python3 - "$CATALOG_FILE" "$target_file" <<'PY' python3 - "$CATALOG_FILE" "$APPS_JSON_FILE" <<'PY'
import json
import sys import sys
catalog_file = sys.argv[1] catalog_file = sys.argv[1]
caddy_file = sys.argv[2] apps_json_file = sys.argv[2]
apps = [] apps = []
current = None current = None
@@ -67,6 +74,147 @@ with open(catalog_file, "r", encoding="utf-8") as f:
if current: if current:
apps.append(current) apps.append(current)
result = {"apps": []}
for app in apps:
app_id = app.get("id")
if not app_id:
continue
name = app.get("name") or app_id
base_path = app.get("base_path") or f"/apps/{app_id}"
health = app.get("health") or f"{base_path}/health"
docs = app.get("docs") or f"{base_path}/docs"
result["apps"].append({
"id": app_id,
"name": name,
"base_url": base_path,
"health_url": health,
"docs_url": docs,
})
with open(apps_json_file, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
f.write("\n")
PY
}
write_app_routes() {
local target_file="$1"
if [ ! -f "$CATALOG_FILE" ]; then
return 0
fi
python3 - "$CATALOG_FILE" "$target_file" "$DB_FILE" <<'PY'
import re
import sqlite3
import sys
from collections import defaultdict
catalog_file = sys.argv[1]
caddy_file = sys.argv[2]
db_file = sys.argv[3]
READ_METHODS = ["GET"]
WRITE_METHODS = ["POST", "PUT", "PATCH", "DELETE"]
ALL_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"]
OPEN_PATH_SUFFIXES = [
"/health",
"/docs",
"/docs/*",
"/openapi.json",
"/swagger.json",
"/swagger/*",
]
apps = []
current = None
with open(catalog_file, "r", encoding="utf-8") as f:
for raw in f:
stripped = raw.strip()
if stripped.startswith("- id:"):
if current:
apps.append(current)
current = {"id": stripped.split(":", 1)[1].strip().strip('"')}
elif current and ":" in stripped:
key, value = stripped.split(":", 1)
current[key.strip()] = value.strip().strip('"')
if current:
apps.append(current)
def safe_matcher_name(value: str) -> str:
value = re.sub(r"[^a-zA-Z0-9_]+", "_", value)
value = value.strip("_")
if not value:
value = "app"
if value[0].isdigit():
value = "app_" + value
return value
def parse_methods(value: str) -> set[str]:
value = (value or "").strip().upper()
result: set[str] = set()
if not value:
return result
for part in value.split(","):
part = part.strip().upper()
if not part:
continue
if part == "READ":
result.update(READ_METHODS)
elif part == "WRITE":
result.update(WRITE_METHODS)
elif part == "ALL":
result.update(ALL_METHODS)
else:
result.add(part)
return {m for m in result if m in ALL_METHODS}
rules_by_app: dict[str, list[dict]] = defaultdict(list)
try:
con = sqlite3.connect(db_file)
con.row_factory = sqlite3.Row
rows = con.execute(
"""
SELECT app_id, ip_cidr, methods
FROM app_ip_access_rules
WHERE is_enabled = 1
ORDER BY app_id, id
"""
).fetchall()
for row in rows:
app_id = (row["app_id"] or "").strip()
ip_cidr = (row["ip_cidr"] or "").strip()
methods = parse_methods(row["methods"] or "")
if not app_id or not ip_cidr or not methods:
continue
rules_by_app[app_id].append({
"ip_cidr": ip_cidr,
"methods": methods,
})
con.close()
except Exception as exc:
print(f"WARN: Unable to read app_ip_access_rules from {db_file}: {exc}", file=sys.stderr)
with open(caddy_file, "a", encoding="utf-8") as f: with open(caddy_file, "a", encoding="utf-8") as f:
for app in apps: for app in apps:
app_id = app.get("id") app_id = app.get("id")
@@ -75,6 +223,41 @@ with open(caddy_file, "a", encoding="utf-8") as f:
if not app_id: if not app_id:
continue continue
rules = rules_by_app.get(app_id, [])
safe_name = safe_matcher_name(app_id)
open_paths = [f"/apps/{app_id}{suffix}" for suffix in OPEN_PATH_SUFFIXES]
if rules:
allowed_by_method = {method: [] for method in ALL_METHODS}
for rule in rules:
for method in rule["methods"]:
allowed_by_method.setdefault(method, []).append(rule["ip_cidr"])
for method in WRITE_METHODS:
matcher = f"blocked_{safe_name}_{method.lower()}"
allowed_ips = sorted(set(allowed_by_method.get(method, [])))
f.write(f" @{matcher} {{\n")
f.write(f" path /apps/{app_id}/*\n")
f.write(f" method {method}\n")
f.write(" not path " + " ".join(open_paths) + "\n")
if allowed_ips:
f.write(" not remote_ip " + " ".join(allowed_ips) + "\n")
f.write(" }\n")
f.write(f" respond @{matcher} \"Forbidden\" 403\n\n")
get_allowed_ips = sorted(set(allowed_by_method.get("GET", [])))
if get_allowed_ips:
matcher = f"blocked_{safe_name}_get"
f.write(f" @{matcher} {{\n")
f.write(f" path /apps/{app_id}/*\n")
f.write(" method GET\n")
f.write(" not path " + " ".join(open_paths) + "\n")
f.write(" not remote_ip " + " ".join(get_allowed_ips) + "\n")
f.write(" }\n")
f.write(f" respond @{matcher} \"Forbidden\" 403\n\n")
f.write(f" handle_path /apps/{app_id}/* {{\n") f.write(f" handle_path /apps/{app_id}/* {{\n")
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")
@@ -98,7 +281,15 @@ write_portal_routes() {
handle /apps { handle /apps {
root * /srv/static root * /srv/static
rewrite * /apps.html rewrite * /apps.json
header Content-Type application/json
file_server
}
handle /apps/ {
root * /srv/static
rewrite * /apps.json
header Content-Type application/json
file_server file_server
} }
@@ -199,12 +390,15 @@ EOF_CADDY
fi fi
} }
write_apps_json
if [ -n "$APPFACTORY_PORTAL_DOMAIN" ] || [ -n "$APPFACTORY_GITEA_DOMAIN" ] || [ -n "$APPFACTORY_REGISTRY_DOMAIN" ]; then if [ -n "$APPFACTORY_PORTAL_DOMAIN" ] || [ -n "$APPFACTORY_GITEA_DOMAIN" ] || [ -n "$APPFACTORY_REGISTRY_DOMAIN" ]; then
write_domain_caddyfile write_domain_caddyfile
else else
write_http_only_caddyfile write_http_only_caddyfile
fi fi
echo "Generated: $APPS_JSON_FILE"
echo "Generated: $CADDY_FILE" echo "Generated: $CADDY_FILE"
if docker inspect appfactory-caddy >/dev/null 2>&1; then if docker inspect appfactory-caddy >/dev/null 2>&1; then