added list images
This commit is contained in:
parent
0da0bfe8a3
commit
244138b6bd
|
|
@ -0,0 +1,16 @@
|
|||
# ===========================================
|
||||
# Docker Registry - Konfiguration
|
||||
# ===========================================
|
||||
|
||||
# Domain unter der die Registry erreichbar ist
|
||||
# Muss auf die IP des Servers zeigen (DNS A-Record)
|
||||
# Caddy holt automatisch ein Let's Encrypt Zertifikat
|
||||
DOMAIN=registry.example.com
|
||||
|
||||
# Admin-Zugangsdaten fuer die Benutzerverwaltungs-Weboberflaeche
|
||||
ADMIN_USER=admin
|
||||
ADMIN_PASSWORD=changeme
|
||||
|
||||
# Geheimer Schluessel fuer Flask-Sessions (bitte aendern!)
|
||||
# Mit ./generate-secret.sh automatisch generieren
|
||||
SECRET_KEY=bitte-aendern-zu-einem-zufaelligen-schluessel
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.env
|
||||
!.env.example
|
||||
data/
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import os
|
||||
import secrets
|
||||
import sqlite3
|
||||
from functools import wraps
|
||||
|
||||
import bcrypt as bc
|
||||
import requests as http_client
|
||||
from flask import (
|
||||
Flask,
|
||||
flash,
|
||||
|
|
@ -20,6 +22,12 @@ ADMIN_USER = os.environ.get("ADMIN_USER", "admin")
|
|||
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "changeme")
|
||||
DB_PATH = os.environ.get("DB_PATH", "/data/users.db")
|
||||
HTPASSWD_PATH = os.environ.get("HTPASSWD_PATH", "/auth/htpasswd")
|
||||
REGISTRY_URL = os.environ.get("REGISTRY_URL", "http://registry:5000")
|
||||
|
||||
# Internal service account for registry API queries (not stored in SQLite)
|
||||
_SERVICE_USER = "_service"
|
||||
_SERVICE_PASSWORD = secrets.token_hex(16)
|
||||
_SERVICE_PASSWORD_HASH = None
|
||||
|
||||
|
||||
def get_db():
|
||||
|
|
@ -53,13 +61,29 @@ def hash_password(password):
|
|||
|
||||
|
||||
def sync_htpasswd():
|
||||
"""Regenerate htpasswd file from all users in SQLite."""
|
||||
"""Regenerate htpasswd file from all users in SQLite + internal service user."""
|
||||
conn = get_db()
|
||||
users = conn.execute("SELECT username, password_hash FROM users").fetchall()
|
||||
conn.close()
|
||||
with open(HTPASSWD_PATH, "w") as f:
|
||||
for user in users:
|
||||
f.write(f"{user['username']}:{user['password_hash']}\n")
|
||||
f.write(f"{_SERVICE_USER}:{_SERVICE_PASSWORD_HASH}\n")
|
||||
|
||||
|
||||
def query_registry(path):
|
||||
"""Query the registry API using the internal service account."""
|
||||
try:
|
||||
resp = http_client.get(
|
||||
f"{REGISTRY_URL}{path}",
|
||||
auth=(_SERVICE_USER, _SERVICE_PASSWORD),
|
||||
timeout=5,
|
||||
)
|
||||
if resp.ok:
|
||||
return resp.json()
|
||||
except http_client.RequestException:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def login_required(f):
|
||||
|
|
@ -101,6 +125,25 @@ def users():
|
|||
return render_template("users.html", users=user_list)
|
||||
|
||||
|
||||
@app.route("/images")
|
||||
@login_required
|
||||
def images():
|
||||
repos = []
|
||||
data = query_registry("/v2/_catalog")
|
||||
if data:
|
||||
for name in sorted(data.get("repositories", [])):
|
||||
tags_data = query_registry(f"/v2/{name}/tags/list")
|
||||
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)
|
||||
|
||||
|
||||
@app.route("/help")
|
||||
@login_required
|
||||
def help_page():
|
||||
return render_template("help.html", domain=request.host)
|
||||
|
||||
|
||||
@app.route("/add", methods=["POST"])
|
||||
@login_required
|
||||
def add_user():
|
||||
|
|
@ -175,4 +218,5 @@ def delete_user(user_id):
|
|||
|
||||
# Initialize database and htpasswd on startup
|
||||
init_db()
|
||||
_SERVICE_PASSWORD_HASH = hash_password(_SERVICE_PASSWORD)
|
||||
sync_htpasswd()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
flask>=3.0
|
||||
bcrypt>=4.0
|
||||
gunicorn>=21.0
|
||||
requests>=2.31
|
||||
|
|
|
|||
|
|
@ -37,13 +37,28 @@
|
|||
.inline-form { display: flex; gap: 0.5rem; align-items: center; }
|
||||
.inline-form input { width: 150px; }
|
||||
.empty { text-align: center; padding: 2rem; color: #9ca3af; }
|
||||
nav { display: flex; gap: 1.5rem; align-items: center; }
|
||||
nav a { color: #94a3b8; text-decoration: none; font-size: 0.9rem; padding: 0.25rem 0; border-bottom: 2px solid transparent; }
|
||||
nav a:hover { color: #fff; }
|
||||
nav a.active { color: #fff; border-bottom-color: #2563eb; }
|
||||
.tag { display: inline-block; background: #e0e7ff; color: #3730a3; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.8rem; margin: 0.15rem; font-family: monospace; }
|
||||
code { background: #f3f4f6; padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.85rem; }
|
||||
pre { background: #1e293b; color: #e2e8f0; padding: 1rem; border-radius: 6px; overflow-x: auto; font-size: 0.85rem; line-height: 1.6; margin: 0.75rem 0; }
|
||||
pre code { background: none; padding: 0; color: inherit; }
|
||||
.help-step { margin-bottom: 0.5rem; }
|
||||
.help-step strong { color: #374151; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Docker Registry</h1>
|
||||
{% if session.get('logged_in') %}
|
||||
<nav>
|
||||
<a href="{{ url_for('users') }}" {% if request.endpoint == 'users' %}class="active"{% endif %}>Benutzer</a>
|
||||
<a href="{{ url_for('images') }}" {% if request.endpoint == 'images' %}class="active"{% endif %}>Images</a>
|
||||
<a href="{{ url_for('help_page') }}" {% if request.endpoint == 'help_page' %}class="active"{% endif %}>Hilfe</a>
|
||||
<a href="{{ url_for('logout') }}">Abmelden</a>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</header>
|
||||
<div class="container">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Hilfe - Docker Registry{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="card">
|
||||
<h2>Anmelden</h2>
|
||||
<p class="help-step">Mit einem in der <a href="{{ url_for('users') }}">Benutzerverwaltung</a> angelegten Account:</p>
|
||||
<pre><code>docker login {{ domain }}</code></pre>
|
||||
<p class="help-step" style="color: #6b7280; font-size: 0.85rem;">Benutzername und Passwort werden interaktiv abgefragt.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Image hochladen (push)</h2>
|
||||
<p class="help-step"><strong>1.</strong> Lokales Image fuer die Registry taggen:</p>
|
||||
<pre><code>docker tag mein-image:latest {{ domain }}/mein-image:latest</code></pre>
|
||||
<p class="help-step"><strong>2.</strong> Image hochladen:</p>
|
||||
<pre><code>docker push {{ domain }}/mein-image:latest</code></pre>
|
||||
<p class="help-step" style="margin-top: 1rem;"><strong>Oder direkt beim Build:</strong></p>
|
||||
<pre><code>docker build -t {{ domain }}/mein-app:1.0 .
|
||||
docker push {{ domain }}/mein-app:1.0</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Image herunterladen (pull)</h2>
|
||||
<pre><code>docker pull {{ domain }}/mein-image:latest</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>In docker-compose.yml verwenden</h2>
|
||||
<pre><code>services:
|
||||
meine-app:
|
||||
image: {{ domain }}/mein-image:latest</code></pre>
|
||||
<p class="help-step" style="color: #6b7280; font-size: 0.85rem;">Vorher auf dem Zielserver <code>docker login {{ domain }}</code> ausfuehren.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Nuetzliche Befehle</h2>
|
||||
<p class="help-step"><strong>Alle Images auflisten:</strong></p>
|
||||
<pre><code>curl -u benutzer:passwort https://{{ domain }}/v2/_catalog</code></pre>
|
||||
<p class="help-step"><strong>Tags eines Images anzeigen:</strong></p>
|
||||
<pre><code>curl -u benutzer:passwort https://{{ domain }}/v2/mein-image/tags/list</code></pre>
|
||||
<p class="help-step"><strong>Abmelden:</strong></p>
|
||||
<pre><code>docker logout {{ domain }}</code></pre>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Images - Docker Registry{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<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 }}</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>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -58,13 +58,4 @@
|
|||
<p class="empty">Noch keine Benutzer angelegt.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Nutzung</h2>
|
||||
<p style="font-size: 0.9rem; color: #6b7280; line-height: 1.6;">
|
||||
Nach dem Anlegen eines Benutzers kann sich dieser mit <code>docker login {{ request.host }}</code> anmelden
|
||||
und Images pushen/pullen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Reference in New Issue