This commit is contained in:
@@ -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 & 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 & 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"])
|
||||
|
||||
Reference in New Issue
Block a user