added support for multiple routes multiple ports

This commit is contained in:
Jakub Dorfman 2026-05-20 00:21:25 +02:00
parent 7399cf83f9
commit 7fd2531501
4 changed files with 445 additions and 151 deletions

View file

@ -214,11 +214,23 @@ class Handler(BaseHTTPRequestHandler):
fields = line.split()
if len(fields) < 4:
continue
# New format: name domain|upstream routes=N auth=bool [repo_url]
first_route = fields[1]
route_parts = first_route.split("|")
domain = route_parts[0].split(",")[0] if route_parts else first_route
upstream = route_parts[1] if len(route_parts) > 1 else ""
route_count_str = fields[2].replace("routes=", "")
# Backward compat: fields[2] may be upstream if old format
if not route_count_str.isdigit():
upstream = fields[2]
route_count_str = "1"
apps.append({
"name": fields[0],
"domain": fields[1].split(",")[0],
"domains": fields[1],
"upstream": fields[2],
"domain": domain,
"domains": domain,
"upstream": upstream,
"first_route": first_route,
"route_count": route_count_str,
"auth": fields[3].replace("auth=", ""),
"repo_url": fields[4] if len(fields) >= 5 else "",
})
@ -312,6 +324,41 @@ class Handler(BaseHTTPRequestHandler):
self.wfile.write(chunk)
return
# /apps/<name>/routes — get parsed routes
if len(parts) == 3 and parts[0] == "apps" and parts[2] == "routes":
name = parts[1]
if not is_safe_name(name):
self._json(400, {"ok": False, "error": "invalid app name"})
return
result = run_panelctl(["show", name])
if not result["ok"]:
self._json(404, result)
return
env = parse_env_blob(result["stdout"])
routes_raw = env.get("APP_ROUTES", "")
# Backward compat: build from old APP_DOMAIN/APP_PORT/APP_UPSTREAM
if not routes_raw and "APP_DOMAIN" in env:
upstream = env.get("APP_UPSTREAM", f"127.0.0.1:{env.get('APP_PORT', '18080')}")
domains_str = env.get("APP_DOMAINS", env["APP_DOMAIN"])
routes_parts = []
for d in domains_str.split(","):
d = d.strip()
if d:
routes_parts.append(f"{d}|{upstream}")
routes_raw = ",".join(routes_parts)
routes = []
for entry in routes_raw.split(","):
entry = entry.strip()
if not entry:
continue
if "|" in entry:
domain, upstream = entry.split("|", 1)
routes.append({"domain": domain.strip(), "upstream": upstream.strip()})
else:
routes.append({"domain": entry.strip(), "upstream": ""})
self._json(200, {"ok": True, "name": name, "routes": routes})
return
# /apps/<name> — show single app
if len(parts) == 2 and parts[0] == "apps":
name = parts[1]
@ -533,14 +580,32 @@ class Handler(BaseHTTPRequestHandler):
try:
payload = self._read_json()
name = payload["name"]
# Support both "domain" (string, possibly comma-separated) and "domains" (array)
if "domains" in payload and isinstance(payload["domains"], list):
domain = ",".join(payload["domains"])
else:
domain = str(payload.get("domain", ""))
port = str(payload["port"])
auth = str(payload.get("auth", True)).lower()
source_type = payload.get("source_type", "default")
# Build routes string: "domain|upstream,domain|upstream,..."
routes_parts = []
if "routes" in payload and isinstance(payload["routes"], list):
for r in payload["routes"]:
d = r.get("domain", "").strip()
u = r.get("upstream", "").strip()
if d and u:
routes_parts.append(f"{d}|{u}")
elif "domain" in payload and "port" in payload:
# Backward compat: single domain + port
domain_str = payload.get("domain", "")
if "domains" in payload and isinstance(payload["domains"], list):
domain_str = ",".join(payload["domains"])
port = str(payload["port"])
for d in domain_str.split(","):
d = d.strip()
if d:
routes_parts.append(f"{d}|127.0.0.1:{port}")
else:
self._json(400, {"ok": False, "error": "missing 'routes' array or 'domain'+'port' fields"})
return
routes_str = ",".join(routes_parts)
except Exception as exc:
self._json(400, {"ok": False, "error": f"invalid payload: {exc}"})
return
@ -550,7 +615,7 @@ class Handler(BaseHTTPRequestHandler):
return
try:
result = run_panelctl(["init", name, domain, port, auth])
result = run_panelctl(["init", name, routes_str, auth])
if not result["ok"]:
self._json(400, result)
return
@ -730,6 +795,33 @@ class Handler(BaseHTTPRequestHandler):
self._json(200 if result["ok"] else 400, result)
return
# POST /apps/<name>/routes — hot update routes
if action == "routes":
if not is_safe_name(name):
self._json(400, {"ok": False, "error": "invalid app name"})
return
try:
payload = self._read_json()
except Exception as exc:
self._json(400, {"ok": False, "error": f"invalid payload: {exc}"})
return
route_list = payload.get("routes", [])
if not isinstance(route_list, list) or not route_list:
self._json(400, {"ok": False, "error": "routes must be a non-empty array"})
return
routes_parts = []
for r in route_list:
d = r.get("domain", "").strip()
u = r.get("upstream", "").strip()
if not d or not u:
self._json(400, {"ok": False, "error": "each route needs 'domain' and 'upstream'"})
return
routes_parts.append(f"{d}|{u}")
routes_str = ",".join(routes_parts)
result = run_panelctl(["set-routes", name, routes_str])
self._json(200 if result["ok"] else 400, result)
return
# Simple panelctl pass-through actions
if action in {"deploy", "stop", "restart", "render-route", "volume-clear"}:
if not is_safe_name(name):