@@ -3,13 +3,20 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from flask import Flask, redirect, render_template_string
|
from flask import Flask, jsonify, redirect, render_template_string, request
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
CF_API_TOKEN = os.environ["CF_API_TOKEN"]
|
CF_API_TOKEN = os.environ["CF_API_TOKEN"]
|
||||||
CF_ZONE_ID = os.environ["CF_ZONE_ID"]
|
CF_ZONE_ID = os.environ["CF_ZONE_ID"]
|
||||||
SELF_NAME = os.environ.get("SELF_CONTAINER_NAME", "")
|
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):
|
def extract_hostname(labels, name):
|
||||||
@@ -46,6 +53,33 @@ def list_projects():
|
|||||||
return 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):
|
def delete_dns_record(hostname):
|
||||||
if not hostname:
|
if not hostname:
|
||||||
return
|
return
|
||||||
@@ -118,7 +152,7 @@ def index():
|
|||||||
|
|
||||||
|
|
||||||
@app.route("/delete/<name>", methods=["POST"])
|
@app.route("/delete/<name>", methods=["POST"])
|
||||||
def delete(name):
|
def delete_ui(name):
|
||||||
projects = {p["name"]: p for p in list_projects()}
|
projects = {p["name"]: p for p in list_projects()}
|
||||||
target = projects.get(name)
|
target = projects.get(name)
|
||||||
if target:
|
if target:
|
||||||
@@ -127,5 +161,65 @@ def delete(name):
|
|||||||
return redirect("/")
|
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/<name>", 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__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5000)
|
app.run(host="0.0.0.0", port=5000)
|
||||||
|
|||||||
Reference in New Issue
Block a user