sadelestir: DNS/Traefik sekmeleri kaldirildi, tek sayfa
Deploy / deploy (push) Successful in 40s

This commit is contained in:
2026-09-23 17:03:04 +03:00
parent a6d8664324
commit bc8b7fa5a5
+25 -381
View File
@@ -16,7 +16,6 @@ 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")
TRAEFIK_API_URL = os.environ.get("TRAEFIK_API_URL", "http://traefik:8080")
ROUTER_RULE_RE = re.compile(r"^traefik\.http\.routers\.[^.]+\.rule$")
@@ -27,9 +26,6 @@ def check_auth():
def extract_hostname(labels):
"""Scan ALL traefik router rule labels, not just one matching the
container's own name — compose-based containers get auto-generated
docker names that no longer match the Traefik router name."""
for key, value in labels.items():
if ROUTER_RULE_RE.match(key) and "`" in value:
return value.split("`")[1]
@@ -157,46 +153,6 @@ def list_all_containers():
return containers
def list_images():
out = subprocess.run(
["docker", "images", "--format", "{{json .}}"],
capture_output=True, text=True, check=True,
).stdout.strip().splitlines()
images = []
for line in out:
if not line:
continue
d = json.loads(line)
repo, tag = d.get("Repository", ""), d.get("Tag", "")
if repo == "<none>" and tag == "<none>":
label = d.get("ID", "")[:19]
else:
label = f"{repo}:{tag}"
images.append({
"id": d.get("ID"),
"label": label,
"size": d.get("Size"),
"created": d.get("CreatedSince"),
})
images.sort(key=lambda i: i["label"])
return images
def list_volumes():
out = subprocess.run(
["docker", "volume", "ls", "--format", "{{json .}}"],
capture_output=True, text=True, check=True,
).stdout.strip().splitlines()
volumes = []
for line in out:
if not line:
continue
d = json.loads(line)
volumes.append({"name": d.get("Name"), "driver": d.get("Driver")})
volumes.sort(key=lambda v: v["name"])
return volumes
def docker_system_df():
out = subprocess.run(["docker", "system", "df"], capture_output=True, text=True).stdout
lines = out.strip().splitlines()
@@ -255,86 +211,18 @@ def delete_dns_record(hostname):
)
def list_dns_records():
records = []
page = 1
while True:
r = requests.get(
f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records",
params={"page": page, "per_page": 100},
headers={"Authorization": f"Bearer {CF_API_TOKEN}"},
timeout=10,
).json()
records.extend(r.get("result", []))
info = r.get("result_info", {})
if page >= info.get("total_pages", 1):
break
page += 1
records.sort(key=lambda x: (x.get("type", ""), x.get("name", "")))
return records
def create_dns_record(rtype, name, content, proxied, ttl=1):
return 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": rtype, "name": name, "content": content, "ttl": int(ttl), "proxied": proxied},
timeout=10,
).json()
def update_dns_record(record_id, content, proxied):
return requests.patch(
f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records/{record_id}",
headers={"Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json"},
json={"content": content, "proxied": proxied},
timeout=10,
).json()
def delete_dns_record_by_id(record_id):
requests.delete(
f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records/{record_id}",
headers={"Authorization": f"Bearer {CF_API_TOKEN}"},
timeout=10,
)
def list_traefik_routers():
try:
routers = requests.get(f"{TRAEFIK_API_URL}/api/http/routers", timeout=3).json()
services = requests.get(f"{TRAEFIK_API_URL}/api/http/services", timeout=3).json()
except requests.RequestException:
return None
services_by_name = {s["name"].split("@")[0]: s for s in services}
result = []
for r in routers:
if r.get("provider") != "docker":
continue
rule = r.get("rule", "")
hostname = rule.split("`")[1] if "`" in rule else rule
service = services_by_name.get(r.get("service"), {})
servers = service.get("loadBalancer", {}).get("servers", [])
backend = servers[0].get("url") if servers else None
result.append({
"name": r.get("name", "").split("@")[0],
"hostname": hostname,
"entrypoints": ", ".join(r.get("entryPoints", [])),
"tls": bool(r.get("tls")),
"status": r.get("status"),
"backend": backend,
})
result.sort(key=lambda x: x["name"])
return result
BASE_STYLE = """
TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Deploy Panel</title>
<meta charset="utf-8">
<meta http-equiv="refresh" content="20">
<style>
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2.5rem; }
h1 { color: #38bdf8; margin-bottom: 0.2rem; }
h2 { color: #38bdf8; font-size: 1.1rem; margin: 2rem 0 0.8rem; }
.sub { opacity: 0.55; font-size: 0.85rem; margin-bottom: 1rem; }
h2 { color: #38bdf8; font-size: 1rem; margin: 2rem 0 0.8rem; }
.sub { opacity: 0.55; font-size: 0.85rem; margin-bottom: 1.5rem; }
table { width: 100%; border-collapse: collapse; }
td, th { padding: 0.7rem; border-bottom: 1px solid #334155; text-align: left; vertical-align: middle; }
th { opacity: 0.7; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: .03em; }
@@ -359,7 +247,6 @@ BASE_STYLE = """
.btn-prune:hover { background: #5b21b6; }
.empty { opacity: 0.6; margin-top: 1.5rem; }
form { display: inline; }
code { background: #1e293b; padding: 0.1rem 0.4rem; border-radius: 4px; font-size: 0.85rem; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem; margin-bottom: 2rem; }
.stat-card { background: #1e293b; border-radius: 10px; padding: 1rem 1.2rem; }
@@ -370,33 +257,11 @@ BASE_STYLE = """
.bar-fill { height: 100%; background: #38bdf8; }
.bar-fill.warn { background: #f59e0b; }
.bar-fill.crit { background: #ef4444; }
nav { margin-bottom: 1.5rem; display: flex; gap: 1.2rem; border-bottom: 1px solid #334155;
padding-bottom: 0.8rem; }
nav a { font-size: 0.9rem; opacity: 0.65; }
nav a.active { opacity: 1; color: #38bdf8; font-weight: 600; }
"""
NAV = """
<nav>
<a href="/" class="{{ 'active' if page == 'containers' else '' }}">Container'lar</a>
<a href="/traefik" class="{{ 'active' if page == 'traefik' else '' }}">Traefik</a>
<a href="/dns" class="{{ 'active' if page == 'dns' else '' }}">DNS</a>
<a href="/system" class="{{ 'active' if page == 'system' else '' }}">Sistem &amp; Disk</a>
</nav>
"""
CONTAINERS_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Deploy Panel</title>
<meta charset="utf-8">
<meta http-equiv="refresh" content="20">
<style>""" + BASE_STYLE + """</style>
.prune-actions { display: flex; gap: 0.6rem; margin-top: 0.8rem; }
</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">{{ containers|length }} container · 20 saniyede bir otomatik yenilenir · <a href="/">şimdi yenile</a></div>
<div class="stats">
@@ -459,7 +324,7 @@ CONTAINERS_TEMPLATE = """
</form>
{% endif %}
<form method="post" action="/delete/{{ c.name }}"
onsubmit="return confirm('{{ c.name }} silinsin mi?{{ ' DNS kaydı da kaldırılacak.' if c.hostname else '' }}');">
onsubmit="return confirm('{{ c.name }} silinsin mi?{{ ' DNS kaydı da otomatik kaldırılacak.' if c.hostname else '' }}');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</div>
@@ -470,23 +335,8 @@ CONTAINERS_TEMPLATE = """
{% else %}
<p class="empty">Hiç container yok.</p>
{% endif %}
</body>
</html>
"""
SYSTEM_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Sistem &amp; Disk - Deploy Panel</title>
<meta charset="utf-8">
<style>""" + BASE_STYLE + """</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">Docker'ın diskte kapladığı alan ve imaj/volume yönetimi</div>
<h2>Disk Kullanımı</h2>
<table>
<tr><th>Tür</th><th>Toplam</th><th>Kullanımda</th><th>Boyut</th><th>Geri kazanılabilir</th></tr>
{% for row in df %}
@@ -499,159 +349,14 @@ SYSTEM_TEMPLATE = """
</tr>
{% endfor %}
</table>
<h2>İmajlar ({{ images|length }})</h2>
<form method="post" action="/images/prune" onsubmit="return confirm('Hiçbir container tarafından kullanılmayan TÜM imajlar silinecek. Emin misin?');">
<div class="prune-actions">
<form method="post" action="/images/prune" onsubmit="return confirm('Hiçbir container tarafından kullanılmayan tüm imajlar silinecek. Emin misin?');">
<button class="btn-prune" type="submit">Kullanılmayan imajları temizle</button>
</form>
{% if images %}
<table>
<tr><th>İmaj</th><th>Boyut</th><th>Oluşturulma</th><th></th></tr>
{% for img in images %}
<tr>
<td><code>{{ img.label }}</code></td>
<td>{{ img.size }}</td>
<td>{{ img.created }}</td>
<td>
<form method="post" action="/images/delete/{{ img.id }}"
onsubmit="return confirm('{{ img.label }} silinsin mi? Kullanan bir container varsa silme başarısız olur.');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Hiç imaj yok.</p>
{% endif %}
<h2>Volume'ler ({{ volumes|length }})</h2>
<form method="post" action="/volumes/prune" onsubmit="return confirm('Hiçbir container tarafından kullanılmayan TÜM volume\\'ler silinecek. Emin misin?');">
<form method="post" action="/volumes/prune" onsubmit="return confirm('Hiçbir container tarafından kullanılmayan tüm volume\\'ler silinecek. Emin misin?');">
<button class="btn-prune" type="submit">Kullanılmayan volume'leri temizle</button>
</form>
{% if volumes %}
<table>
<tr><th>İsim</th><th>Driver</th><th></th></tr>
{% for v in volumes %}
<tr>
<td><code>{{ v.name }}</code></td>
<td>{{ v.driver }}</td>
<td>
<form method="post" action="/volumes/delete/{{ v.name }}"
onsubmit="return confirm('{{ v.name }} silinsin mi? Kullanan bir container varsa silme başarısız olur.');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Hiç volume yok.</p>
{% endif %}
</body>
</html>
"""
TRAEFIK_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Traefik - Deploy Panel</title>
<meta charset="utf-8">
<style>""" + BASE_STYLE + """</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">Traefik'in kendi API'sinden canlı okunan router/servis listesi</div>
{% if routers is none %}
<p class="empty">Traefik API'sine ulaşılamadı ({{ api_url }}).</p>
{% elif routers %}
<table>
<tr><th>Router</th><th>Adres</th><th>Entrypoint</th><th>TLS</th><th>Durum</th><th>Backend</th></tr>
{% for r in routers %}
<tr>
<td>{{ r.name }}</td>
<td>{% if r.hostname %}<a href="https://{{ r.hostname }}" target="_blank">{{ r.hostname }}</a>{% else %}{{ r.hostname }}{% endif %}</td>
<td>{{ r.entrypoints }}</td>
<td>{{ '' if r.tls else '' }}</td>
<td><span class="badge {{ 'running' if r.status == 'enabled' else 'exited' }}">{{ r.status }}</span></td>
<td><code>{{ r.backend or '' }}</code></td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Aktif router yok.</p>
{% endif %}
</body>
</html>
"""
DNS_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>DNS - Deploy Panel</title>
<meta charset="utf-8">
<style>""" + BASE_STYLE + """</style>
<style>
.dns-form { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; margin-bottom: 1.5rem;
background: #1e293b; padding: 1rem; border-radius: 10px; }
.dns-form input, .dns-form select {
background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 0.45rem 0.6rem;
border-radius: 6px; font-size: 0.85rem;
}
.dns-form label { font-size: 0.8rem; opacity: 0.7; display: flex; align-items: center; gap: 0.3rem; }
.btn-add { background: #065f46; }
.btn-add:hover { background: #047857; }
.edit-row input[type=text] { width: 100%; }
</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">akalan.dev · Cloudflare DNS kayıtları · {{ records|length }} kayıt</div>
<form class="dns-form" method="post" action="/dns/add">
<select name="type">
<option value="A">A</option>
<option value="CNAME">CNAME</option>
<option value="TXT">TXT</option>
<option value="MX">MX</option>
</select>
<input type="text" name="name" placeholder="ad (örn. blog veya blog.akalan.dev)" required>
<input type="text" name="content" placeholder="değer (örn. 148.135.181.126)" required>
<label><input type="checkbox" name="proxied" checked> turuncu bulut</label>
<button class="btn-add" type="submit">Ekle</button>
</form>
{% if records %}
<table>
<tr><th>Tip</th><th>Ad</th><th>Değer</th><th>Turuncu Bulut</th><th></th></tr>
{% for r in records %}
<tr class="edit-row">
<td>{{ r.type }}</td>
<td>{{ r.name }}</td>
<td>
<form method="post" action="/dns/update/{{ r.id }}" style="display:flex; gap:0.4rem; align-items:center;">
<input type="text" name="content" value="{{ r.content }}">
<label><input type="checkbox" name="proxied" {{ 'checked' if r.proxied else '' }}> proxy</label>
<button class="btn-restart" type="submit">Güncelle</button>
</form>
</td>
<td>{{ '🟠 açık' if r.proxied else '⚪ kapalı' }}</td>
<td>
<form method="post" action="/dns/delete/{{ r.id }}"
onsubmit="return confirm('{{ r.name }} ({{ r.type }}) kaydı silinsin mi?');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Hiç DNS kaydı yok.</p>
{% endif %}
</div>
</body>
</html>
"""
@@ -682,59 +387,10 @@ LOGS_TEMPLATE = """
@app.route("/")
def index():
return render_template_string(
CONTAINERS_TEMPLATE, containers=list_all_containers(), s=server_stats(), page="containers"
TEMPLATE, containers=list_all_containers(), s=server_stats(), df=docker_system_df()
)
@app.route("/system")
def system_ui():
return render_template_string(
SYSTEM_TEMPLATE, df=docker_system_df(), images=list_images(), volumes=list_volumes(), page="system"
)
@app.route("/traefik")
def traefik_ui():
return render_template_string(
TRAEFIK_TEMPLATE, routers=list_traefik_routers(), api_url=TRAEFIK_API_URL, page="traefik"
)
@app.route("/dns")
def dns_ui():
return render_template_string(DNS_TEMPLATE, records=list_dns_records(), page="dns")
@app.route("/dns/add", methods=["POST"])
def dns_add():
rtype = request.form.get("type", "A")
name = request.form.get("name", "").strip()
content = request.form.get("content", "").strip()
proxied = request.form.get("proxied") == "on"
if name and content:
if name == "@":
name = "akalan.dev"
elif not name.endswith("akalan.dev"):
name = f"{name}.akalan.dev"
create_dns_record(rtype, name, content, proxied)
return redirect("/dns")
@app.route("/dns/update/<record_id>", methods=["POST"])
def dns_update(record_id):
content = request.form.get("content", "").strip()
proxied = request.form.get("proxied") == "on"
if content:
update_dns_record(record_id, content, proxied)
return redirect("/dns")
@app.route("/dns/delete/<record_id>", methods=["POST"])
def dns_delete(record_id):
delete_dns_record_by_id(record_id)
return redirect("/dns")
@app.route("/logs/<name>")
def logs_ui(name):
result = subprocess.run(
@@ -748,19 +404,19 @@ def logs_ui(name):
@app.route("/start/<name>", methods=["POST"])
def start_ui(name):
subprocess.run(["docker", "start", name])
return redirect(request.referrer or "/")
return redirect("/")
@app.route("/stop/<name>", methods=["POST"])
def stop_ui(name):
subprocess.run(["docker", "stop", name])
return redirect(request.referrer or "/")
return redirect("/")
@app.route("/restart/<name>", methods=["POST"])
def restart_ui(name):
subprocess.run(["docker", "restart", name])
return redirect(request.referrer or "/")
return redirect("/")
@app.route("/delete/<name>", methods=["POST"])
@@ -768,31 +424,19 @@ def delete_ui(name):
hostname = find_container_hostname(name)
subprocess.run(["docker", "rm", "-f", name])
delete_dns_record(hostname)
return redirect(request.referrer or "/")
@app.route("/images/delete/<image_id>", methods=["POST"])
def delete_image(image_id):
subprocess.run(["docker", "rmi", image_id])
return redirect("/system")
return redirect("/")
@app.route("/images/prune", methods=["POST"])
def prune_images():
subprocess.run(["docker", "image", "prune", "-af"])
return redirect("/system")
@app.route("/volumes/delete/<name>", methods=["POST"])
def delete_volume(name):
subprocess.run(["docker", "volume", "rm", name])
return redirect("/system")
return redirect("/")
@app.route("/volumes/prune", methods=["POST"])
def prune_volumes():
subprocess.run(["docker", "volume", "prune", "-f"])
return redirect("/system")
return redirect("/")
@app.route("/api/ensure-dns", methods=["POST"])