Leeres Repository nach Loeschen des letzten Tags entfernen
Die Registry-API loescht nur Manifeste/Tags, das Repository-Verzeichnis
bleibt im Speicher und damit im Katalog ("keine Tags") stehen. Da der
Auth-Container den Registry-Speicher gemountet hat, wird das leere
Verzeichnis jetzt entfernt:
- beim Loeschen des letzten Tags direkt
- bei der Garbage Collection werden zusaetzlich alle Tag-losen
Repositories aufgeraeumt (fuer bereits vorhandene Reste)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+59
-1
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
from functools import wraps
|
||||
@@ -26,6 +27,7 @@ ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "changeme")
|
||||
DB_PATH = os.environ.get("DB_PATH", "/data/users.db")
|
||||
HTPASSWD_PATH = os.environ.get("HTPASSWD_PATH", "/auth/htpasswd")
|
||||
GC_CONFIG = os.environ.get("GC_CONFIG", "/app/registry-gc-config.yml")
|
||||
REGISTRY_DATA = os.environ.get("REGISTRY_DATA", "/var/lib/registry")
|
||||
REGISTRY_URL = os.environ.get("REGISTRY_URL", "http://registry:5000")
|
||||
# Host:Port ohne Schema, fuer skopeo-Ziel (z. B. "registry:5000")
|
||||
REGISTRY_HOST = REGISTRY_URL.split("://", 1)[-1]
|
||||
@@ -213,6 +215,51 @@ def delete_manifest(name, digest):
|
||||
return False
|
||||
|
||||
|
||||
def repo_has_tags(name):
|
||||
"""True, wenn das Repository noch mindestens einen Tag hat."""
|
||||
data = query_registry(f"/v2/{name}/tags/list")
|
||||
return bool(data and data.get("tags"))
|
||||
|
||||
|
||||
def remove_repository_storage(name):
|
||||
"""Entfernt das leere Repository-Verzeichnis aus dem Registry-Speicher.
|
||||
|
||||
Die Registry-API kennt kein "Repository loeschen"; nach dem Loeschen des
|
||||
letzten Tags bleibt sonst ein leerer Eintrag im Katalog stehen.
|
||||
"""
|
||||
base = os.path.realpath(
|
||||
os.path.join(REGISTRY_DATA, "docker", "registry", "v2", "repositories")
|
||||
)
|
||||
target = os.path.realpath(os.path.join(base, *name.split("/")))
|
||||
# Sicherheit: Ziel muss innerhalb des repositories-Verzeichnisses liegen
|
||||
if target != base and not target.startswith(base + os.sep):
|
||||
return False
|
||||
if not os.path.isdir(target):
|
||||
return False
|
||||
shutil.rmtree(target, ignore_errors=True)
|
||||
# Leere Eltern-Verzeichnisse (z. B. "bitnami" nach "bitnami/redis") aufraeumen
|
||||
parent = os.path.dirname(target)
|
||||
while parent != base and os.path.isdir(parent) and not os.listdir(parent):
|
||||
try:
|
||||
os.rmdir(parent)
|
||||
except OSError:
|
||||
break
|
||||
parent = os.path.dirname(parent)
|
||||
return True
|
||||
|
||||
|
||||
def prune_empty_repositories():
|
||||
"""Entfernt alle Repositories ohne Tags aus dem Speicher. Gibt Anzahl zurueck."""
|
||||
data = query_registry("/v2/_catalog")
|
||||
if not data:
|
||||
return 0
|
||||
removed = 0
|
||||
for name in data.get("repositories", []):
|
||||
if not repo_has_tags(name) and remove_repository_storage(name):
|
||||
removed += 1
|
||||
return removed
|
||||
|
||||
|
||||
def remote_digest(source, src_creds=None, src_token=None):
|
||||
"""Liefert den Manifest-Digest eines Quell-Images, ohne es herunterzuladen."""
|
||||
cmd = ["skopeo", "inspect", "--format", "{{.Digest}}", f"docker://{source}"]
|
||||
@@ -439,6 +486,15 @@ def delete_image():
|
||||
return redirect(url_for("images"))
|
||||
|
||||
if delete_manifest(name, digest):
|
||||
# War das der letzte Tag? Dann das leere Repository entfernen, damit
|
||||
# es nicht weiter (ohne Tags) im Katalog stehen bleibt.
|
||||
if not repo_has_tags(name) and remove_repository_storage(name):
|
||||
flash(
|
||||
f'Image "{name}:{tag}" geloescht - Repository "{name}" entfernt '
|
||||
"(keine Tags mehr).",
|
||||
"success",
|
||||
)
|
||||
else:
|
||||
flash(f'Image "{name}:{tag}" wurde geloescht.', "success")
|
||||
else:
|
||||
flash(f'Image "{name}:{tag}" konnte nicht geloescht werden.', "error")
|
||||
@@ -465,9 +521,11 @@ def garbage_collect():
|
||||
for line in result.stdout.splitlines()
|
||||
if line.startswith("blob eligible for deletion")
|
||||
)
|
||||
pruned = prune_empty_repositories()
|
||||
extra = f" {pruned} leere(s) Repository entfernt." if pruned else ""
|
||||
flash(
|
||||
"Garbage Collection abgeschlossen. "
|
||||
f"{freed} nicht mehr benoetigte Blob(s) wurden entfernt.",
|
||||
f"{freed} nicht mehr benoetigte Blob(s) wurden entfernt.{extra}",
|
||||
"success",
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -188,8 +188,9 @@
|
||||
<p style="margin-top: 0.6rem; font-size: 0.85rem; color: #6b7280; line-height: 1.6;">
|
||||
Beim Loeschen eines Images verschwindet der Tag sofort, der belegte Speicherplatz wird
|
||||
aber erst hierdurch freigegeben. Entfernt alle Daten, die von keinem Image mehr
|
||||
referenziert werden. <strong>Wichtig:</strong> waehrend der Aufraeumung keine Images
|
||||
hochladen, sonst koennen frisch hochgeladene Daten verloren gehen.
|
||||
referenziert werden, sowie leere Repositories ohne Tags.
|
||||
<strong>Wichtig:</strong> waehrend der Aufraeumung keine Images hochladen, sonst
|
||||
koennen frisch hochgeladene Daten verloren gehen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user