cfc4866377
Pull (einzeln/Bulk) und Garbage Collection laufen synchron und koennen mehrere Minuten dauern. Ohne Rueckmeldung wirkte es, als ob nach dem Klick "nichts passiert". Jetzt erscheint beim Abschicken ein Overlay mit Spinner und Hinweistext, bis die Antwort da ist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
228 lines
12 KiB
HTML
228 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Images - Docker Registry{% endblock %}
|
|
|
|
{# Wiederverwendbarer Zugangsdaten-Block (Benutzername/Kennwort ODER Access Token).
|
|
prefix unterscheidet die Felder von Einzel- und Bulk-Formular. #}
|
|
{% macro cred_fields(prefix) %}
|
|
<p style="margin-top: 0.75rem; font-size: 0.8rem; color: #6b7280;">
|
|
Nur bei privaten Quell-Images noetig – passend ankreuzen, was du hast:
|
|
</p>
|
|
<div class="form-group" style="margin-top: 0.4rem;">
|
|
<label style="display: flex; align-items: center; gap: 0.5rem; font-weight: 600;">
|
|
<input type="checkbox" id="{{ prefix }}use_creds" name="{{ prefix }}use_creds"
|
|
onchange="toggleCredMode('{{ prefix }}', 'creds')" style="width: auto;">
|
|
Benutzername / Kennwort
|
|
</label>
|
|
</div>
|
|
<div id="{{ prefix }}creds_box" style="display: none;">
|
|
<div class="form-row" style="margin-top: 0.5rem;">
|
|
<div class="form-group">
|
|
<label for="{{ prefix }}src_username">Benutzername</label>
|
|
<input type="text" id="{{ prefix }}src_username" name="{{ prefix }}src_username" autocomplete="off">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="{{ prefix }}src_password">Kennwort</label>
|
|
<input type="password" id="{{ prefix }}src_password" name="{{ prefix }}src_password" autocomplete="new-password">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group" style="margin-top: 0.6rem;">
|
|
<label style="display: flex; align-items: center; gap: 0.5rem; font-weight: 600;">
|
|
<input type="checkbox" id="{{ prefix }}use_token" name="{{ prefix }}use_token"
|
|
onchange="toggleCredMode('{{ prefix }}', 'token')" style="width: auto;">
|
|
Access Token
|
|
</label>
|
|
</div>
|
|
<div id="{{ prefix }}token_box" style="display: none;">
|
|
<div class="form-group" style="margin-top: 0.5rem;">
|
|
<label for="{{ prefix }}src_token">Access Token</label>
|
|
<input type="password" id="{{ prefix }}src_token" name="{{ prefix }}src_token" autocomplete="new-password"
|
|
placeholder="Bearer-Token der Quell-Registry">
|
|
</div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% block content %}
|
|
|
|
<div class="card">
|
|
<h2>Image aus anderer Registry holen</h2>
|
|
<form method="post" action="{{ url_for('pull_image') }}"
|
|
onsubmit="showBusy('Image wird geholt – das kann je nach Groesse einige Minuten dauern. Bitte warten…');">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="source">Quell-Image</label>
|
|
<input type="text" id="source" name="source"
|
|
placeholder="z. B. nginx:latest oder bitnami/redis:7" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Holen</button>
|
|
</div>
|
|
{{ cred_fields('') }}
|
|
</form>
|
|
|
|
<div style="margin-top: 1.25rem; font-size: 0.9rem; color: #6b7280; line-height: 1.7;">
|
|
<p class="help-step"><strong>So funktioniert es</strong></p>
|
|
<p class="help-step">
|
|
Trage den Namen eines Images aus einer anderen Registry ein (z. B. von Docker Hub)
|
|
und klicke auf <strong>Holen</strong>. Das Image wird dann heruntergeladen und in
|
|
diese Registry kopiert (alle Architekturen).
|
|
</p>
|
|
<p class="help-step">Beispiele fuer das Quell-Image:</p>
|
|
<ul style="margin: 0.25rem 0 0.5rem 1.25rem;">
|
|
<li><code>nginx:latest</code> – offizielles Image von Docker Hub</li>
|
|
<li><code>bitnami/redis:7</code> – Image eines Docker-Hub-Benutzers</li>
|
|
<li><code>ghcr.io/owner/app:v1</code> – Image aus einer anderen Registry (z. B. GitHub)</li>
|
|
</ul>
|
|
<p class="help-step">
|
|
<strong>Public Images</strong> (der Normalfall, z. B. von Docker Hub) brauchen
|
|
keine Zugangsdaten – einfach nur den Namen eingeben und Holen.
|
|
</p>
|
|
<p class="help-step">
|
|
Nur bei <strong>privaten</strong> Quell-Images Zugangsdaten angeben – je nachdem,
|
|
was du hast:
|
|
</p>
|
|
<ul style="margin: 0.25rem 0 0.5rem 1.25rem;">
|
|
<li><strong>Benutzername / Kennwort</strong> – fuer Docker Hub und die meisten
|
|
Registries. Bei Docker Hub hier das <em>Access Token als Kennwort</em> eintragen.</li>
|
|
<li><strong>Access Token</strong> – fuer Registries, die einen direkten
|
|
Bearer-Token akzeptieren.</li>
|
|
</ul>
|
|
<p class="help-step">
|
|
Ist ein Image mit dem gleichen Digest bereits vorhanden, wird es automatisch
|
|
uebersprungen (kein Doppel-Upload – auch bei <code>latest</code>).
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Mehrere Images aus docker-compose holen</h2>
|
|
<form method="post" action="{{ url_for('pull_compose') }}" enctype="multipart/form-data"
|
|
onsubmit="showBusy('Images werden geholt – bei mehreren oder grossen Images kann das einige Minuten dauern. Bitte warten…');">
|
|
<div class="form-group">
|
|
<label for="compose">docker-compose einfuegen</label>
|
|
<textarea id="compose" name="compose" rows="10"
|
|
placeholder="services: web: image: nginx:latest cache: image: redis:7"></textarea>
|
|
</div>
|
|
<div class="form-group" style="margin-top: 0.75rem;">
|
|
<label for="compose_file">… oder eine Datei hochladen</label>
|
|
<input type="file" id="compose_file" name="compose_file"
|
|
accept=".yml,.yaml,.txt,text/yaml,text/plain">
|
|
</div>
|
|
{{ cred_fields('bulk_') }}
|
|
<div style="margin-top: 0.9rem;">
|
|
<button type="submit" class="btn btn-primary">Alle Images holen</button>
|
|
</div>
|
|
</form>
|
|
|
|
<div style="margin-top: 1.25rem; font-size: 0.9rem; color: #6b7280; line-height: 1.7;">
|
|
<p class="help-step"><strong>So funktioniert es</strong></p>
|
|
<p class="help-step">
|
|
Fuege den Inhalt einer <code>docker-compose.yml</code> ein oder lade die Datei hoch
|
|
und klicke auf <strong>Alle Images holen</strong>. Aus jedem Service wird der
|
|
<code>image:</code>-Eintrag gelesen und das Image in diese Registry kopiert.
|
|
</p>
|
|
<p class="help-step">
|
|
Eintraege mit nicht aufgeloesten Variablen (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 class="help-step">
|
|
Etwaige Zugangsdaten gelten fuer <em>alle</em> Images der Compose-Datei.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Gespeicherte Images</h2>
|
|
{% if repos %}
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Image</th>
|
|
<th>Tags</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for repo in repos %}
|
|
<tr>
|
|
<td><strong>{{ request.host }}/{{ repo.name }}</strong></td>
|
|
<td>
|
|
{% if repo.tags %}
|
|
{% for tag in repo.tags %}
|
|
<span class="tag tag-deletable">
|
|
{{ tag }}
|
|
<form method="post" action="{{ url_for('delete_image') }}" class="tag-delete-form"
|
|
onsubmit="return confirm('Image {{ repo.name }}:{{ tag }} wirklich loeschen?');">
|
|
<input type="hidden" name="name" value="{{ repo.name }}">
|
|
<input type="hidden" name="tag" value="{{ tag }}">
|
|
<button type="submit" title="Image loeschen" aria-label="Image loeschen">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="3 6 5 6 21 6"></polyline>
|
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
|
<line x1="10" y1="11" x2="10" y2="17"></line>
|
|
<line x1="14" y1="11" x2="14" y2="17"></line>
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
</span>
|
|
{% endfor %}
|
|
{% else %}
|
|
<span style="color: #9ca3af;">keine Tags</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="empty">Noch keine Images in der Registry vorhanden.</p>
|
|
{% endif %}
|
|
|
|
<div style="margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid #e5e7eb;">
|
|
<form method="post" action="{{ url_for('garbage_collect') }}"
|
|
onsubmit="if(!confirm('Garbage Collection jetzt ausfuehren? Loesche waehrenddessen keine Images und pushe nichts.')) return false; showBusy('Speicher wird aufgeraeumt. Bitte warten…');">
|
|
<button type="submit" class="btn btn-secondary">Speicher aufraeumen (Garbage Collection)</button>
|
|
</form>
|
|
<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.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="busy-overlay"
|
|
style="display: none; position: fixed; inset: 0; background: rgba(15,23,42,0.6);
|
|
z-index: 100; align-items: center; justify-content: center;">
|
|
<div style="background: #fff; padding: 1.5rem 2rem; border-radius: 8px; max-width: 420px;
|
|
text-align: center; box-shadow: 0 10px 25px rgba(0,0,0,0.2);">
|
|
<div class="spinner" style="width: 32px; height: 32px; margin: 0 auto 1rem;
|
|
border: 3px solid #e5e7eb; border-top-color: #2563eb; border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;"></div>
|
|
<p id="busy-text" style="font-size: 0.95rem; color: #374151; line-height: 1.5;">Bitte warten…</p>
|
|
</div>
|
|
</div>
|
|
<style>@keyframes spin { to { transform: rotate(360deg); } }</style>
|
|
|
|
<script>
|
|
// Vollbild-Hinweis fuer langlaufende Aktionen (Pull / Garbage Collection).
|
|
function showBusy(htmlText) {
|
|
document.getElementById('busy-text').innerHTML = htmlText;
|
|
document.getElementById('busy-overlay').style.display = 'flex';
|
|
return true;
|
|
}
|
|
|
|
// Benutzername/Kennwort und Access Token schliessen sich gegenseitig aus.
|
|
function toggleCredMode(prefix, mode) {
|
|
var creds = document.getElementById(prefix + 'use_creds');
|
|
var token = document.getElementById(prefix + 'use_token');
|
|
if (mode === 'creds' && creds.checked) { token.checked = false; }
|
|
if (mode === 'token' && token.checked) { creds.checked = false; }
|
|
document.getElementById(prefix + 'creds_box').style.display = creds.checked ? 'block' : 'none';
|
|
document.getElementById(prefix + 'token_box').style.display = token.checked ? 'block' : 'none';
|
|
}
|
|
</script>
|
|
|
|
{% endblock %}
|