diff --git a/auth-app/app.py b/auth-app/app.py index 92a81a2..0a60401 100644 --- a/auth-app/app.py +++ b/auth-app/app.py @@ -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,7 +486,16 @@ def delete_image(): return redirect(url_for("images")) if delete_manifest(name, digest): - flash(f'Image "{name}:{tag}" wurde geloescht.', "success") + # 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: diff --git a/auth-app/templates/images.html b/auth-app/templates/images.html index 07e382c..8b10390 100644 --- a/auth-app/templates/images.html +++ b/auth-app/templates/images.html @@ -188,8 +188,9 @@
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. Wichtig: waehrend der Aufraeumung keine Images - hochladen, sonst koennen frisch hochgeladene Daten verloren gehen. + referenziert werden, sowie leere Repositories ohne Tags. + Wichtig: waehrend der Aufraeumung keine Images hochladen, sonst + koennen frisch hochgeladene Daten verloren gehen.