diff --git a/app.py b/app.py index b53fe66..a5ac8dc 100644 --- a/app.py +++ b/app.py @@ -3,13 +3,20 @@ import os import subprocess import requests -from flask import Flask, redirect, render_template_string +from flask import Flask, jsonify, redirect, render_template_string, request app = Flask(__name__) CF_API_TOKEN = os.environ["CF_API_TOKEN"] CF_ZONE_ID = os.environ["CF_ZONE_ID"] SELF_NAME = os.environ.get("SELF_CONTAINER_NAME", "") +API_TOKEN = os.environ["PANEL_API_TOKEN"] +SERVER_IP = os.environ.get("SERVER_IP", "148.135.181.126") + + +def check_auth(): + auth = request.headers.get("Authorization", "") + return auth == f"Bearer {API_TOKEN}" def extract_hostname(labels, name): @@ -46,6 +53,33 @@ def list_projects(): return projects +def find_container_hostname(name): + result = subprocess.run(["docker", "inspect", name], capture_output=True, text=True) + if result.returncode != 0: + return None + info = json.loads(result.stdout)[0] + labels = info["Config"]["Labels"] or {} + return extract_hostname(labels, name) + + +def ensure_dns(hostname): + existing = requests.get( + f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records", + params={"type": "A", "name": hostname}, + headers={"Authorization": f"Bearer {CF_API_TOKEN}"}, + timeout=10, + ).json() + if existing.get("result"): + return {"created": False} + requests.post( + f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records", + headers={"Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json"}, + json={"type": "A", "name": hostname, "content": SERVER_IP, "ttl": 1, "proxied": True}, + timeout=10, + ) + return {"created": True} + + def delete_dns_record(hostname): if not hostname: return @@ -118,7 +152,7 @@ def index(): @app.route("/delete/", methods=["POST"]) -def delete(name): +def delete_ui(name): projects = {p["name"]: p for p in list_projects()} target = projects.get(name) if target: @@ -127,5 +161,65 @@ def delete(name): return redirect("/") +@app.route("/api/ensure-dns", methods=["POST"]) +def api_ensure_dns(): + if not check_auth(): + return jsonify({"error": "unauthorized"}), 401 + body = request.get_json(force=True) + hostname = body.get("hostname") + if not hostname: + return jsonify({"error": "hostname required"}), 400 + result = ensure_dns(hostname) + return jsonify(result), 200 + + +@app.route("/api/deploy", methods=["POST"]) +def api_deploy(): + if not check_auth(): + return jsonify({"error": "unauthorized"}), 401 + body = request.get_json(force=True) + name = body.get("name") + hostname = body.get("hostname") + port = str(body.get("port", "80")) + if not name or not hostname: + return jsonify({"error": "name and hostname required"}), 400 + + current_hostname = find_container_hostname(name) + if current_hostname is not None and current_hostname != hostname: + return jsonify({ + "error": "name_conflict", + "message": f"'{name}' is already deployed with a different hostname ({current_hostname}).", + }), 409 + + ensure_dns(hostname) + + subprocess.run(["docker", "rm", "-f", name]) + rule = f"Host(`{hostname}`)" + subprocess.run([ + "docker", "run", "-d", + "--name", name, + "--network", "proxy", + "--restart", "always", + "-l", "traefik.enable=true", + "-l", f"traefik.http.routers.{name}.rule={rule}", + "-l", f"traefik.http.routers.{name}.entrypoints=websecure", + "-l", f"traefik.http.routers.{name}.tls.certresolver=letsencrypt", + "-l", f"traefik.http.services.{name}.loadbalancer.server.port={port}", + f"{name}:latest", + ], check=True) + + return jsonify({"status": "deployed", "url": f"https://{hostname}"}), 200 + + +@app.route("/api/deploy/", methods=["DELETE"]) +def api_delete(name): + if not check_auth(): + return jsonify({"error": "unauthorized"}), 401 + hostname = find_container_hostname(name) + subprocess.run(["docker", "rm", "-f", name]) + delete_dns_record(hostname) + return jsonify({"status": "deleted"}), 200 + + if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)