katalon-cli 0.1.20__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. katalon_cli-0.3.0/PKG-INFO +171 -0
  2. katalon_cli-0.3.0/README.md +157 -0
  3. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/pyproject.toml +1 -1
  4. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/pyproject.toml.orig +1 -1
  5. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/core/compose_gen.py +31 -4
  6. katalon_cli-0.3.0/src/katalon_cli/core/permissions.py +113 -0
  7. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/core/state.py +1 -1
  8. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/main.py +128 -7
  9. katalon_cli-0.3.0/src/katalon_cli/templates/Caddyfile.j2 +3 -0
  10. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/templates/compose.yaml.j2 +49 -3
  11. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/templates/nginx.conf.j2 +24 -6
  12. katalon_cli-0.1.20/PKG-INFO +0 -94
  13. katalon_cli-0.1.20/README.md +0 -80
  14. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/__init__.py +0 -0
  15. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/core/__init__.py +0 -0
  16. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/core/backup.py +0 -0
  17. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/core/checks.py +0 -0
  18. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/core/docker.py +0 -0
  19. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/core/paths.py +0 -0
  20. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/core/reindex.py +0 -0
  21. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/core/release.py +0 -0
  22. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/core/selfupdate.py +0 -0
  23. {katalon_cli-0.1.20 → katalon_cli-0.3.0}/src/katalon_cli/py.typed +0 -0
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.3
2
+ Name: katalon-cli
3
+ Version: 0.3.0
4
+ Summary: Installer & Updater CLI für Katalon Production-Instanzen
5
+ Author: Karl Krägelin
6
+ Author-email: Karl Krägelin <karlkraegelin@gmail.com>
7
+ Requires-Dist: typer>=0.15
8
+ Requires-Dist: rich>=13.9
9
+ Requires-Dist: httpx>=0.28
10
+ Requires-Dist: pydantic>=2.9
11
+ Requires-Dist: jinja2>=3.1
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+
15
+ # katalon-cli
16
+
17
+ Installer & Updater für [Katalon Collections](https://github.com/katalon-collections/katalon) Production-Instanzen.
18
+ Verwaltet eine Instanz unter einem frei gewählten Zielverzeichnis — pullt gepinnte
19
+ Release-Images, generiert `compose.yaml`, macht Backups vor jedem Update, kann rollbacken.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ curl -LsSf https://astral.sh/uv/install.sh | sh
25
+ uv tool install katalon-cli
26
+ ```
27
+
28
+ ## Nutzung
29
+
30
+ ```bash
31
+ katalon install # interaktiver Setup-Wizard (Rich-Prompts + Progress)
32
+ katalon start / stop / status
33
+ katalon check-updates # prüft nur, ob ein neues Release verfügbar ist
34
+ katalon update # interaktives Update, Backup zuerst immer
35
+ katalon rollback # letztes Backup einspielen
36
+ katalon doctor # Docker, Diskspace, Ports, Schreibrechte von MEDIA_ROOT prüfen
37
+ katalon backup # manuelles Backup
38
+ katalon logs [service]
39
+ katalon manage <args> # katalon-manage im api-Container (z.B. reset-admin, create-user)
40
+ katalon restore DUMP # DB durch pg_dump ersetzen (-Fc oder SQL), danach Reindex
41
+ katalon reindex # Suchindex neu aufbauen, mit Fortschrittsanzeige
42
+ ```
43
+
44
+ Ein DB-Dump enthält nicht den Suchindex (Elasticsearch). `restore` baut ihn deshalb nach dem
45
+ Einspielen neu auf (`--no-reindex` zum Überspringen, später `katalon reindex`). Bei großen
46
+ Beständen dauert das lange (ca. 45 Min. für 80k Datensätze). Außerdem prüft `katalon` einmal
47
+ täglich auf neue Versionen (abschalten: `KATALON_NO_UPDATE_CHECK=1`).
48
+
49
+ `install` ohne `--dir` fragt das Zielverzeichnis interaktiv ab. Nach erfolgreicher Einrichtung
50
+ wird der absolute Pfad benutzerweit in `~/.config/katalon/instance` gespeichert.
51
+ Bei gesetztem, absolutem `XDG_CONFIG_HOME` liegt die Datei stattdessen unter
52
+ `$XDG_CONFIG_HOME/katalon/instance`. Das Konfigurationsverzeichnis wird bei Bedarf angelegt.
53
+ Alle weiteren Befehle verwenden den gespeicherten Pfad, unabhängig vom Arbeitsverzeichnis.
54
+ Ohne gespeicherten Pfad ist der Default auf allen Plattformen `~/katalon`.
55
+
56
+ Alle Befehle unterstützen `--dir PATH`: überschreibt den Pfad nur für diesen Aufruf.
57
+ Eine erfolgreiche neue Installation speichert ihr Zielverzeichnis als neuen Default.
58
+ Bestehende Instanzen lassen sich weiterhin mit `--dir` verwenden; alternativ ihren absoluten
59
+ Pfad als einzige Zeile in die Konfigurationsdatei eintragen.
60
+ `install`/`update` fragen interaktiv (Auswahl über `rich.prompt`),
61
+ `update`/`rollback` haben `--yes` zum Überspringen der Rückfrage.
62
+
63
+ ```bash
64
+ katalon install --dir ~/meine-katalon-instanz
65
+ katalon start
66
+ katalon status
67
+ ```
68
+
69
+ ## API-Adresse
70
+
71
+ Die REST-API liegt direkt unter der Base-URL, ohne `/api`-Präfix: `<KATALON_BASE_URL>/v1/...`
72
+ (z. B. Login unter `http://127.0.0.1:8080/v1/...` bei `behind-proxy`). Unter `/api/` liegen nur
73
+ die OpenAPI-Dokumentation (`/api/docs`, `/api/openapi.json`) und interne Endpunkte;
74
+ `/api/v1/...` liefert für normale API-Aufrufe 404.
75
+
76
+ ## Medienverzeichnis und Rechte
77
+
78
+ API und Worker laufen im Container als unprivilegierter User (`USER` im Image). `MEDIA_ROOT`
79
+ ist ein Bind-Mount vom Host und muss für diesen User beschreibbar sein. `katalon start`
80
+ (auch beim Install), `update` und `rollback` lesen UID/GID per `id` aus dem api-Container und
81
+ setzen in einem kurzlebigen root-Container desselben Images den Eigentümer aller Einträge
82
+ unter `MEDIA_ROOT`, die noch einem anderen Eigentümer gehören. Dateiinhalte werden nicht
83
+ verändert; ein erneuter Lauf ändert nichts. `katalon doctor` meldet ein nicht beschreibbares
84
+ `MEDIA_ROOT`, bevor Uploads mit HTTP 500 scheitern.
85
+
86
+ ## Bild-Cache (Cantaloupe)
87
+
88
+ Vorschaubilder und Viewer-Kacheln rechnet Cantaloupe aus den Originalen und legt sie im
89
+ Instanzverzeichnis unter `cantaloupe-cache/` ab (abweichend: `CANTALOUPE_CACHE_DIR` in `.env`).
90
+ Der Cache übersteht Neustarts und Updates; ein kurzlebiger Init-Container übergibt das
91
+ Verzeichnis vor jedem Start dem Cantaloupe-User. Abgelaufene Derivate
92
+ (`CANTALOUPE_CACHE_SERVER_DERIVATIVE_TTL_SECONDS`, Standard 30 Tage) löscht Cantaloupe selbst.
93
+
94
+ Eine Größenbegrenzung kennt Cantaloupe nicht. Als Richtwert ~25 KB pro Vorschaubild plus
95
+ Kacheln der aufgerufenen Detailansichten einplanen; wer den Platz hart begrenzen muss, legt
96
+ `CANTALOUPE_CACHE_DIR` auf ein eigenes Dateisystem bzw. Quota-Verzeichnis. Hosts ohne
97
+ Plattenplatz nutzen mit `CANTALOUPE_FILESYSTEMCACHE_PATHNAME=/tmpfs/cache` einen RAM-Cache
98
+ (`CANTALOUPE_CACHE_TMPFS_SIZE`, Standard `512m`), der nach jedem Neustart leer ist.
99
+
100
+ Vorschaubilder lassen sich vorab berechnen, damit schon der erste Aufruf der Trefferliste
101
+ schnell ist: `katalon media warm-thumbnails` (Hauptbilder zuerst, dann die übrigen; mit
102
+ `--primary-only` nur Hauptbilder, `--status` zeigt den Fortschritt). Der Lauf arbeitet im
103
+ Worker in kleinen Paketen, belegt höchstens `THUMBNAIL_WARM_CONCURRENCY` (Standard 3)
104
+ Cantaloupe-Kerne und setzt nach einer Unterbrechung beim letzten Stand fort. Neu hochgeladene
105
+ Bilder werden automatisch vorberechnet.
106
+
107
+ Bestehende Instanzen stellt `katalon update` automatisch um (neue `compose.yaml`); der bisherige
108
+ RAM-Cache entfällt dabei. Releases, die eine neuere CLI voraussetzen, bricht `katalon update`
109
+ mit einem Hinweis auf `uv tool upgrade katalon-cli` ab.
110
+
111
+ ## Domain und HTTPS
112
+
113
+ Für eine lokale Installation bleibt `http://localhost` mit TLS-Modus `none` der Standard.
114
+ Bei einer öffentlichen Domain schlägt der Setup-Wizard `caddy` vor. Du kannst auch nur
115
+ `sammlung.example.org` eingeben; mit Caddy wird daraus `https://sammlung.example.org`.
116
+
117
+ Vor der Installation müssen die DNS-Einträge wirksam sein: Der A-Record zeigt auf die öffentliche
118
+ IPv4-Adresse des Servers. Ein vorhandener AAAA-Record muss auf dessen erreichbare IPv6-Adresse
119
+ zeigen. TCP-Ports 80 und 443 müssen frei und öffentlich erreichbar sein, auch durch Server- und
120
+ Provider-Firewalls sowie gegebenenfalls Portweiterleitungen. Docker und Docker Compose müssen
121
+ installiert sein. Die CLI prüft freie Ports lokal, aber keine öffentliche DNS-Auflösung oder
122
+ Erreichbarkeit von außen.
123
+
124
+ Die TLS-Modi:
125
+
126
+ - `caddy`: Caddy läuft als Docker-Container vor dem internen nginx, belegt Ports 80 und 443,
127
+ beantragt öffentlich vertrauenswürdige Zertifikate und erneuert sie automatisch.
128
+ HTTP wird auf HTTPS umgeleitet. Zertifikate und Zustand liegen dauerhaft in den Docker-Volumes
129
+ `caddy_data` und `caddy_config`, die bei Neustarts, Updates und Rollbacks erhalten bleiben.
130
+ Dafür muss Caddy weiterlaufen, DNS muss stimmen und der Server erreichbar bleiben.
131
+ - `standalone`: nginx verwendet ein selbstsigniertes Zertifikat. Browser zeigen eine Warnung;
132
+ es gibt keine automatische Erneuerung. Eigene Zertifikate können unter `<dir>/certs/`
133
+ als `fullchain.pem` und `privkey.pem` abgelegt werden. Ihre Erneuerung und das anschließende
134
+ Neuladen von nginx musst du selbst einrichten.
135
+ - `behind-proxy`: Ein eigener Reverse-Proxy übernimmt HTTPS und Zertifikate.
136
+ Katalon ist standardmäßig nur auf `127.0.0.1:8080` erreichbar.
137
+ - `none`: HTTP ohne TLS, für lokale Tests oder IP-Adressen.
138
+
139
+ Die erste Zertifikatsausstellung kann nach dem Start kurz dauern. Bei HTTPS-Problemen:
140
+ `katalon logs caddy`. Caddy-Konfiguration und Zertifikatsverwaltung sind in der
141
+ [Caddy-Dokumentation](https://caddyserver.com/docs/automatic-https) beschrieben.
142
+ Die CLI erzeugt `<dir>/Caddyfile` bei Installation, Update und Rollback neu;
143
+ instanzspezifische Compose-Anpassungen gehören in `compose.override.yaml`.
144
+
145
+ ## Entwicklung
146
+
147
+ ```bash
148
+ uv sync
149
+ uv run katalon --help
150
+ uv run pytest
151
+ ```
152
+
153
+ ## Architektur
154
+
155
+ - `core/state.py` — `installation.json`, Single Source of Truth für laufende Version.
156
+ - `core/release.py` — GitHub-Release-Metadaten (`katalon-release.json` Asset). Enthält
157
+ optional `env_vars`/`deprecated_env_vars`: `install`/`update` ergänzen neue Pflichtvariablen
158
+ automatisch (Secret generieren oder Default übernehmen), fragen bei fehlendem Default interaktiv
159
+ nach (`--yes` bricht dann ab), tragen neue optionale Variablen auskommentiert in `.env` ein und
160
+ warnen vor veralteten Variablen, die noch in der `.env` stehen.
161
+ - `core/compose_gen.py` + `templates/compose.yaml.j2` + `templates/nginx.conf.j2` — rendert `compose.yaml`
162
+ + `nginx.conf` und bei `tls_mode=caddy` das `Caddyfile` aus Version + TLS-Modus;
163
+ erzeugt bei `tls_mode=standalone` ein selbstsigniertes
164
+ Zertifikat unter `<dir>/certs/` (eigenes Zertifikat dort ablegen, um es zu ersetzen).
165
+ `install` fragt zusätzlich `MEDIA_ROOT` ab und legt `<dir>/compose.override.yaml.example` an
166
+ (instanzspezifische Overrides — umbenennen zu `compose.override.yaml`, wird automatisch eingebunden
167
+ und bei Updates nicht angefasst). Optionale Env-Vars (Admin-Login, SMTP, Geonames, …) landet
168
+ auskommentiert in `.env`.
169
+ - `core/backup.py` — `pg_dump` vor jedem Update; Rollback restauriert Dump statt `alembic downgrade`.
170
+ - `core/docker.py` — dünner `docker compose`-Subprocess-Wrapper.
171
+ - `main.py` — Typer-Commands, interaktive Teile über `rich.prompt`/`rich.progress`.
@@ -0,0 +1,157 @@
1
+ # katalon-cli
2
+
3
+ Installer & Updater für [Katalon Collections](https://github.com/katalon-collections/katalon) Production-Instanzen.
4
+ Verwaltet eine Instanz unter einem frei gewählten Zielverzeichnis — pullt gepinnte
5
+ Release-Images, generiert `compose.yaml`, macht Backups vor jedem Update, kann rollbacken.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ curl -LsSf https://astral.sh/uv/install.sh | sh
11
+ uv tool install katalon-cli
12
+ ```
13
+
14
+ ## Nutzung
15
+
16
+ ```bash
17
+ katalon install # interaktiver Setup-Wizard (Rich-Prompts + Progress)
18
+ katalon start / stop / status
19
+ katalon check-updates # prüft nur, ob ein neues Release verfügbar ist
20
+ katalon update # interaktives Update, Backup zuerst immer
21
+ katalon rollback # letztes Backup einspielen
22
+ katalon doctor # Docker, Diskspace, Ports, Schreibrechte von MEDIA_ROOT prüfen
23
+ katalon backup # manuelles Backup
24
+ katalon logs [service]
25
+ katalon manage <args> # katalon-manage im api-Container (z.B. reset-admin, create-user)
26
+ katalon restore DUMP # DB durch pg_dump ersetzen (-Fc oder SQL), danach Reindex
27
+ katalon reindex # Suchindex neu aufbauen, mit Fortschrittsanzeige
28
+ ```
29
+
30
+ Ein DB-Dump enthält nicht den Suchindex (Elasticsearch). `restore` baut ihn deshalb nach dem
31
+ Einspielen neu auf (`--no-reindex` zum Überspringen, später `katalon reindex`). Bei großen
32
+ Beständen dauert das lange (ca. 45 Min. für 80k Datensätze). Außerdem prüft `katalon` einmal
33
+ täglich auf neue Versionen (abschalten: `KATALON_NO_UPDATE_CHECK=1`).
34
+
35
+ `install` ohne `--dir` fragt das Zielverzeichnis interaktiv ab. Nach erfolgreicher Einrichtung
36
+ wird der absolute Pfad benutzerweit in `~/.config/katalon/instance` gespeichert.
37
+ Bei gesetztem, absolutem `XDG_CONFIG_HOME` liegt die Datei stattdessen unter
38
+ `$XDG_CONFIG_HOME/katalon/instance`. Das Konfigurationsverzeichnis wird bei Bedarf angelegt.
39
+ Alle weiteren Befehle verwenden den gespeicherten Pfad, unabhängig vom Arbeitsverzeichnis.
40
+ Ohne gespeicherten Pfad ist der Default auf allen Plattformen `~/katalon`.
41
+
42
+ Alle Befehle unterstützen `--dir PATH`: überschreibt den Pfad nur für diesen Aufruf.
43
+ Eine erfolgreiche neue Installation speichert ihr Zielverzeichnis als neuen Default.
44
+ Bestehende Instanzen lassen sich weiterhin mit `--dir` verwenden; alternativ ihren absoluten
45
+ Pfad als einzige Zeile in die Konfigurationsdatei eintragen.
46
+ `install`/`update` fragen interaktiv (Auswahl über `rich.prompt`),
47
+ `update`/`rollback` haben `--yes` zum Überspringen der Rückfrage.
48
+
49
+ ```bash
50
+ katalon install --dir ~/meine-katalon-instanz
51
+ katalon start
52
+ katalon status
53
+ ```
54
+
55
+ ## API-Adresse
56
+
57
+ Die REST-API liegt direkt unter der Base-URL, ohne `/api`-Präfix: `<KATALON_BASE_URL>/v1/...`
58
+ (z. B. Login unter `http://127.0.0.1:8080/v1/...` bei `behind-proxy`). Unter `/api/` liegen nur
59
+ die OpenAPI-Dokumentation (`/api/docs`, `/api/openapi.json`) und interne Endpunkte;
60
+ `/api/v1/...` liefert für normale API-Aufrufe 404.
61
+
62
+ ## Medienverzeichnis und Rechte
63
+
64
+ API und Worker laufen im Container als unprivilegierter User (`USER` im Image). `MEDIA_ROOT`
65
+ ist ein Bind-Mount vom Host und muss für diesen User beschreibbar sein. `katalon start`
66
+ (auch beim Install), `update` und `rollback` lesen UID/GID per `id` aus dem api-Container und
67
+ setzen in einem kurzlebigen root-Container desselben Images den Eigentümer aller Einträge
68
+ unter `MEDIA_ROOT`, die noch einem anderen Eigentümer gehören. Dateiinhalte werden nicht
69
+ verändert; ein erneuter Lauf ändert nichts. `katalon doctor` meldet ein nicht beschreibbares
70
+ `MEDIA_ROOT`, bevor Uploads mit HTTP 500 scheitern.
71
+
72
+ ## Bild-Cache (Cantaloupe)
73
+
74
+ Vorschaubilder und Viewer-Kacheln rechnet Cantaloupe aus den Originalen und legt sie im
75
+ Instanzverzeichnis unter `cantaloupe-cache/` ab (abweichend: `CANTALOUPE_CACHE_DIR` in `.env`).
76
+ Der Cache übersteht Neustarts und Updates; ein kurzlebiger Init-Container übergibt das
77
+ Verzeichnis vor jedem Start dem Cantaloupe-User. Abgelaufene Derivate
78
+ (`CANTALOUPE_CACHE_SERVER_DERIVATIVE_TTL_SECONDS`, Standard 30 Tage) löscht Cantaloupe selbst.
79
+
80
+ Eine Größenbegrenzung kennt Cantaloupe nicht. Als Richtwert ~25 KB pro Vorschaubild plus
81
+ Kacheln der aufgerufenen Detailansichten einplanen; wer den Platz hart begrenzen muss, legt
82
+ `CANTALOUPE_CACHE_DIR` auf ein eigenes Dateisystem bzw. Quota-Verzeichnis. Hosts ohne
83
+ Plattenplatz nutzen mit `CANTALOUPE_FILESYSTEMCACHE_PATHNAME=/tmpfs/cache` einen RAM-Cache
84
+ (`CANTALOUPE_CACHE_TMPFS_SIZE`, Standard `512m`), der nach jedem Neustart leer ist.
85
+
86
+ Vorschaubilder lassen sich vorab berechnen, damit schon der erste Aufruf der Trefferliste
87
+ schnell ist: `katalon media warm-thumbnails` (Hauptbilder zuerst, dann die übrigen; mit
88
+ `--primary-only` nur Hauptbilder, `--status` zeigt den Fortschritt). Der Lauf arbeitet im
89
+ Worker in kleinen Paketen, belegt höchstens `THUMBNAIL_WARM_CONCURRENCY` (Standard 3)
90
+ Cantaloupe-Kerne und setzt nach einer Unterbrechung beim letzten Stand fort. Neu hochgeladene
91
+ Bilder werden automatisch vorberechnet.
92
+
93
+ Bestehende Instanzen stellt `katalon update` automatisch um (neue `compose.yaml`); der bisherige
94
+ RAM-Cache entfällt dabei. Releases, die eine neuere CLI voraussetzen, bricht `katalon update`
95
+ mit einem Hinweis auf `uv tool upgrade katalon-cli` ab.
96
+
97
+ ## Domain und HTTPS
98
+
99
+ Für eine lokale Installation bleibt `http://localhost` mit TLS-Modus `none` der Standard.
100
+ Bei einer öffentlichen Domain schlägt der Setup-Wizard `caddy` vor. Du kannst auch nur
101
+ `sammlung.example.org` eingeben; mit Caddy wird daraus `https://sammlung.example.org`.
102
+
103
+ Vor der Installation müssen die DNS-Einträge wirksam sein: Der A-Record zeigt auf die öffentliche
104
+ IPv4-Adresse des Servers. Ein vorhandener AAAA-Record muss auf dessen erreichbare IPv6-Adresse
105
+ zeigen. TCP-Ports 80 und 443 müssen frei und öffentlich erreichbar sein, auch durch Server- und
106
+ Provider-Firewalls sowie gegebenenfalls Portweiterleitungen. Docker und Docker Compose müssen
107
+ installiert sein. Die CLI prüft freie Ports lokal, aber keine öffentliche DNS-Auflösung oder
108
+ Erreichbarkeit von außen.
109
+
110
+ Die TLS-Modi:
111
+
112
+ - `caddy`: Caddy läuft als Docker-Container vor dem internen nginx, belegt Ports 80 und 443,
113
+ beantragt öffentlich vertrauenswürdige Zertifikate und erneuert sie automatisch.
114
+ HTTP wird auf HTTPS umgeleitet. Zertifikate und Zustand liegen dauerhaft in den Docker-Volumes
115
+ `caddy_data` und `caddy_config`, die bei Neustarts, Updates und Rollbacks erhalten bleiben.
116
+ Dafür muss Caddy weiterlaufen, DNS muss stimmen und der Server erreichbar bleiben.
117
+ - `standalone`: nginx verwendet ein selbstsigniertes Zertifikat. Browser zeigen eine Warnung;
118
+ es gibt keine automatische Erneuerung. Eigene Zertifikate können unter `<dir>/certs/`
119
+ als `fullchain.pem` und `privkey.pem` abgelegt werden. Ihre Erneuerung und das anschließende
120
+ Neuladen von nginx musst du selbst einrichten.
121
+ - `behind-proxy`: Ein eigener Reverse-Proxy übernimmt HTTPS und Zertifikate.
122
+ Katalon ist standardmäßig nur auf `127.0.0.1:8080` erreichbar.
123
+ - `none`: HTTP ohne TLS, für lokale Tests oder IP-Adressen.
124
+
125
+ Die erste Zertifikatsausstellung kann nach dem Start kurz dauern. Bei HTTPS-Problemen:
126
+ `katalon logs caddy`. Caddy-Konfiguration und Zertifikatsverwaltung sind in der
127
+ [Caddy-Dokumentation](https://caddyserver.com/docs/automatic-https) beschrieben.
128
+ Die CLI erzeugt `<dir>/Caddyfile` bei Installation, Update und Rollback neu;
129
+ instanzspezifische Compose-Anpassungen gehören in `compose.override.yaml`.
130
+
131
+ ## Entwicklung
132
+
133
+ ```bash
134
+ uv sync
135
+ uv run katalon --help
136
+ uv run pytest
137
+ ```
138
+
139
+ ## Architektur
140
+
141
+ - `core/state.py` — `installation.json`, Single Source of Truth für laufende Version.
142
+ - `core/release.py` — GitHub-Release-Metadaten (`katalon-release.json` Asset). Enthält
143
+ optional `env_vars`/`deprecated_env_vars`: `install`/`update` ergänzen neue Pflichtvariablen
144
+ automatisch (Secret generieren oder Default übernehmen), fragen bei fehlendem Default interaktiv
145
+ nach (`--yes` bricht dann ab), tragen neue optionale Variablen auskommentiert in `.env` ein und
146
+ warnen vor veralteten Variablen, die noch in der `.env` stehen.
147
+ - `core/compose_gen.py` + `templates/compose.yaml.j2` + `templates/nginx.conf.j2` — rendert `compose.yaml`
148
+ + `nginx.conf` und bei `tls_mode=caddy` das `Caddyfile` aus Version + TLS-Modus;
149
+ erzeugt bei `tls_mode=standalone` ein selbstsigniertes
150
+ Zertifikat unter `<dir>/certs/` (eigenes Zertifikat dort ablegen, um es zu ersetzen).
151
+ `install` fragt zusätzlich `MEDIA_ROOT` ab und legt `<dir>/compose.override.yaml.example` an
152
+ (instanzspezifische Overrides — umbenennen zu `compose.override.yaml`, wird automatisch eingebunden
153
+ und bei Updates nicht angefasst). Optionale Env-Vars (Admin-Login, SMTP, Geonames, …) landet
154
+ auskommentiert in `.env`.
155
+ - `core/backup.py` — `pg_dump` vor jedem Update; Rollback restauriert Dump statt `alembic downgrade`.
156
+ - `core/docker.py` — dünner `docker compose`-Subprocess-Wrapper.
157
+ - `main.py` — Typer-Commands, interaktive Teile über `rich.prompt`/`rich.progress`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "katalon-cli"
3
- version = "0.1.20"
3
+ version = "0.3.0"
4
4
  description = "Installer & Updater CLI für Katalon Production-Instanzen"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "katalon-cli"
3
- version = "0.1.20"
3
+ version = "0.3.0"
4
4
  description = "Installer & Updater CLI für Katalon Production-Instanzen"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -2,8 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  import subprocess
7
+ from ipaddress import ip_address
6
8
  from pathlib import Path
9
+ from urllib.parse import urlparse
7
10
 
8
11
  from jinja2 import Environment, PackageLoader, select_autoescape
9
12
 
@@ -22,8 +25,8 @@ def render_compose(
22
25
  tls_mode: str,
23
26
  registry: str = "ghcr.io/katalon-collections/katalon",
24
27
  ) -> str:
25
- from urllib.parse import urlparse
26
-
28
+ if tls_mode == "caddy":
29
+ caddy_host(base_url)
27
30
  parsed_port = urlparse(base_url).port
28
31
  template = _env.get_template("compose.yaml.j2")
29
32
  return template.render(
@@ -47,6 +50,10 @@ def write_compose(instance_dir: Path, **kwargs) -> Path:
47
50
  nginx_path = instance_dir / "nginx.conf"
48
51
  nginx_path.write_text(render_nginx_conf(tls_mode=kwargs["tls_mode"]))
49
52
 
53
+ if kwargs["tls_mode"] == "caddy":
54
+ template = _env.get_template("Caddyfile.j2")
55
+ (instance_dir / "Caddyfile").write_text(template.render(host=caddy_host(kwargs["base_url"])))
56
+
50
57
  if kwargs["tls_mode"] == "standalone":
51
58
  ensure_self_signed_cert(instance_dir, host=_host_of(kwargs["base_url"]))
52
59
 
@@ -79,7 +86,27 @@ def ensure_self_signed_cert(instance_dir: Path, *, host: str) -> None:
79
86
  )
80
87
 
81
88
 
82
- def _host_of(base_url: str) -> str:
83
- from urllib.parse import urlparse
89
+ def caddy_host(base_url: str) -> str:
90
+ """Validiert die öffentliche HTTPS-Adresse vor dem Rendern der Caddy-Konfiguration."""
91
+ parsed = urlparse(base_url)
92
+ host = (parsed.hostname or "").encode("idna").decode("ascii")
93
+ try:
94
+ ip_address(host)
95
+ is_ip = True
96
+ except ValueError:
97
+ is_ip = False
98
+ label = r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?"
99
+ if (
100
+ parsed.scheme != "https" or parsed.port not in (None, 443)
101
+ or parsed.username is not None or parsed.password is not None
102
+ or parsed.path not in ("", "/") or parsed.query or parsed.fragment
103
+ or is_ip or len(host) > 253
104
+ or not re.fullmatch(rf"{label}(?:\.{label})+", host)
105
+ or host.endswith((".localhost", ".local"))
106
+ ):
107
+ raise ValueError("Caddy benötigt eine öffentliche Domain als https://domain.tld ohne Pfad (Port 443).")
108
+ return host
84
109
 
110
+
111
+ def _host_of(base_url: str) -> str:
85
112
  return urlparse(base_url).hostname or "localhost"
@@ -0,0 +1,113 @@
1
+ """Schreibrechte für Bind-Mounts, in die API und Worker schreiben.
2
+
3
+ API- und Worker-Image laufen als unprivilegierter User (Dockerfile `USER`). Ein vom
4
+ Installer oder per Hand angelegtes Host-Verzeichnis gehört dagegen meist root. Die
5
+ UID/GID wird nicht im CLI hinterlegt, sondern zur Laufzeit aus dem Container gelesen
6
+ (`id` im api-Service, inkl. eventuellem `user:` aus compose.override.yaml).
7
+
8
+ Die Korrektur läuft als root in einem Wegwerf-Container desselben Images — dadurch
9
+ funktioniert sie auch, wenn die CLI selbst nicht als root läuft. Sie ändert nur Eigentümer
10
+ von Einträgen mit falschem Eigentümer, nie Inhalte, und ist damit idempotent.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+
19
+ from . import docker
20
+
21
+ # Container-Pfad des MEDIA_ROOT-Bind-Mounts (compose.yaml.j2, api + worker).
22
+ CONTAINER_MEDIA_ROOT = "/var/lib/katalon/media"
23
+ # Services, die in CONTAINER_MEDIA_ROOT schreiben.
24
+ WRITER_SERVICES = ("api", "worker")
25
+ # Service, dessen Image-User als Eigentümer gesetzt wird.
26
+ OWNER_SERVICE = "api"
27
+
28
+
29
+ class PermissionFixError(RuntimeError):
30
+ pass
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ContainerUser:
35
+ uid: int
36
+ gid: int
37
+ name: str = ""
38
+
39
+ def __str__(self) -> str:
40
+ label = f"{self.uid}:{self.gid}"
41
+ return f"{label} ({self.name})" if self.name else label
42
+
43
+
44
+ _ID_RE = re.compile(r"uid=(\d+)(?:\(([^)]*)\))?\s+gid=(\d+)")
45
+
46
+
47
+ def parse_id_output(output: str) -> ContainerUser:
48
+ match = _ID_RE.search(output)
49
+ if not match:
50
+ raise PermissionFixError(f"Unerwartete Ausgabe von `id`: {output.strip()!r}")
51
+ return ContainerUser(uid=int(match.group(1)), gid=int(match.group(3)), name=match.group(2) or "")
52
+
53
+
54
+ def _run(instance_dir: Path, service: str, *args: str, root: bool = False):
55
+ user = ["--user", "0:0"] if root else []
56
+ return docker.compose(
57
+ instance_dir,
58
+ "run", "--rm", "--no-deps", "-T", *user, "--entrypoint", args[0], service, *args[1:],
59
+ check=False,
60
+ capture=True,
61
+ )
62
+
63
+
64
+ def container_user(instance_dir: Path, service: str = OWNER_SERVICE) -> ContainerUser:
65
+ """UID/GID, unter der `service` tatsächlich läuft (Image-USER bzw. compose `user:`)."""
66
+ result = _run(instance_dir, service, "id")
67
+ if result.returncode != 0:
68
+ raise PermissionFixError(
69
+ f"UID des {service}-Containers nicht ermittelbar: {(result.stderr or result.stdout).strip()}"
70
+ )
71
+ return parse_id_output(result.stdout)
72
+
73
+
74
+ def chown_args(path: str, user: ContainerUser) -> list[str]:
75
+ """`find`-Aufruf, der nur Einträge mit falschem Eigentümer umstellt (Inhalte bleiben unberührt)."""
76
+ owner = f"{user.uid}:{user.gid}"
77
+ return [
78
+ "find", path, "-xdev",
79
+ "(", "!", "-user", str(user.uid), "-o", "!", "-group", str(user.gid), ")",
80
+ "-exec", "chown", "-h", owner, "{}", "+",
81
+ ]
82
+
83
+
84
+ def is_writable(instance_dir: Path, service: str) -> bool:
85
+ result = _run(instance_dir, service, "test", "-w", CONTAINER_MEDIA_ROOT)
86
+ return result.returncode == 0
87
+
88
+
89
+ def unwritable_services(instance_dir: Path) -> list[str]:
90
+ return [service for service in WRITER_SERVICES if not is_writable(instance_dir, service)]
91
+
92
+
93
+ def fix_media_permissions(instance_dir: Path) -> ContainerUser:
94
+ """Setzt den Eigentümer von MEDIA_ROOT auf den Container-User und prüft danach die Schreibrechte."""
95
+ user = container_user(instance_dir)
96
+ result = _run(instance_dir, OWNER_SERVICE, *chown_args(CONTAINER_MEDIA_ROOT, user), root=True)
97
+ if result.returncode != 0:
98
+ raise PermissionFixError(
99
+ f"Eigentümer von MEDIA_ROOT konnte nicht auf {user} gesetzt werden: "
100
+ f"{(result.stderr or result.stdout).strip()}"
101
+ )
102
+ failing = unwritable_services(instance_dir)
103
+ if failing:
104
+ raise PermissionFixError(
105
+ f"MEDIA_ROOT ist nach der Korrektur für {', '.join(failing)} nicht beschreibbar "
106
+ f"(Eigentümer jetzt {user})."
107
+ )
108
+ return user
109
+
110
+
111
+ def manual_fix_hint(media_root: str, user: ContainerUser | None) -> str:
112
+ owner = f"{user.uid}:{user.gid}" if user else "<UID>:<GID>"
113
+ return f"sudo chown -R {owner} {media_root}"
@@ -24,7 +24,7 @@ class InstallationState(BaseModel):
24
24
  version: str
25
25
  compose_revision: int
26
26
  base_url: str
27
- tls_mode: str # "standalone" | "behind-proxy" | "none"
27
+ tls_mode: str # "caddy" | "standalone" | "behind-proxy" | "none"
28
28
  installed_at: datetime
29
29
  history: list[HistoryEntry] = Field(default_factory=list)
30
30
 
@@ -28,9 +28,9 @@ from rich.prompt import Confirm, Prompt
28
28
  from rich.table import Table
29
29
 
30
30
  from .core import backup as backup_mod
31
- from .core import checks, docker, release, selfupdate
31
+ from .core import checks, docker, permissions, release, selfupdate
32
32
  from .core import reindex as reindex_mod
33
- from .core.compose_gen import write_compose
33
+ from .core.compose_gen import caddy_host, write_compose
34
34
  from .core.paths import default_instance_dir, save_instance_dir
35
35
  from .core.state import InstallationState, instance_dir_or_raise
36
36
 
@@ -57,8 +57,9 @@ def _main(
57
57
  pass
58
58
 
59
59
  TLS_CHOICES = {
60
- "standalone": "Standalone — eigenes nginx mit selbstsigniertem Zertifikat (Ports 80+443)",
61
- "behind-proxy": "Hinter eigenem Reverse-Proxy — kein TLS hier",
60
+ "caddy": "Caddy – automatische HTTPS-Zertifikate und Erneuerung (Ports 80+443)",
61
+ "standalone": "Selbstsigniert – eigenes nginx, Browserwarnung, keine automatische Erneuerung (Ports 80+443)",
62
+ "behind-proxy": "Hinter eigenem Reverse-Proxy – Zertifikate dort verwalten",
62
63
  "none": "Kein TLS (lokal / IP)",
63
64
  }
64
65
 
@@ -247,6 +248,10 @@ def _run_checks(dir: Path, ports: list[int] | None = None, need_openssl: bool =
247
248
  def _resolve_ports_and_base_url(
248
249
  base_url: str, tls_mode: str, is_localhost: bool
249
250
  ) -> tuple[str, list[int]]:
251
+ if tls_mode == "caddy":
252
+ base_url = urlparse(base_url)._replace(scheme="https").geturl()
253
+ caddy_host(base_url)
254
+ return base_url, [80, 443]
250
255
  if tls_mode == "standalone":
251
256
  return base_url, [80, 443]
252
257
 
@@ -308,16 +313,37 @@ def install(
308
313
  "sonst echte Domain (dann folgt TLS-Auswahl)."
309
314
  )
310
315
  base_url = Prompt.ask("KATALON_BASE_URL", default="http://localhost")
316
+ if "://" not in base_url:
317
+ base_url = f"http://{base_url}"
311
318
 
312
319
  console.print()
313
320
  console.print("MEDIA_ROOT — Verzeichnis auf dem Host, in dem Katalon Mediendateien ablegt.")
314
321
  media_root = Prompt.ask("MEDIA_ROOT", default=str(dir / "media"))
315
322
 
316
323
  is_localhost = urlparse(base_url).hostname in (None, "localhost", "127.0.0.1")
324
+ try:
325
+ caddy_host(f"https://{urlparse(base_url).hostname or ''}")
326
+ is_domain = True
327
+ except ValueError:
328
+ is_domain = False
317
329
  console.print()
318
- for key, label in TLS_CHOICES.items():
319
- console.print(f" [cyan]{key}[/] — {label}")
320
- tls_mode = Prompt.ask("TLS-Modus", choices=list(TLS_CHOICES), default="none" if is_localhost else "standalone")
330
+ if is_domain:
331
+ console.print(
332
+ "Für öffentliches HTTPS: Der A-Record muss auf die öffentliche IPv4-Adresse dieses Servers zeigen. "
333
+ "Ein vorhandener AAAA-Record muss auf seine erreichbare IPv6-Adresse zeigen. "
334
+ "DNS-Änderungen müssen bereits wirksam sein."
335
+ )
336
+ console.print(
337
+ "Für Caddy müssen TCP-Ports 80 und 443 öffentlich erreichbar sein "
338
+ "(Server-/Provider-Firewall, ggf. Portweiterleitung). "
339
+ "Caddy beantragt und erneuert Zertifikate automatisch, solange es läuft und erreichbar bleibt. "
340
+ "Die CLI prüft freie Ports lokal; öffentliche DNS-Auflösung und Erreichbarkeit musst du sicherstellen."
341
+ )
342
+ choices = [key for key in TLS_CHOICES if key != "caddy" or is_domain]
343
+ for key in choices:
344
+ console.print(f" [cyan]{key}[/] – {TLS_CHOICES[key]}")
345
+ default_tls = "none" if is_localhost else ("caddy" if is_domain else "standalone")
346
+ tls_mode = Prompt.ask("TLS-Modus", choices=choices, default=default_tls)
321
347
 
322
348
  console.print()
323
349
  base_url, ports = _resolve_ports_and_base_url(base_url, tls_mode, is_localhost)
@@ -403,6 +429,7 @@ def _print_next_steps(dir: Path, base_url: str, show_credentials: bool = False)
403
429
  console.print()
404
430
  console.print(f"Portal: [bold]{base_url}/[/]")
405
431
  console.print(f"Admin: [bold]{base_url}/admin/[/]")
432
+ console.print(f"API: [bold]{base_url}/v1/[/] (OpenAPI-Doku: {base_url}/api/docs)")
406
433
  creds = _first_run_credentials(dir) if show_credentials else None
407
434
  if not show_credentials:
408
435
  console.print("Admin-Passwort vergessen? [bold]katalon manage reset-admin[/]")
@@ -423,10 +450,55 @@ def _print_next_steps(dir: Path, base_url: str, show_credentials: bool = False)
423
450
  console.print("Docs: [bold]https://katalon-collections.github.io/katalon-docs/[/]")
424
451
 
425
452
 
453
+ def _media_root(dir: Path) -> str:
454
+ value = _env_value(dir, "MEDIA_ROOT") or "./media"
455
+ path = Path(value)
456
+ return str(path if path.is_absolute() else dir / path)
457
+
458
+
459
+ def _require_installer_version(minimum: str) -> None:
460
+ """Bricht ab, wenn das Release eine neuere CLI braucht (z. B. neues Compose-Template).
461
+
462
+ Sonst würde eine alte CLI ihr altes compose.yaml-Template schreiben und das Release
463
+ liefe ohne die Topologie, für die es gebaut ist."""
464
+ current = version("katalon-cli")
465
+ if selfupdate.is_newer(minimum, current):
466
+ console.print(
467
+ f"[red]✖ Dieses Release braucht katalon-cli ≥ {minimum} (installiert: {current}).[/]\n"
468
+ " Zuerst aktualisieren: [bold]uv tool upgrade katalon-cli[/], dann erneut "
469
+ "[bold]katalon update[/]."
470
+ )
471
+ raise typer.Exit(1)
472
+
473
+
474
+ def _ensure_media_permissions(dir: Path) -> bool:
475
+ """Macht MEDIA_ROOT für den Container-User von API/Worker beschreibbar (idempotent).
476
+
477
+ Fehler brechen nicht ab: Der Stack startet trotzdem, nur Uploads schlagen fehl."""
478
+ try:
479
+ user = permissions.fix_media_permissions(dir)
480
+ except permissions.PermissionFixError as exc:
481
+ console.print(f"[red]✖ MEDIA_ROOT {_media_root(dir)}: {exc}[/]")
482
+ console.print(
483
+ " Uploads schlagen sonst mit HTTP 500 fehl. Manuell beheben: "
484
+ f"[bold]{permissions.manual_fix_hint(_media_root(dir), None)}[/] "
485
+ "(UID/GID: `katalon doctor`)"
486
+ )
487
+ return False
488
+ console.print(f"[green]✔[/] MEDIA_ROOT beschreibbar für Container-User {user}.")
489
+ return True
490
+
491
+
426
492
  def _start(dir: Path, show_credentials: bool = False) -> None:
493
+ _ensure_media_permissions(dir)
427
494
  docker.compose(dir, "up", "-d")
428
495
  console.print("[green]✔[/] Stack gestartet.")
429
496
  state = InstallationState.load(dir)
497
+ if state.tls_mode == "caddy":
498
+ console.print(
499
+ "Caddy beantragt beim ersten Start das HTTPS-Zertifikat; das kann kurz dauern. "
500
+ "Bei HTTPS-Problemen: [bold]katalon logs caddy[/]."
501
+ )
430
502
  _print_next_steps(dir, state.base_url, show_credentials=show_credentials)
431
503
 
432
504
 
@@ -502,6 +574,32 @@ def manage(
502
574
  raise typer.Exit(result.returncode)
503
575
 
504
576
 
577
+ media_app = typer.Typer(no_args_is_help=True, help="Medien: Vorschaubilder vorberechnen.")
578
+ app.add_typer(media_app, name="media")
579
+
580
+
581
+ @media_app.command("warm-thumbnails")
582
+ def media_warm_thumbnails(
583
+ dir: Path | None = typer.Option(None, "--dir"),
584
+ primary_only: bool = typer.Option(False, "--primary-only", help="Nur das Hauptbild jedes Objekts."),
585
+ restart: bool = typer.Option(False, "--restart", help="Von vorn beginnen statt fortsetzen."),
586
+ status: bool = typer.Option(False, "--status", help="Nur den Fortschritt anzeigen."),
587
+ ):
588
+ """Vorschaubilder im Cantaloupe-Cache vorberechnen (läuft im Worker, fortsetzbar).
589
+
590
+ Sinnvoll nach Installation, Massenimport oder Umstellung des Bild-Caches."""
591
+ dir = instance_dir_or_raise(dir)
592
+ args = ["warm-thumbnails"]
593
+ if primary_only:
594
+ args.append("--primary-only")
595
+ if restart:
596
+ args.append("--restart")
597
+ if status:
598
+ args.append("--status")
599
+ result = docker.compose(dir, "exec", "api", "katalon-manage", *args, check=False)
600
+ raise typer.Exit(result.returncode)
601
+
602
+
505
603
  def _ensure_db_running(dir: Path, yes: bool = False) -> None:
506
604
  if docker.is_running(dir, "db"):
507
605
  return
@@ -537,8 +635,28 @@ def doctor(dir: Path | None = typer.Option(None, "--dir")):
537
635
  "[yellow]⚠ Hinweis: Port 443 ist belegt. Falls Browser http://localhost/admin/ "
538
636
  "auf https:// umleiten, Base-URL auf z. B. http://localhost:8080 anpassen.[/]"
539
637
  )
638
+ _check_media_writable(dir)
540
639
  docker.compose(dir, "ps")
541
640
 
641
+
642
+ def _check_media_writable(dir: Path) -> bool:
643
+ media_root = _media_root(dir)
644
+ try:
645
+ user = permissions.container_user(dir)
646
+ failing = permissions.unwritable_services(dir)
647
+ except (permissions.PermissionFixError, docker.DockerError) as exc:
648
+ console.print(f"[yellow]⚠[/] MEDIA_ROOT: Prüfung nicht möglich — {exc}")
649
+ return False
650
+ if failing:
651
+ console.print(
652
+ f"[red]✖[/] MEDIA_ROOT: {media_root} ist für {', '.join(failing)} "
653
+ f"(Container-User {user}) nicht beschreibbar — Medien-Uploads scheitern mit HTTP 500. "
654
+ f"Beheben: [bold]katalon start[/] oder [bold]{permissions.manual_fix_hint(media_root, user)}[/]"
655
+ )
656
+ return False
657
+ console.print(f"[green]✔[/] MEDIA_ROOT: {media_root} beschreibbar für Container-User {user}")
658
+ return True
659
+
542
660
  @app.command()
543
661
  def check_updates(dir: Path | None = typer.Option(None, "--dir")):
544
662
  """Prüft, ob eine neue Katalon-Version verfügbar ist."""
@@ -553,6 +671,7 @@ def check_updates(dir: Path | None = typer.Option(None, "--dir")):
553
671
  if meta.version == state.version:
554
672
  console.print(f"[green]✔[/] Bereits auf aktueller Version {meta.version}.")
555
673
  return
674
+ _require_installer_version(meta.minimum_installer_version)
556
675
  console.print(f"Update verfügbar: [bold]{state.version}[/] → [bold]{meta.version}[/]")
557
676
 
558
677
 
@@ -620,6 +739,7 @@ def update(
620
739
  lambda: write_compose(dir, version=meta.version, base_url=state.base_url, tls_mode=state.tls_mode),
621
740
  ),
622
741
  ("Images pullen", lambda: docker.compose(dir, "pull")),
742
+ ("Medienrechte prüfen", lambda: _ensure_media_permissions(dir)),
623
743
  # DB-Migration läuft automatisch im api-Container-Entrypoint beim Start.
624
744
  ("Container neu starten", lambda: docker.compose(dir, "up", "-d")),
625
745
  ]
@@ -703,6 +823,7 @@ def rollback(
703
823
  write_compose(dir, version=previous.version, base_url=state.base_url, tls_mode=state.tls_mode)
704
824
  docker.compose(dir, "pull")
705
825
 
826
+ _ensure_media_permissions(dir)
706
827
  docker.compose(dir, "up", "-d")
707
828
  console.print("[green]✔[/] Rollback abgeschlossen.")
708
829
 
@@ -0,0 +1,3 @@
1
+ {{ host }} {
2
+ reverse_proxy nginx:80
3
+ }
@@ -39,15 +39,31 @@ services:
39
39
  timeout: 10s
40
40
  retries: 20
41
41
 
42
+ # Legt den Derivat-Cache an und übergibt ihn dem Cantaloupe-User des Images.
43
+ # Ändert nur Einträge mit falschem Eigentümer (idempotent, schnell bei vollem Cache).
44
+ cantaloupe-cache-init:
45
+ image: islandora/cantaloupe:main
46
+ user: "0:0"
47
+ entrypoint: ["sh", "-c", "mkdir -p /data/cache && find /data ! -user cantaloupe -exec chown cantaloupe:cantaloupe {} +"]
48
+ volumes:
49
+ - ${CANTALOUPE_CACHE_DIR:-./cantaloupe-cache}:/data
50
+ restart: "no"
51
+
42
52
  cantaloupe:
43
53
  image: islandora/cantaloupe:main
44
54
  restart: unless-stopped
45
55
  volumes:
46
56
  - ${MEDIA_ROOT:-./media}:/var/lib/cantaloupe/images:ro
57
+ # Derivat-Cache auf Platte, übersteht Neustarts. Ohne Plattenplatz:
58
+ # CANTALOUPE_FILESYSTEMCACHE_PATHNAME=/tmpfs/cache in .env (RAM, leer nach Neustart).
59
+ - ${CANTALOUPE_CACHE_DIR:-./cantaloupe-cache}:/data
47
60
  - type: tmpfs
48
- target: /var/lib/cantaloupe/cache
61
+ target: /tmpfs
49
62
  tmpfs:
50
63
  size: ${CANTALOUPE_CACHE_TMPFS_SIZE:-512m}
64
+ mode: 1023 # = 0o1777 (Compose liest den Wert dezimal)
65
+ depends_on:
66
+ cantaloupe-cache-init: { condition: service_completed_successfully }
51
67
  environment:
52
68
  CANTALOUPE_SOURCE_STATIC: FilesystemSource
53
69
  CANTALOUPE_FILESYSTEMSOURCE_LOOKUP_STRATEGY: BasicLookupStrategy
@@ -55,10 +71,19 @@ services:
55
71
  CANTALOUPE_BASE_URI: ${CANTALOUPE_PUBLIC_URL:-http://localhost}
56
72
  CANTALOUPE_CACHE_SERVER_DERIVATIVE: FilesystemCache
57
73
  CANTALOUPE_CACHE_SERVER_DERIVATIVE_ENABLED: "true"
58
- CANTALOUPE_FILESYSTEMCACHE_PATHNAME: /var/lib/cantaloupe/cache
74
+ CANTALOUPE_FILESYSTEMCACHE_PATHNAME: ${CANTALOUPE_FILESYSTEMCACHE_PATHNAME:-/data/cache}
75
+ # Der Cache-Worker löscht Derivate älter als die TTL; ohne ihn wächst der Cache unbegrenzt.
76
+ CANTALOUPE_CACHE_SERVER_WORKER_ENABLED: "true"
77
+ CANTALOUPE_CACHE_SERVER_DERIVATIVE_TTL_SECONDS: ${CANTALOUPE_CACHE_SERVER_DERIVATIVE_TTL_SECONDS:-2592000}
59
78
  CANTALOUPE_PROCESSOR_MANUALSELECTIONSTRATEGY_JPG: Java2dProcessor
79
+ # Fallback ohne nginx davor: nie "public". nginx setzt Cache-Control für /iiif/
80
+ # nach Sichtbarkeit (X-Media-Visibility aus /v1/media/_authorize).
81
+ CANTALOUPE_CACHE_CLIENT_PUBLIC: "false"
82
+ CANTALOUPE_CACHE_CLIENT_PRIVATE: "true"
60
83
  healthcheck:
61
- test: ["CMD-SHELL", "curl -sf http://localhost:8182/iiif/3"]
84
+ # Prüft neben der Erreichbarkeit, dass der Cantaloupe-Prozess in seinen Cache
85
+ # schreiben kann — sonst liefert er Bilder mit 0 Bytes bei HTTP 200.
86
+ test: ["CMD-SHELL", "curl -sf http://localhost:8182/iiif/3 && runuser -u cantaloupe -- sh -c 'd=$${CANTALOUPE_FILESYSTEMCACHE_PATHNAME:-/data/cache}; mkdir -p \"$$d\" && touch \"$$d/.healthcheck\" && rm -f \"$$d/.healthcheck\"'"]
62
87
  interval: 10s
63
88
  timeout: 5s
64
89
  start_period: 300s
@@ -159,6 +184,7 @@ services:
159
184
  {% if tls_mode == "standalone" %}
160
185
  - ./certs:/etc/nginx/certs:ro
161
186
  {% endif %}
187
+ {% if tls_mode != "caddy" %}
162
188
  ports:
163
189
  {% if tls_mode == "standalone" %}
164
190
  - "80:80"
@@ -167,12 +193,32 @@ services:
167
193
  - "127.0.0.1:{{ port or 8080 }}:80"
168
194
  {% else %}
169
195
  - "{{ port or 80 }}:80"
196
+ {% endif %}
170
197
  {% endif %}
171
198
  depends_on:
172
199
  api: { condition: service_healthy }
173
200
  admin: { condition: service_healthy }
174
201
  portal: { condition: service_healthy }
175
202
 
203
+ {% if tls_mode == "caddy" %}
204
+ caddy:
205
+ image: caddy:2-alpine
206
+ restart: unless-stopped
207
+ ports:
208
+ - "80:80"
209
+ - "443:443"
210
+ volumes:
211
+ - ./Caddyfile:/etc/caddy/Caddyfile:ro
212
+ - caddy_data:/data
213
+ - caddy_config:/config
214
+ depends_on:
215
+ nginx: { condition: service_started }
216
+
217
+ {% endif %}
176
218
  volumes:
177
219
  db_data:
178
220
  es_data:
221
+ {% if tls_mode == "caddy" %}
222
+ caddy_data:
223
+ caddy_config:
224
+ {% endif %}
@@ -1,5 +1,13 @@
1
1
  resolver 127.0.0.11 valid=10s ipv6=off;
2
2
 
3
+ # Cache-Control for /iiif/ from the media's visibility (set by /v1/media/_authorize via
4
+ # auth_request_set): shared caches (CDN, proxies) may keep only what anonymous
5
+ # visitors may see; derivatives of internal media stay in the requesting browser.
6
+ map $media_visibility $iiif_cache_control {
7
+ public "public, max-age=2592000";
8
+ default "private, max-age=86400";
9
+ }
10
+
3
11
  server {
4
12
  {% if tls_mode == "standalone" %}
5
13
  listen 80;
@@ -23,7 +31,7 @@ server {
23
31
  proxy_set_header Host $host;
24
32
  proxy_set_header X-Real-IP $remote_addr;
25
33
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
26
- proxy_set_header X-Forwarded-Proto $scheme;
34
+ proxy_set_header X-Forwarded-Proto {{ '$http_x_forwarded_proto' if tls_mode == 'caddy' else '$scheme' }};
27
35
  }
28
36
 
29
37
  location /v1/ {
@@ -32,7 +40,7 @@ server {
32
40
  proxy_set_header Host $host;
33
41
  proxy_set_header X-Real-IP $remote_addr;
34
42
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
35
- proxy_set_header X-Forwarded-Proto $scheme;
43
+ proxy_set_header X-Forwarded-Proto {{ '$http_x_forwarded_proto' if tls_mode == 'caddy' else '$scheme' }};
36
44
  }
37
45
 
38
46
  location /portal/v1/ {
@@ -41,7 +49,7 @@ server {
41
49
  proxy_set_header Host $host;
42
50
  proxy_set_header X-Real-IP $remote_addr;
43
51
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44
- proxy_set_header X-Forwarded-Proto $scheme;
52
+ proxy_set_header X-Forwarded-Proto {{ '$http_x_forwarded_proto' if tls_mode == 'caddy' else '$scheme' }};
45
53
  }
46
54
 
47
55
  location = /health {
@@ -50,7 +58,7 @@ server {
50
58
  proxy_set_header Host $host;
51
59
  proxy_set_header X-Real-IP $remote_addr;
52
60
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
53
- proxy_set_header X-Forwarded-Proto $scheme;
61
+ proxy_set_header X-Forwarded-Proto {{ '$http_x_forwarded_proto' if tls_mode == 'caddy' else '$scheme' }};
54
62
  }
55
63
 
56
64
  location = /oai {
@@ -59,7 +67,7 @@ server {
59
67
  proxy_set_header Host $host;
60
68
  proxy_set_header X-Real-IP $remote_addr;
61
69
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
62
- proxy_set_header X-Forwarded-Proto $scheme;
70
+ proxy_set_header X-Forwarded-Proto {{ '$http_x_forwarded_proto' if tls_mode == 'caddy' else '$scheme' }};
63
71
  }
64
72
 
65
73
  location ^~ /ark:/ {
@@ -68,17 +76,27 @@ server {
68
76
  proxy_set_header Host $host;
69
77
  proxy_set_header X-Real-IP $remote_addr;
70
78
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
71
- proxy_set_header X-Forwarded-Proto $scheme;
79
+ proxy_set_header X-Forwarded-Proto {{ '$http_x_forwarded_proto' if tls_mode == 'caddy' else '$scheme' }};
72
80
  }
73
81
 
74
82
  location /iiif/ {
75
83
  auth_request /internal/media-authorize;
84
+ auth_request_set $media_visibility $upstream_http_x_media_visibility;
76
85
  set $upstream_cantaloupe cantaloupe:8182;
77
86
  proxy_pass http://$upstream_cantaloupe;
78
87
  proxy_set_header Host $host;
79
88
  proxy_set_header X-Real-IP $remote_addr;
80
89
  proxy_read_timeout 300s;
81
90
  proxy_buffering off;
91
+ # Cantaloupe marks every derivative public; decide by visibility instead.
92
+ proxy_hide_header Cache-Control;
93
+ add_header Cache-Control $iiif_cache_control;
94
+ # An add_header here drops the server-level ones: repeat them.
95
+ add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; form-action 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: blob: https:; media-src 'self' blob: https:; connect-src 'self' https:; frame-src 'self' https://www.openstreetmap.org; worker-src 'self' blob:" always;
96
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
97
+ add_header X-Content-Type-Options "nosniff" always;
98
+ add_header X-Frame-Options "SAMEORIGIN" always;
99
+ add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always;
82
100
  }
83
101
 
84
102
  location = /internal/media-authorize {
@@ -1,94 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: katalon-cli
3
- Version: 0.1.20
4
- Summary: Installer & Updater CLI für Katalon Production-Instanzen
5
- Author: Karl Krägelin
6
- Author-email: Karl Krägelin <karlkraegelin@gmail.com>
7
- Requires-Dist: typer>=0.15
8
- Requires-Dist: rich>=13.9
9
- Requires-Dist: httpx>=0.28
10
- Requires-Dist: pydantic>=2.9
11
- Requires-Dist: jinja2>=3.1
12
- Requires-Python: >=3.12
13
- Description-Content-Type: text/markdown
14
-
15
- # katalon-cli
16
-
17
- Installer & Updater für [Katalon Collections](https://github.com/katalon-collections/katalon) Production-Instanzen.
18
- Verwaltet eine Instanz unter einem frei gewählten Zielverzeichnis — pullt gepinnte
19
- Release-Images, generiert `compose.yaml`, macht Backups vor jedem Update, kann rollbacken.
20
-
21
- ## Installation
22
-
23
- ```bash
24
- curl -LsSf https://astral.sh/uv/install.sh | sh
25
- uv tool install katalon-cli
26
- ```
27
-
28
- ## Nutzung
29
-
30
- ```bash
31
- katalon install # interaktiver Setup-Wizard (Rich-Prompts + Progress)
32
- katalon start / stop / status
33
- katalon check-updates # prüft nur, ob ein neues Release verfügbar ist
34
- katalon update # interaktives Update, Backup zuerst immer
35
- katalon rollback # letztes Backup einspielen
36
- katalon doctor # Docker, Diskspace, Ports prüfen
37
- katalon backup # manuelles Backup
38
- katalon logs [service]
39
- katalon manage <args> # katalon-manage im api-Container (z.B. reset-admin, create-user)
40
- katalon restore DUMP # DB durch pg_dump ersetzen (-Fc oder SQL), danach Reindex
41
- katalon reindex # Suchindex neu aufbauen, mit Fortschrittsanzeige
42
- ```
43
-
44
- Ein DB-Dump enthält nicht den Suchindex (Elasticsearch). `restore` baut ihn deshalb nach dem
45
- Einspielen neu auf (`--no-reindex` zum Überspringen, später `katalon reindex`). Bei großen
46
- Beständen dauert das lange (ca. 45 Min. für 80k Datensätze). Außerdem prüft `katalon` einmal
47
- täglich auf neue Versionen (abschalten: `KATALON_NO_UPDATE_CHECK=1`).
48
-
49
- `install` ohne `--dir` fragt das Zielverzeichnis interaktiv ab. Nach erfolgreicher Einrichtung
50
- wird der absolute Pfad benutzerweit in `~/.config/katalon/instance` gespeichert.
51
- Bei gesetztem, absolutem `XDG_CONFIG_HOME` liegt die Datei stattdessen unter
52
- `$XDG_CONFIG_HOME/katalon/instance`. Das Konfigurationsverzeichnis wird bei Bedarf angelegt.
53
- Alle weiteren Befehle verwenden den gespeicherten Pfad, unabhängig vom Arbeitsverzeichnis.
54
- Ohne gespeicherten Pfad ist der Default auf allen Plattformen `~/katalon`.
55
-
56
- Alle Befehle unterstützen `--dir PATH`: überschreibt den Pfad nur für diesen Aufruf.
57
- Eine erfolgreiche neue Installation speichert ihr Zielverzeichnis als neuen Default.
58
- Bestehende Instanzen lassen sich weiterhin mit `--dir` verwenden; alternativ ihren absoluten
59
- Pfad als einzige Zeile in die Konfigurationsdatei eintragen.
60
- `install`/`update` fragen interaktiv (Auswahl über `rich.prompt`),
61
- `update`/`rollback` haben `--yes` zum Überspringen der Rückfrage.
62
-
63
- ```bash
64
- katalon install --dir ~/meine-katalon-instanz
65
- katalon start
66
- katalon status
67
- ```
68
-
69
- ## Entwicklung
70
-
71
- ```bash
72
- uv sync
73
- uv run katalon --help
74
- uv run pytest
75
- ```
76
-
77
- ## Architektur
78
-
79
- - `core/state.py` — `installation.json`, Single Source of Truth für laufende Version.
80
- - `core/release.py` — GitHub-Release-Metadaten (`katalon-release.json` Asset). Enthält
81
- optional `env_vars`/`deprecated_env_vars`: `install`/`update` ergänzen neue Pflichtvariablen
82
- automatisch (Secret generieren oder Default übernehmen), fragen bei fehlendem Default interaktiv
83
- nach (`--yes` bricht dann ab), tragen neue optionale Variablen auskommentiert in `.env` ein und
84
- warnen vor veralteten Variablen, die noch in der `.env` stehen.
85
- - `core/compose_gen.py` + `templates/compose.yaml.j2` + `templates/nginx.conf.j2` — rendert `compose.yaml`
86
- + `nginx.conf` aus Version + TLS-Modus; erzeugt bei `tls_mode=standalone` ein selbstsigniertes
87
- Zertifikat unter `<dir>/certs/` (eigenes Zertifikat dort ablegen, um es zu ersetzen).
88
- `install` fragt zusätzlich `MEDIA_ROOT` ab und legt `<dir>/compose.override.yaml.example` an
89
- (instanzspezifische Overrides — umbenennen zu `compose.override.yaml`, wird automatisch eingebunden
90
- und bei Updates nicht angefasst). Optionale Env-Vars (Admin-Login, SMTP, Geonames, …) landet
91
- auskommentiert in `.env`.
92
- - `core/backup.py` — `pg_dump` vor jedem Update; Rollback restauriert Dump statt `alembic downgrade`.
93
- - `core/docker.py` — dünner `docker compose`-Subprocess-Wrapper.
94
- - `main.py` — Typer-Commands, interaktive Teile über `rich.prompt`/`rich.progress`.
@@ -1,80 +0,0 @@
1
- # katalon-cli
2
-
3
- Installer & Updater für [Katalon Collections](https://github.com/katalon-collections/katalon) Production-Instanzen.
4
- Verwaltet eine Instanz unter einem frei gewählten Zielverzeichnis — pullt gepinnte
5
- Release-Images, generiert `compose.yaml`, macht Backups vor jedem Update, kann rollbacken.
6
-
7
- ## Installation
8
-
9
- ```bash
10
- curl -LsSf https://astral.sh/uv/install.sh | sh
11
- uv tool install katalon-cli
12
- ```
13
-
14
- ## Nutzung
15
-
16
- ```bash
17
- katalon install # interaktiver Setup-Wizard (Rich-Prompts + Progress)
18
- katalon start / stop / status
19
- katalon check-updates # prüft nur, ob ein neues Release verfügbar ist
20
- katalon update # interaktives Update, Backup zuerst immer
21
- katalon rollback # letztes Backup einspielen
22
- katalon doctor # Docker, Diskspace, Ports prüfen
23
- katalon backup # manuelles Backup
24
- katalon logs [service]
25
- katalon manage <args> # katalon-manage im api-Container (z.B. reset-admin, create-user)
26
- katalon restore DUMP # DB durch pg_dump ersetzen (-Fc oder SQL), danach Reindex
27
- katalon reindex # Suchindex neu aufbauen, mit Fortschrittsanzeige
28
- ```
29
-
30
- Ein DB-Dump enthält nicht den Suchindex (Elasticsearch). `restore` baut ihn deshalb nach dem
31
- Einspielen neu auf (`--no-reindex` zum Überspringen, später `katalon reindex`). Bei großen
32
- Beständen dauert das lange (ca. 45 Min. für 80k Datensätze). Außerdem prüft `katalon` einmal
33
- täglich auf neue Versionen (abschalten: `KATALON_NO_UPDATE_CHECK=1`).
34
-
35
- `install` ohne `--dir` fragt das Zielverzeichnis interaktiv ab. Nach erfolgreicher Einrichtung
36
- wird der absolute Pfad benutzerweit in `~/.config/katalon/instance` gespeichert.
37
- Bei gesetztem, absolutem `XDG_CONFIG_HOME` liegt die Datei stattdessen unter
38
- `$XDG_CONFIG_HOME/katalon/instance`. Das Konfigurationsverzeichnis wird bei Bedarf angelegt.
39
- Alle weiteren Befehle verwenden den gespeicherten Pfad, unabhängig vom Arbeitsverzeichnis.
40
- Ohne gespeicherten Pfad ist der Default auf allen Plattformen `~/katalon`.
41
-
42
- Alle Befehle unterstützen `--dir PATH`: überschreibt den Pfad nur für diesen Aufruf.
43
- Eine erfolgreiche neue Installation speichert ihr Zielverzeichnis als neuen Default.
44
- Bestehende Instanzen lassen sich weiterhin mit `--dir` verwenden; alternativ ihren absoluten
45
- Pfad als einzige Zeile in die Konfigurationsdatei eintragen.
46
- `install`/`update` fragen interaktiv (Auswahl über `rich.prompt`),
47
- `update`/`rollback` haben `--yes` zum Überspringen der Rückfrage.
48
-
49
- ```bash
50
- katalon install --dir ~/meine-katalon-instanz
51
- katalon start
52
- katalon status
53
- ```
54
-
55
- ## Entwicklung
56
-
57
- ```bash
58
- uv sync
59
- uv run katalon --help
60
- uv run pytest
61
- ```
62
-
63
- ## Architektur
64
-
65
- - `core/state.py` — `installation.json`, Single Source of Truth für laufende Version.
66
- - `core/release.py` — GitHub-Release-Metadaten (`katalon-release.json` Asset). Enthält
67
- optional `env_vars`/`deprecated_env_vars`: `install`/`update` ergänzen neue Pflichtvariablen
68
- automatisch (Secret generieren oder Default übernehmen), fragen bei fehlendem Default interaktiv
69
- nach (`--yes` bricht dann ab), tragen neue optionale Variablen auskommentiert in `.env` ein und
70
- warnen vor veralteten Variablen, die noch in der `.env` stehen.
71
- - `core/compose_gen.py` + `templates/compose.yaml.j2` + `templates/nginx.conf.j2` — rendert `compose.yaml`
72
- + `nginx.conf` aus Version + TLS-Modus; erzeugt bei `tls_mode=standalone` ein selbstsigniertes
73
- Zertifikat unter `<dir>/certs/` (eigenes Zertifikat dort ablegen, um es zu ersetzen).
74
- `install` fragt zusätzlich `MEDIA_ROOT` ab und legt `<dir>/compose.override.yaml.example` an
75
- (instanzspezifische Overrides — umbenennen zu `compose.override.yaml`, wird automatisch eingebunden
76
- und bei Updates nicht angefasst). Optionale Env-Vars (Admin-Login, SMTP, Geonames, …) landet
77
- auskommentiert in `.env`.
78
- - `core/backup.py` — `pg_dump` vor jedem Update; Rollback restauriert Dump statt `alembic downgrade`.
79
- - `core/docker.py` — dünner `docker compose`-Subprocess-Wrapper.
80
- - `main.py` — Typer-Commands, interaktive Teile über `rich.prompt`/`rich.progress`.