Compare commits
6 Commits
cfc4866377
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a1b4500d6 | |||
| b24bd96917 | |||
| cd5beeedd3 | |||
| cd9e33bdfe | |||
| 4a11aabdf3 | |||
| b273098b50 |
@@ -113,9 +113,10 @@ uebersprungen – so wird ein Doppel-Upload vermieden, auch wenn der Tag (z.
|
|||||||
|
|
||||||
Im selben Tab kann der Inhalt einer `docker-compose.yml` in ein Textfeld eingefuegt
|
Im selben Tab kann der Inhalt einer `docker-compose.yml` in ein Textfeld eingefuegt
|
||||||
oder eine Datei hochgeladen werden. Mit **Alle Images holen** werden alle
|
oder eine Datei hochgeladen werden. Mit **Alle Images holen** werden alle
|
||||||
`image:`-Eintraege der Services in die Registry kopiert. Eintraege mit nicht
|
`image:`-Eintraege der Services in die Registry kopiert. Variablen mit Standardwert
|
||||||
aufgeloesten Variablen (z. B. `${TAG}`) oder die bereits auf diese Registry zeigen,
|
(z. B. `${GRAYLOG_IMAGE:-graylog/graylog:7.1}`) werden mit ihrem Standardwert
|
||||||
werden uebersprungen.
|
aufgeloest. Eintraege mit Variablen *ohne* Standardwert (z. B. `${TAG}`) oder die
|
||||||
|
bereits auf diese Registry zeigen, werden uebersprungen.
|
||||||
|
|
||||||
## Images loeschen und Speicher aufraeumen (Web-Oberflaeche)
|
## Images loeschen und Speicher aufraeumen (Web-Oberflaeche)
|
||||||
|
|
||||||
|
|||||||
+147
-18
@@ -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]
|
||||||
@@ -126,12 +128,22 @@ def split_dest(dest):
|
|||||||
return repo, tag
|
return repo, tag
|
||||||
|
|
||||||
|
|
||||||
|
# Loest ${VAR:-default} und ${VAR-default} zum Default-Wert auf.
|
||||||
|
# ${VAR}, ${VAR:?...} usw. (ohne Default) werden NICHT aufgeloest.
|
||||||
|
_VAR_DEFAULT_RE = re.compile(r"\$\{[A-Za-z_][A-Za-z0-9_]*:?-([^}]*)\}")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_compose_image(image):
|
||||||
|
"""Ersetzt Compose-Variablen mit Default (${VAR:-default}) durch den Default."""
|
||||||
|
return _VAR_DEFAULT_RE.sub(lambda m: m.group(1), image)
|
||||||
|
|
||||||
|
|
||||||
def extract_compose_images(content):
|
def extract_compose_images(content):
|
||||||
"""Liest alle 'image:'-Eintraege aus den Services einer docker-compose.
|
"""Liest alle 'image:'-Eintraege aus den Services einer docker-compose.
|
||||||
|
|
||||||
Gibt (images, ignored) zurueck. images ist None bei ungueltigem YAML.
|
Gibt (images, ignored) zurueck. images ist None bei ungueltigem YAML.
|
||||||
Eintraege mit Variablen (${...}) oder auf die eigene Registry werden
|
Variablen mit Default (${VAR:-wert}) werden aufgeloest. Eintraege mit
|
||||||
uebersprungen und in ignored gesammelt.
|
nicht aufloesbaren Variablen oder auf die eigene Registry landen in ignored.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = yaml.safe_load(content)
|
data = yaml.safe_load(content)
|
||||||
@@ -147,13 +159,14 @@ def extract_compose_images(content):
|
|||||||
for svc in services.values():
|
for svc in services.values():
|
||||||
if not isinstance(svc, dict):
|
if not isinstance(svc, dict):
|
||||||
continue
|
continue
|
||||||
image = svc.get("image")
|
raw = svc.get("image")
|
||||||
if not isinstance(image, str) or not image.strip():
|
if not isinstance(raw, str) or not raw.strip():
|
||||||
continue
|
continue
|
||||||
image = image.strip()
|
raw = raw.strip()
|
||||||
|
image = resolve_compose_image(raw)
|
||||||
host = image.split("/", 1)[0] if "/" in image else ""
|
host = image.split("/", 1)[0] if "/" in image else ""
|
||||||
if "$" in image or "{" in image:
|
if not image or "$" in image or "{" in image or "}" in image:
|
||||||
ignored.append(image) # nicht aufgeloeste Variable
|
ignored.append(raw) # Variable ohne Default - nicht aufloesbar
|
||||||
elif host and host in (REGISTRY_HOST, request.host):
|
elif host and host in (REGISTRY_HOST, request.host):
|
||||||
ignored.append(image) # zeigt bereits auf unsere Registry
|
ignored.append(image) # zeigt bereits auf unsere Registry
|
||||||
elif not _IMAGE_REF_RE.match(image):
|
elif not _IMAGE_REF_RE.match(image):
|
||||||
@@ -202,6 +215,104 @@ 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 _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 scan_fs_repositories():
|
||||||
|
"""Listet alle Repositories direkt aus dem Speicher (Dateisystem).
|
||||||
|
|
||||||
|
Erfasst auch Repositories, die der Registry-Katalog (/v2/_catalog) nicht
|
||||||
|
zeigt - etwa nach Korruption, wenn das _layers-Verzeichnis fehlt.
|
||||||
|
"""
|
||||||
|
base = _repositories_base()
|
||||||
|
repos = []
|
||||||
|
if not os.path.isdir(base):
|
||||||
|
return repos
|
||||||
|
for root, dirs, _files in os.walk(base):
|
||||||
|
if "_manifests" in dirs:
|
||||||
|
name = os.path.relpath(root, base).replace(os.sep, "/")
|
||||||
|
if name != ".":
|
||||||
|
repos.append(name)
|
||||||
|
dirs[:] = [] # innerhalb eines Repos nicht weiter absteigen
|
||||||
|
return repos
|
||||||
|
|
||||||
|
|
||||||
|
def repo_tags(name):
|
||||||
|
"""Tags eines Repositories - aus dem Dateisystem, mit API als Fallback."""
|
||||||
|
tags_dir = _safe_repo_path(*name.split("/"), "_manifests", "tags")
|
||||||
|
if tags_dir and os.path.isdir(tags_dir):
|
||||||
|
return sorted(
|
||||||
|
d for d in os.listdir(tags_dir) if os.path.isdir(os.path.join(tags_dir, d))
|
||||||
|
)
|
||||||
|
data = query_registry(f"/v2/{name}/tags/list")
|
||||||
|
return sorted(data.get("tags") or []) if data else []
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
os.rmdir(parent)
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
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")
|
||||||
|
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}"]
|
||||||
@@ -308,13 +419,13 @@ def users():
|
|||||||
@app.route("/images")
|
@app.route("/images")
|
||||||
@login_required
|
@login_required
|
||||||
def images():
|
def images():
|
||||||
repos = []
|
# Repos aus dem Dateisystem UND dem Katalog (Vereinigung), damit auch
|
||||||
|
# Repositories sichtbar/loeschbar sind, die der Katalog nicht zeigt.
|
||||||
|
names = set(scan_fs_repositories())
|
||||||
data = query_registry("/v2/_catalog")
|
data = query_registry("/v2/_catalog")
|
||||||
if data:
|
if data:
|
||||||
for name in sorted(data.get("repositories", [])):
|
names.update(data.get("repositories", []))
|
||||||
tags_data = query_registry(f"/v2/{name}/tags/list")
|
repos = [{"name": name, "tags": repo_tags(name)} for name in sorted(names)]
|
||||||
tags = sorted(tags_data.get("tags", [])) if tags_data and tags_data.get("tags") else []
|
|
||||||
repos.append({"name": name, "tags": tags})
|
|
||||||
return render_template("images.html", repos=repos)
|
return render_template("images.html", repos=repos)
|
||||||
|
|
||||||
|
|
||||||
@@ -423,14 +534,26 @@ def delete_image():
|
|||||||
return redirect(url_for("images"))
|
return redirect(url_for("images"))
|
||||||
|
|
||||||
digest = get_manifest_digest(name, tag)
|
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")
|
flash(f'Tag "{tag}" von "{name}" wurde nicht gefunden.', "error")
|
||||||
return redirect(url_for("images"))
|
return redirect(url_for("images"))
|
||||||
|
|
||||||
if delete_manifest(name, digest):
|
# War das der letzte Tag? Dann das leere Repository entfernen, damit
|
||||||
flash(f'Image "{name}:{tag}" wurde geloescht.', "success")
|
# 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:
|
else:
|
||||||
flash(f'Image "{name}:{tag}" konnte nicht geloescht werden.', "error")
|
flash(f'Image "{name}:{tag}" wurde geloescht.', "success")
|
||||||
|
|
||||||
return redirect(url_for("images"))
|
return redirect(url_for("images"))
|
||||||
|
|
||||||
@@ -441,9 +564,13 @@ def garbage_collect():
|
|||||||
# Entfernt Blobs/Layer, die von keinem Manifest mehr referenziert werden
|
# Entfernt Blobs/Layer, die von keinem Manifest mehr referenziert werden
|
||||||
# (z. B. nach dem Loeschen von Tags). Nutzt die registry-Binary direkt auf
|
# (z. B. nach dem Loeschen von Tags). Nutzt die registry-Binary direkt auf
|
||||||
# dem gemounteten Speicher - kein Docker-Socket noetig.
|
# dem gemounteten Speicher - kein Docker-Socket noetig.
|
||||||
|
#
|
||||||
|
# WICHTIG: KEIN --delete-untagged! Dieses Flag loescht bei Multi-Arch-Images
|
||||||
|
# die per-Architektur-Manifeste (die nur ueber die Manifest-Liste referenziert
|
||||||
|
# sind) und macht die Images damit unbrauchbar ("manifest unknown" beim Pull).
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["registry", "garbage-collect", GC_CONFIG, "--delete-untagged"],
|
["registry", "garbage-collect", GC_CONFIG],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=600,
|
timeout=600,
|
||||||
@@ -454,9 +581,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:
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
|
input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
|
||||||
textarea { padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.85rem; width: 100%; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; }
|
textarea { padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.85rem; width: 100%; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; }
|
||||||
textarea:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
|
textarea:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
|
||||||
|
.pw-wrap { position: relative; }
|
||||||
|
.pw-wrap input { padding-right: 2.5rem; }
|
||||||
|
.pw-toggle { position: absolute; top: 50%; right: 0.5rem; transform: translateY(-50%); background: none; border: none; padding: 0.25rem; cursor: pointer; color: #6b7280; display: flex; align-items: center; }
|
||||||
|
.pw-toggle:hover { color: #374151; }
|
||||||
.btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 0.85rem; cursor: pointer; font-weight: 500; }
|
.btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-size: 0.85rem; cursor: pointer; font-weight: 500; }
|
||||||
.btn-primary { background: #2563eb; color: #fff; }
|
.btn-primary { background: #2563eb; color: #fff; }
|
||||||
.btn-primary:hover { background: #1d4ed8; }
|
.btn-primary:hover { background: #1d4ed8; }
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Images - Docker Registry{% endblock %}
|
{% block title %}Images - Docker Registry{% endblock %}
|
||||||
|
|
||||||
|
{# Passwort-Feld mit Auge zum Ein-/Ausblenden der Eingabe. #}
|
||||||
|
{% macro pw_field(fid, ph='') %}
|
||||||
|
<div class="pw-wrap">
|
||||||
|
<input type="password" id="{{ fid }}" name="{{ fid }}" autocomplete="new-password"
|
||||||
|
{% if ph %}placeholder="{{ ph }}"{% endif %}>
|
||||||
|
<button type="button" class="pw-toggle" onclick="togglePw('{{ fid }}', this)"
|
||||||
|
title="Anzeigen/Verbergen" aria-label="Eingabe anzeigen oder verbergen">
|
||||||
|
<svg class="eye-on" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
<svg class="eye-off" style="display: none;" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
|
||||||
|
<line x1="1" y1="1" x2="23" y2="23"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{# Wiederverwendbarer Zugangsdaten-Block (Benutzername/Kennwort ODER Access Token).
|
{# Wiederverwendbarer Zugangsdaten-Block (Benutzername/Kennwort ODER Access Token).
|
||||||
prefix unterscheidet die Felder von Einzel- und Bulk-Formular. #}
|
prefix unterscheidet die Felder von Einzel- und Bulk-Formular. #}
|
||||||
{% macro cred_fields(prefix) %}
|
{% macro cred_fields(prefix) %}
|
||||||
@@ -22,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ prefix }}src_password">Kennwort</label>
|
<label for="{{ prefix }}src_password">Kennwort</label>
|
||||||
<input type="password" id="{{ prefix }}src_password" name="{{ prefix }}src_password" autocomplete="new-password">
|
{{ pw_field(prefix ~ 'src_password') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,8 +57,7 @@
|
|||||||
<div id="{{ prefix }}token_box" style="display: none;">
|
<div id="{{ prefix }}token_box" style="display: none;">
|
||||||
<div class="form-group" style="margin-top: 0.5rem;">
|
<div class="form-group" style="margin-top: 0.5rem;">
|
||||||
<label for="{{ prefix }}src_token">Access Token</label>
|
<label for="{{ prefix }}src_token">Access Token</label>
|
||||||
<input type="password" id="{{ prefix }}src_token" name="{{ prefix }}src_token" autocomplete="new-password"
|
{{ pw_field(prefix ~ 'src_token', 'Bearer-Token der Quell-Registry') }}
|
||||||
placeholder="Bearer-Token der Quell-Registry">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
@@ -121,9 +141,11 @@
|
|||||||
<code>image:</code>-Eintrag gelesen und das Image in diese Registry kopiert.
|
<code>image:</code>-Eintrag gelesen und das Image in diese Registry kopiert.
|
||||||
</p>
|
</p>
|
||||||
<p class="help-step">
|
<p class="help-step">
|
||||||
Eintraege mit nicht aufgeloesten Variablen (z. B. <code>${TAG}</code>) oder die bereits
|
Variablen mit Standardwert (z. B. <code>${GRAYLOG_IMAGE:-graylog/graylog:7.1}</code>)
|
||||||
auf diese Registry zeigen, werden uebersprungen. Bereits vorhandene Images (gleicher
|
werden automatisch mit ihrem Standardwert aufgeloest. Eintraege mit Variablen
|
||||||
Digest) werden ebenfalls nicht erneut geladen.
|
<em>ohne</em> Standardwert (z. B. <code>${TAG}</code>) oder die bereits auf diese
|
||||||
|
Registry zeigen, werden uebersprungen. Bereits vorhandene Images (gleicher Digest)
|
||||||
|
werden ebenfalls nicht erneut geladen.
|
||||||
</p>
|
</p>
|
||||||
<p class="help-step">
|
<p class="help-step">
|
||||||
Etwaige Zugangsdaten gelten fuer <em>alle</em> Images der Compose-Datei.
|
Etwaige Zugangsdaten gelten fuer <em>alle</em> Images der Compose-Datei.
|
||||||
@@ -186,8 +208,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>
|
||||||
@@ -213,6 +236,15 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auge: Eingabe ein-/ausblenden.
|
||||||
|
function togglePw(id, btn) {
|
||||||
|
var inp = document.getElementById(id);
|
||||||
|
var show = inp.type === 'password';
|
||||||
|
inp.type = show ? 'text' : 'password';
|
||||||
|
btn.querySelector('.eye-on').style.display = show ? 'none' : '';
|
||||||
|
btn.querySelector('.eye-off').style.display = show ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Benutzername/Kennwort und Access Token schliessen sich gegenseitig aus.
|
// Benutzername/Kennwort und Access Token schliessen sich gegenseitig aus.
|
||||||
function toggleCredMode(prefix, mode) {
|
function toggleCredMode(prefix, mode) {
|
||||||
var creds = document.getElementById(prefix + 'use_creds');
|
var creds = document.getElementById(prefix + 'use_creds');
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ services:
|
|||||||
REGISTRY_AUTH_HTPASSWD_REALM: Docker Registry
|
REGISTRY_AUTH_HTPASSWD_REALM: Docker Registry
|
||||||
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
|
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
|
||||||
REGISTRY_STORAGE_DELETE_ENABLED: "true"
|
REGISTRY_STORAGE_DELETE_ENABLED: "true"
|
||||||
|
# Blob-Cache deaktivieren: Die Garbage Collection laeuft als separater
|
||||||
|
# Prozess auf dem gemounteten Speicher. Mit aktivem In-Memory-Cache wuerde
|
||||||
|
# die laufende Registry geloeschte Blobs faelschlich fuer vorhanden halten
|
||||||
|
# und beim erneuten Pull keine Layer-Verknuepfung anlegen (Image fehlt dann
|
||||||
|
# im Katalog / "manifest unknown"). Ohne Cache ist die Live-GC sicher.
|
||||||
|
REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR: ""
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/registry:/var/lib/registry
|
- ./data/registry:/var/lib/registry
|
||||||
- ./data/htpasswd:/auth:ro
|
- ./data/htpasswd:/auth:ro
|
||||||
|
|||||||
Reference in New Issue
Block a user