diff --git a/auth-app/app.py b/auth-app/app.py index 0a60401..c3e48e3 100644 --- a/auth-app/app.py +++ b/auth-app/app.py @@ -221,23 +221,33 @@ def repo_has_tags(name): return bool(data and data.get("tags")) +def _repositories_base(): + return os.path.realpath( + os.path.join(REGISTRY_DATA, "docker", "registry", "v2", "repositories") + ) + + +def _safe_repo_path(*parts): + """Absoluter Pfad innerhalb des repositories-Verzeichnisses oder None (Schutz vor ../).""" + base = _repositories_base() + target = os.path.realpath(os.path.join(base, *parts)) + if target != base and not target.startswith(base + os.sep): + return None + return target + + 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): + target = _safe_repo_path(*name.split("/")) + if not target or not os.path.isdir(target): return False shutil.rmtree(target, ignore_errors=True) # Leere Eltern-Verzeichnisse (z. B. "bitnami" nach "bitnami/redis") aufraeumen + base = _repositories_base() parent = os.path.dirname(target) while parent != base and os.path.isdir(parent) and not os.listdir(parent): try: @@ -248,6 +258,19 @@ def remove_repository_storage(name): return True +def remove_tag_storage(name, tag): + """Entfernt den Tag-Verweis direkt aus dem Speicher. + + Noetig fuer verwaiste Tags, die noch im Katalog stehen, deren Manifest + aber nicht mehr abrufbar ist (Loeschen ueber die API schlaegt dann fehl). + """ + target = _safe_repo_path(*name.split("/"), "_manifests", "tags", tag) + if not target or not os.path.isdir(target): + return False + shutil.rmtree(target, ignore_errors=True) + return True + + def prune_empty_repositories(): """Entfernt alle Repositories ohne Tags aus dem Speicher. Gibt Anzahl zurueck.""" data = query_registry("/v2/_catalog") @@ -481,23 +504,26 @@ def delete_image(): return redirect(url_for("images")) digest = get_manifest_digest(name, tag) - if not digest: + # Normalfall: Manifest per Digest loeschen. Faellt der API-Lookup aus + # (verwaister Tag: im Katalog gelistet, Manifest aber nicht abrufbar), + # den Tag-Verweis direkt aus dem Speicher entfernen. + manifest_deleted = delete_manifest(name, digest) if digest else False + tag_removed = remove_tag_storage(name, tag) + + if not manifest_deleted and not tag_removed: flash(f'Tag "{tag}" von "{name}" wurde nicht gefunden.', "error") 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") + # 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}" konnte nicht geloescht werden.', "error") + flash(f'Image "{name}:{tag}" wurde geloescht.', "success") return redirect(url_for("images"))