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; }

Docker Registry

{% if session.get('logged_in') %} - Abmelden + {% endif %}
diff --git a/auth-app/templates/help.html b/auth-app/templates/help.html new file mode 100644 index 0000000..1cc9c45 --- /dev/null +++ b/auth-app/templates/help.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% block title %}Hilfe - Docker Registry{% endblock %} +{% block content %} + +
+

Anmelden

+

Mit einem in der Benutzerverwaltung angelegten Account:

+
docker login {{ domain }}
+

Benutzername und Passwort werden interaktiv abgefragt.

+
+ +
+

Image hochladen (push)

+

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
+
+ +
+

Image herunterladen (pull)

+
docker pull {{ domain }}/mein-image:latest
+
+ +
+

In docker-compose.yml verwenden

+
services:
+  meine-app:
+    image: {{ domain }}/mein-image:latest
+

Vorher auf dem Zielserver docker login {{ domain }} ausfuehren.

+
+ +
+

Nuetzliche Befehle

+

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 }}
+
+ +{% endblock %} diff --git a/auth-app/templates/images.html b/auth-app/templates/images.html new file mode 100644 index 0000000..55dd0bc --- /dev/null +++ b/auth-app/templates/images.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% block title %}Images - Docker Registry{% endblock %} +{% block content %} + +
+

Gespeicherte Images

+ {% if repos %} + + + + + + + + + {% for repo in repos %} + + + + + {% endfor %} + +
ImageTags
{{ request.host }}/{{ repo.name }} + {% if repo.tags %} + {% for tag in repo.tags %} + {{ tag }} + {% endfor %} + {% else %} + keine Tags + {% endif %} +
+ {% else %} +

Noch keine Images in der Registry vorhanden.

+ {% endif %} +
+ +{% endblock %} diff --git a/auth-app/templates/users.html b/auth-app/templates/users.html index 182565f..ac19aae 100644 --- a/auth-app/templates/users.html +++ b/auth-app/templates/users.html @@ -58,13 +58,4 @@

Noch keine Benutzer angelegt.

{% endif %}
- -
-

Nutzung

-

- Nach dem Anlegen eines Benutzers kann sich dieser mit docker login {{ request.host }} anmelden - und Images pushen/pullen. -

-
- {% endblock %}