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:
+60
-2
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import subprocess
|
import subprocess
|
||||||
from functools import wraps
|
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")
|
DB_PATH = os.environ.get("DB_PATH", "/data/users.db")
|
||||||
HTPASSWD_PATH = os.environ.get("HTPASSWD_PATH", "/auth/htpasswd")
|
HTPASSWD_PATH = os.environ.get("HTPASSWD_PATH", "/auth/htpasswd")
|
||||||
GC_CONFIG = os.environ.get("GC_CONFIG", "/app/registry-gc-config.yml")
|
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")
|
REGISTRY_URL = os.environ.get("REGISTRY_URL", "http://registry:5000")
|
||||||
# Host:Port ohne Schema, fuer skopeo-Ziel (z. B. "registry:5000")
|
# Host:Port ohne Schema, fuer skopeo-Ziel (z. B. "registry:5000")
|
||||||
REGISTRY_HOST = REGISTRY_URL.split("://", 1)[-1]
|
REGISTRY_HOST = REGISTRY_URL.split("://", 1)[-1]
|
||||||
@@ -213,6 +215,51 @@ def delete_manifest(name, digest):
|
|||||||
return False
|
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):
|
def remote_digest(source, src_creds=None, src_token=None):
|
||||||
"""Liefert den Manifest-Digest eines Quell-Images, ohne es herunterzuladen."""
|
"""Liefert den Manifest-Digest eines Quell-Images, ohne es herunterzuladen."""
|
||||||
cmd = ["skopeo", "inspect", "--format", "{{.Digest}}", f"docker://{source}"]
|
cmd = ["skopeo", "inspect", "--format", "{{.Digest}}", f"docker://{source}"]
|
||||||
@@ -439,7 +486,16 @@ def delete_image():
|
|||||||
return redirect(url_for("images"))
|
return redirect(url_for("images"))
|
||||||
|
|
||||||
if delete_manifest(name, digest):
|
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:
|
else:
|
||||||
flash(f'Image "{name}:{tag}" konnte nicht geloescht werden.', "error")
|
flash(f'Image "{name}:{tag}" konnte nicht geloescht werden.', "error")
|
||||||
|
|
||||||
@@ -465,9 +521,11 @@ def garbage_collect():
|
|||||||
for line in result.stdout.splitlines()
|
for line in result.stdout.splitlines()
|
||||||
if line.startswith("blob eligible for deletion")
|
if line.startswith("blob eligible for deletion")
|
||||||
)
|
)
|
||||||
|
pruned = prune_empty_repositories()
|
||||||
|
extra = f" {pruned} leere(s) Repository entfernt." if pruned else ""
|
||||||
flash(
|
flash(
|
||||||
"Garbage Collection abgeschlossen. "
|
"Garbage Collection abgeschlossen. "
|
||||||
f"{freed} nicht mehr benoetigte Blob(s) wurden entfernt.",
|
f"{freed} nicht mehr benoetigte Blob(s) wurden entfernt.{extra}",
|
||||||
"success",
|
"success",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -188,8 +188,9 @@
|
|||||||
<p style="margin-top: 0.6rem; font-size: 0.85rem; color: #6b7280; line-height: 1.6;">
|
<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
|
Beim Loeschen eines Images verschwindet der Tag sofort, der belegte Speicherplatz wird
|
||||||
aber erst hierdurch freigegeben. Entfernt alle Daten, die von keinem Image mehr
|
aber erst hierdurch freigegeben. Entfernt alle Daten, die von keinem Image mehr
|
||||||
referenziert werden. <strong>Wichtig:</strong> waehrend der Aufraeumung keine Images
|
referenziert werden, sowie leere Repositories ohne Tags.
|
||||||
hochladen, sonst koennen frisch hochgeladene Daten verloren gehen.
|
<strong>Wichtig:</strong> waehrend der Aufraeumung keine Images hochladen, sonst
|
||||||
|
koennen frisch hochgeladene Daten verloren gehen.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user