636 lines
22 KiB
Python
636 lines
22 KiB
Python
import json
|
||
import os
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import time
|
||
from datetime import datetime, timezone
|
||
|
||
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")
|
||
|
||
ROUTER_RULE_RE = re.compile(r"^traefik\.http\.routers\.[^.]+\.rule$")
|
||
|
||
|
||
def check_auth():
|
||
auth = request.headers.get("Authorization", "")
|
||
return auth == f"Bearer {API_TOKEN}"
|
||
|
||
|
||
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]
|
||
return None
|
||
|
||
|
||
def human_relative(iso_ts):
|
||
if not iso_ts or iso_ts.startswith("0001-01-01"):
|
||
return None
|
||
try:
|
||
dt = datetime.fromisoformat(iso_ts.replace("Z", "+00:00"))
|
||
except ValueError:
|
||
return None
|
||
secs = int((datetime.now(timezone.utc) - dt).total_seconds())
|
||
if secs < 60:
|
||
return f"{secs} sn önce"
|
||
mins = secs // 60
|
||
if mins < 60:
|
||
return f"{mins} dk önce"
|
||
hours = mins // 60
|
||
if hours < 24:
|
||
return f"{hours} sa önce"
|
||
days = hours // 24
|
||
return f"{days} gün önce"
|
||
|
||
|
||
def _cpu_times():
|
||
with open("/proc/stat") as f:
|
||
parts = f.readline().split()[1:]
|
||
nums = [int(x) for x in parts]
|
||
idle = nums[3] + nums[4]
|
||
total = sum(nums)
|
||
return idle, total
|
||
|
||
|
||
def server_stats():
|
||
stats = {}
|
||
|
||
idle1, total1 = _cpu_times()
|
||
time.sleep(0.3)
|
||
idle2, total2 = _cpu_times()
|
||
d_idle = idle2 - idle1
|
||
d_total = total2 - total1
|
||
stats["cpu_percent"] = round((1 - d_idle / d_total) * 100, 1) if d_total > 0 else 0.0
|
||
stats["cpu_cores"] = os.cpu_count()
|
||
|
||
with open("/proc/loadavg") as f:
|
||
stats["load"] = " / ".join(f.read().split()[:3])
|
||
|
||
meminfo = {}
|
||
with open("/proc/meminfo") as f:
|
||
for line in f:
|
||
key, _, rest = line.partition(":")
|
||
meminfo[key] = int(rest.strip().split()[0])
|
||
mem_total = meminfo.get("MemTotal", 0)
|
||
mem_available = meminfo.get("MemAvailable", 0)
|
||
mem_used = mem_total - mem_available
|
||
stats["mem_used_gb"] = round(mem_used / 1024 / 1024, 1)
|
||
stats["mem_total_gb"] = round(mem_total / 1024 / 1024, 1)
|
||
stats["mem_percent"] = round(mem_used / mem_total * 100, 1) if mem_total else 0.0
|
||
|
||
disk = shutil.disk_usage("/")
|
||
stats["disk_used_gb"] = round(disk.used / 1024**3, 1)
|
||
stats["disk_total_gb"] = round(disk.total / 1024**3, 1)
|
||
stats["disk_percent"] = round(disk.used / disk.total * 100, 1) if disk.total else 0.0
|
||
|
||
with open("/proc/uptime") as f:
|
||
up_secs = int(float(f.read().split()[0]))
|
||
days, rem = divmod(up_secs, 86400)
|
||
hours, _ = divmod(rem, 3600)
|
||
stats["uptime"] = f"{days} gün {hours} saat" if days else f"{hours} saat"
|
||
|
||
all_states = subprocess.run(
|
||
["docker", "ps", "-a", "--format", "{{.State}}"],
|
||
capture_output=True, text=True,
|
||
).stdout.split()
|
||
stats["containers_running"] = sum(1 for s in all_states if s == "running")
|
||
stats["containers_total"] = len(all_states)
|
||
|
||
return stats
|
||
|
||
|
||
def inspect_all(cid):
|
||
return json.loads(subprocess.run(
|
||
["docker", "inspect", cid], capture_output=True, text=True, check=True
|
||
).stdout)[0]
|
||
|
||
|
||
def port_summary(info):
|
||
ports = info.get("NetworkSettings", {}).get("Ports") or {}
|
||
out = []
|
||
for container_port, bindings in ports.items():
|
||
if not bindings:
|
||
continue
|
||
for b in bindings:
|
||
out.append(f"{b.get('HostPort')}→{container_port}")
|
||
return ", ".join(out) if out else None
|
||
|
||
|
||
def list_all_containers():
|
||
out = subprocess.run(
|
||
["docker", "ps", "-a", "--format", "{{.ID}}"],
|
||
capture_output=True, text=True, check=True,
|
||
).stdout.split()
|
||
|
||
containers = []
|
||
for cid in out:
|
||
info = inspect_all(cid)
|
||
name = info["Name"].lstrip("/")
|
||
if name == SELF_NAME:
|
||
continue
|
||
labels = info["Config"]["Labels"] or {}
|
||
state = info["State"]
|
||
status = state["Status"]
|
||
when = human_relative(state.get("StartedAt") if status == "running" else state.get("FinishedAt"))
|
||
containers.append({
|
||
"name": name,
|
||
"hostname": extract_hostname(labels),
|
||
"status": status,
|
||
"since": when,
|
||
"image": info["Config"]["Image"],
|
||
"ports": port_summary(info),
|
||
})
|
||
containers.sort(key=lambda c: c["name"])
|
||
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()
|
||
rows = []
|
||
for line in lines[1:]:
|
||
parts = re.split(r"\s{2,}", line.strip())
|
||
if len(parts) >= 4:
|
||
rows.append({
|
||
"type": parts[0], "total": parts[1], "active": parts[2],
|
||
"size": parts[3], "reclaimable": parts[4] if len(parts) > 4 else "",
|
||
})
|
||
return rows
|
||
|
||
|
||
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)
|
||
|
||
|
||
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,
|
||
)
|
||
|
||
|
||
BASE_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; }
|
||
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; }
|
||
a { color: #38bdf8; text-decoration: none; }
|
||
a:hover { text-decoration: underline; }
|
||
.badge { padding: 0.25rem 0.65rem; border-radius: 999px; font-size: 0.78rem; font-weight: 600; }
|
||
.running { background: #065f46; color: #d1fae5; }
|
||
.exited { background: #7f1d1d; color: #fecaca; }
|
||
.since { opacity: 0.55; font-size: 0.8rem; display: block; }
|
||
.actions { display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
||
button, .btn { border: none; padding: 0.4rem 0.8rem; border-radius: 6px; cursor: pointer;
|
||
font-size: 0.85rem; color: white; }
|
||
.btn-restart, .btn-start { background: #1e3a8a; }
|
||
.btn-restart:hover, .btn-start:hover { background: #1e40af; }
|
||
.btn-stop { background: #78350f; }
|
||
.btn-stop:hover { background: #92400e; }
|
||
.btn-logs { background: #334155; }
|
||
.btn-logs:hover { background: #475569; }
|
||
.btn-delete { background: #7f1d1d; }
|
||
.btn-delete:hover { background: #991b1b; }
|
||
.btn-prune { background: #4c1d95; }
|
||
.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; }
|
||
.stat-label { font-size: 0.75rem; opacity: 0.6; text-transform: uppercase; letter-spacing: .03em; }
|
||
.stat-value { font-size: 1.5rem; font-weight: 700; margin: 0.15rem 0 0.5rem; }
|
||
.stat-sub { font-size: 0.78rem; opacity: 0.55; }
|
||
.bar { height: 6px; border-radius: 999px; background: #334155; overflow: hidden; margin-top: 0.5rem; }
|
||
.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="/system" class="{{ 'active' if page == 'system' else '' }}">Sistem & 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>
|
||
</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">
|
||
<div class="stat-card">
|
||
<div class="stat-label">CPU ({{ s.cpu_cores }} çekirdek)</div>
|
||
<div class="stat-value">%{{ s.cpu_percent }}</div>
|
||
<div class="bar"><div class="bar-fill {{ 'crit' if s.cpu_percent > 85 else ('warn' if s.cpu_percent > 60 else '') }}" style="width: {{ s.cpu_percent }}%"></div></div>
|
||
<div class="stat-sub">yük: {{ s.load }}</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Bellek</div>
|
||
<div class="stat-value">%{{ s.mem_percent }}</div>
|
||
<div class="bar"><div class="bar-fill {{ 'crit' if s.mem_percent > 85 else ('warn' if s.mem_percent > 60 else '') }}" style="width: {{ s.mem_percent }}%"></div></div>
|
||
<div class="stat-sub">{{ s.mem_used_gb }} / {{ s.mem_total_gb }} GB</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Disk</div>
|
||
<div class="stat-value">%{{ s.disk_percent }}</div>
|
||
<div class="bar"><div class="bar-fill {{ 'crit' if s.disk_percent > 85 else ('warn' if s.disk_percent > 60 else '') }}" style="width: {{ s.disk_percent }}%"></div></div>
|
||
<div class="stat-sub">{{ s.disk_used_gb }} / {{ s.disk_total_gb }} GB</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Sunucu</div>
|
||
<div class="stat-value" style="font-size:1.1rem;">{{ s.uptime }}</div>
|
||
<div class="stat-sub">çalışma süresi</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Container</div>
|
||
<div class="stat-value" style="font-size:1.1rem;">{{ s.containers_running }} / {{ s.containers_total }}</div>
|
||
<div class="stat-sub">çalışan / toplam</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if containers %}
|
||
<table>
|
||
<tr><th>İsim</th><th>Adres</th><th>Portlar</th><th>Durum</th><th>İmaj</th><th></th></tr>
|
||
{% for c in containers %}
|
||
<tr>
|
||
<td>{{ c.name }}</td>
|
||
<td>{% if c.hostname %}<a href="https://{{ c.hostname }}" target="_blank">{{ c.hostname }}</a>{% else %}—{% endif %}</td>
|
||
<td>{{ c.ports or '—' }}</td>
|
||
<td>
|
||
<span class="badge {{ c.status }}">{{ c.status }}</span>
|
||
{% if c.since %}<span class="since">{{ c.since }}</span>{% endif %}
|
||
</td>
|
||
<td>{{ c.image }}</td>
|
||
<td>
|
||
<div class="actions">
|
||
<a class="btn btn-logs" href="/logs/{{ c.name }}">Loglar</a>
|
||
{% if c.status == 'running' %}
|
||
<form method="post" action="/stop/{{ c.name }}">
|
||
<button class="btn-stop" type="submit">Durdur</button>
|
||
</form>
|
||
<form method="post" action="/restart/{{ c.name }}">
|
||
<button class="btn-restart" type="submit">Yeniden Başlat</button>
|
||
</form>
|
||
{% else %}
|
||
<form method="post" action="/start/{{ c.name }}">
|
||
<button class="btn-start" type="submit">Başlat</button>
|
||
</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 '' }}');">
|
||
<button class="btn-delete" type="submit">Sil</button>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</table>
|
||
{% else %}
|
||
<p class="empty">Hiç container yok.</p>
|
||
{% endif %}
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
SYSTEM_TEMPLATE = """
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<title>Sistem & 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>
|
||
|
||
<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 %}
|
||
<tr>
|
||
<td>{{ row.type }}</td>
|
||
<td>{{ row.total }}</td>
|
||
<td>{{ row.active }}</td>
|
||
<td>{{ row.size }}</td>
|
||
<td>{{ row.reclaimable }}</td>
|
||
</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?');">
|
||
<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?');">
|
||
<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>
|
||
"""
|
||
|
||
LOGS_TEMPLATE = """
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<title>{{ name }} - Loglar</title>
|
||
<meta charset="utf-8">
|
||
<style>
|
||
body { font-family: ui-monospace, monospace; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||
a { color: #38bdf8; }
|
||
h1 { font-family: system-ui, sans-serif; color: #38bdf8; font-size: 1.2rem; }
|
||
pre { background: #1e293b; padding: 1rem; border-radius: 8px; overflow-x: auto;
|
||
white-space: pre-wrap; word-break: break-word; font-size: 0.85rem; line-height: 1.4; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<a href="/">← panele dön</a>
|
||
<h1>{{ name }} — son {{ lines }} satır</h1>
|
||
<pre>{{ logs }}</pre>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
|
||
@app.route("/")
|
||
def index():
|
||
return render_template_string(
|
||
CONTAINERS_TEMPLATE, containers=list_all_containers(), s=server_stats(), page="containers"
|
||
)
|
||
|
||
|
||
@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("/logs/<name>")
|
||
def logs_ui(name):
|
||
result = subprocess.run(
|
||
["docker", "logs", "--tail", "150", name],
|
||
capture_output=True, text=True,
|
||
)
|
||
output = (result.stdout or "") + (result.stderr or "")
|
||
return render_template_string(LOGS_TEMPLATE, name=name, lines=150, logs=output or "(log yok)")
|
||
|
||
|
||
@app.route("/start/<name>", methods=["POST"])
|
||
def start_ui(name):
|
||
subprocess.run(["docker", "start", name])
|
||
return redirect(request.referrer or "/")
|
||
|
||
|
||
@app.route("/stop/<name>", methods=["POST"])
|
||
def stop_ui(name):
|
||
subprocess.run(["docker", "stop", name])
|
||
return redirect(request.referrer or "/")
|
||
|
||
|
||
@app.route("/restart/<name>", methods=["POST"])
|
||
def restart_ui(name):
|
||
subprocess.run(["docker", "restart", name])
|
||
return redirect(request.referrer or "/")
|
||
|
||
|
||
@app.route("/delete/<name>", methods=["POST"])
|
||
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")
|
||
|
||
|
||
@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")
|
||
|
||
|
||
@app.route("/volumes/prune", methods=["POST"])
|
||
def prune_volumes():
|
||
subprocess.run(["docker", "volume", "prune", "-f"])
|
||
return redirect("/system")
|
||
|
||
|
||
@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)
|