226 lines
7.1 KiB
Python
226 lines
7.1 KiB
Python
import json
|
||
import os
|
||
import subprocess
|
||
|
||
import requests
|
||
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):
|
||
key = f"traefik.http.routers.{name}.rule"
|
||
rule = labels.get(key, "")
|
||
if "`" in rule:
|
||
return rule.split("`")[1]
|
||
return None
|
||
|
||
|
||
def list_projects():
|
||
out = subprocess.run(
|
||
["docker", "ps", "-a", "--filter", "label=traefik.enable=true", "--format", "{{.ID}}"],
|
||
capture_output=True, text=True, check=True,
|
||
).stdout.split()
|
||
|
||
projects = []
|
||
for cid in out:
|
||
info = json.loads(subprocess.run(
|
||
["docker", "inspect", cid], capture_output=True, text=True, check=True
|
||
).stdout)[0]
|
||
name = info["Name"].lstrip("/")
|
||
if name == SELF_NAME:
|
||
continue
|
||
labels = info["Config"]["Labels"] or {}
|
||
hostname = extract_hostname(labels, name)
|
||
projects.append({
|
||
"name": name,
|
||
"hostname": hostname,
|
||
"status": info["State"]["Status"],
|
||
"image": info["Config"]["Image"],
|
||
})
|
||
projects.sort(key=lambda p: p["name"])
|
||
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
|
||
r = requests.get(
|
||
f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records",
|
||
params={"name": hostname},
|
||
headers={"Authorization": f"Bearer {CF_API_TOKEN}"},
|
||
timeout=10,
|
||
).json()
|
||
for rec in r.get("result", []):
|
||
requests.delete(
|
||
f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records/{rec['id']}",
|
||
headers={"Authorization": f"Bearer {CF_API_TOKEN}"},
|
||
timeout=10,
|
||
)
|
||
|
||
|
||
TEMPLATE = """
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<title>Deploy Panel</title>
|
||
<meta charset="utf-8">
|
||
<style>
|
||
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2.5rem; }
|
||
h1 { color: #38bdf8; }
|
||
table { width: 100%; border-collapse: collapse; margin-top: 1.5rem; }
|
||
td, th { padding: 0.7rem; border-bottom: 1px solid #334155; text-align: left; }
|
||
a { color: #38bdf8; text-decoration: none; }
|
||
.badge { padding: 0.2rem 0.6rem; border-radius: 4px; font-size: 0.8rem; }
|
||
.running { background: #065f46; }
|
||
.exited { background: #7f1d1d; }
|
||
button { background: #7f1d1d; color: white; border: none; padding: 0.4rem 0.9rem;
|
||
border-radius: 4px; cursor: pointer; }
|
||
button:hover { background: #991b1b; }
|
||
.empty { opacity: 0.6; margin-top: 1.5rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Deploy Panel</h1>
|
||
{% if projects %}
|
||
<table>
|
||
<tr><th>Proje</th><th>Adres</th><th>Durum</th><th>İmaj</th><th></th></tr>
|
||
{% for p in projects %}
|
||
<tr>
|
||
<td>{{ p.name }}</td>
|
||
<td>{% if p.hostname %}<a href="https://{{ p.hostname }}" target="_blank">{{ p.hostname }}</a>{% else %}—{% endif %}</td>
|
||
<td><span class="badge {{ p.status }}">{{ p.status }}</span></td>
|
||
<td>{{ p.image }}</td>
|
||
<td>
|
||
<form method="post" action="/delete/{{ p.name }}"
|
||
onsubmit="return confirm('{{ p.name }} silinsin mi? Container ve DNS kaydı kalıcı olarak kaldırılacak.');">
|
||
<button type="submit">Sil</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</table>
|
||
{% else %}
|
||
<p class="empty">Henüz deploy edilmiş bir proje yok.</p>
|
||
{% endif %}
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
|
||
@app.route("/")
|
||
def index():
|
||
return render_template_string(TEMPLATE, projects=list_projects())
|
||
|
||
|
||
@app.route("/delete/<name>", methods=["POST"])
|
||
def delete_ui(name):
|
||
projects = {p["name"]: p for p in list_projects()}
|
||
target = projects.get(name)
|
||
if target:
|
||
subprocess.run(["docker", "rm", "-f", name])
|
||
delete_dns_record(target["hostname"])
|
||
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__":
|
||
app.run(host="0.0.0.0", port=5000)
|