diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..59b7686 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..618767c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +!.env.example +data/ diff --git a/auth-app/app.py b/auth-app/app.py index 45736bf..77dade4 100644 --- a/auth-app/app.py +++ b/auth-app/app.py @@ -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() diff --git a/auth-app/requirements.txt b/auth-app/requirements.txt index 340eecd..b57b5e5 100644 --- a/auth-app/requirements.txt +++ b/auth-app/requirements.txt @@ -1,3 +1,4 @@ flask>=3.0 bcrypt>=4.0 gunicorn>=21.0 +requests>=2.31 diff --git a/auth-app/templates/base.html b/auth-app/templates/base.html index 8967549..d1a9756 100644 --- a/auth-app/templates/base.html +++ b/auth-app/templates/base.html @@ -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; }
Mit einem in der Benutzerverwaltung angelegten Account:
+docker login {{ domain }}
+ Benutzername und Passwort werden interaktiv abgefragt.
+1. Lokales Image fuer die Registry taggen:
+docker tag mein-image:latest {{ domain }}/mein-image:latest
+ 2. Image hochladen:
+docker push {{ domain }}/mein-image:latest
+ Oder direkt beim Build:
+docker build -t {{ domain }}/mein-app:1.0 .
+docker push {{ domain }}/mein-app:1.0
+docker pull {{ domain }}/mein-image:latest
+services:
+ meine-app:
+ image: {{ domain }}/mein-image:latest
+ Vorher auf dem Zielserver docker login {{ domain }} ausfuehren.
Alle Images auflisten:
+curl -u benutzer:passwort https://{{ domain }}/v2/_catalog
+ Tags eines Images anzeigen:
+curl -u benutzer:passwort https://{{ domain }}/v2/mein-image/tags/list
+ Abmelden:
+docker logout {{ domain }}
+| Image | +Tags | +
|---|---|
| {{ request.host }}/{{ repo.name }} | ++ {% if repo.tags %} + {% for tag in repo.tags %} + {{ tag }} + {% endfor %} + {% else %} + keine Tags + {% endif %} + | +
Noch keine Images in der Registry vorhanden.
+ {% endif %} +Noch keine Benutzer angelegt.
{% endif %}
- Nach dem Anlegen eines Benutzers kann sich dieser mit docker login {{ request.host }} anmelden
- und Images pushen/pullen.
-