cd9e33bdfe
Kennwort- und Access-Token-Feld haben jetzt ein Augen-Symbol, mit dem die Eingabe sichtbar gemacht und kontrolliert werden kann. Umgesetzt ueber ein wiederverwendbares pw_field-Macro (gilt fuer Einzel- und Bulk-Formular). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
260 lines
13 KiB
HTML
260 lines
13 KiB
HTML
{% extends "base.html" %}
|
|
{% 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).
|
|
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>
|
|
{{ pw_field(prefix ~ 'src_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>
|
|
{{ pw_field(prefix ~ 'src_token', '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">
|
|
Variablen mit Standardwert (z. B. <code>${GRAYLOG_IMAGE:-graylog/graylog:7.1}</code>)
|
|
werden automatisch mit ihrem Standardwert aufgeloest. Eintraege mit Variablen
|
|
<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 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, sowie leere Repositories ohne Tags.
|
|
<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;
|
|
}
|
|
|
|
// 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.
|
|
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 %}
|