Files
deploy-panel/app.py
T
akalan 29cd52cdf9
Deploy / deploy (push) Successful in 19s
api endpoints ekle
2026-09-23 15:27:03 +03:00

226 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)