katalon-cli 0.3.5__tar.gz → 0.5.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.
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/PKG-INFO +24 -2
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/README.md +23 -1
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/pyproject.toml +1 -1
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/pyproject.toml.orig +1 -1
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/compose_gen.py +5 -1
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/docker.py +18 -0
- katalon_cli-0.5.0/src/katalon_cli/core/images.py +72 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/release.py +26 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/state.py +6 -1
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/main.py +51 -6
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/templates/compose.yaml.j2 +9 -7
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/templates/nginx.conf.j2 +19 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/__init__.py +0 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/__init__.py +0 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/backup.py +0 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/checks.py +0 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/paths.py +0 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/permissions.py +0 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/reindex.py +0 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/core/selfupdate.py +0 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/py.typed +0 -0
- {katalon_cli-0.3.5 → katalon_cli-0.5.0}/src/katalon_cli/templates/Caddyfile.j2 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: katalon-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Installer & Updater CLI für Katalon Production-Instanzen
|
|
5
5
|
Author: Karl Krägelin
|
|
6
6
|
Author-email: Karl Krägelin <karlkraegelin@gmail.com>
|
|
@@ -66,6 +66,27 @@ katalon start
|
|
|
66
66
|
katalon status
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
## Images der Fremdservices
|
|
70
|
+
|
|
71
|
+
Welche Images Postgres, Redis, Elasticsearch, Cantaloupe, nginx und Caddy nutzen, legt das
|
|
72
|
+
Katalon-Release fest (`images` in `katalon-release.json`), nicht die CLI. `install` und `update`
|
|
73
|
+
schreiben sie in `compose.yaml`, `update` pullt sie mit. Releases ohne `images` (bis Katalon
|
|
74
|
+
1.46.0) bekommen die Images, die bis katalon-cli 0.3.5 fest im Template standen.
|
|
75
|
+
|
|
76
|
+
Vor einem Update vergleicht `katalon update` die wirksamen Images (inklusive
|
|
77
|
+
`compose.override.yaml`) vor und nach dem Update, zeigt jeden Wechsel an und bricht ab, bevor
|
|
78
|
+
etwas geändert wird, wenn
|
|
79
|
+
|
|
80
|
+
- Postgres oder Elasticsearch die Major-Version wechseln würden (Postgres braucht dafür
|
|
81
|
+
Dump/Restore, Elasticsearch ein neues Datenverzeichnis und einen Reindex), oder
|
|
82
|
+
- ein Postgres- oder Elasticsearch-Image die Anforderung des Releases (`requires`, z. B.
|
|
83
|
+
`>=8.13,<9`) nicht erfüllt, etwa ein eigenes Image aus `compose.override.yaml`. `requires`
|
|
84
|
+
gilt erst ab Releases mit `images`; ältere Releases nannten dort Werte, die nie geprüft wurden.
|
|
85
|
+
|
|
86
|
+
`installation.json` merkt sich die Images je Version. `rollback` nimmt die der vorigen Version,
|
|
87
|
+
behält aber ein neueres Elasticsearch derselben Major-Version, weil Elasticsearch ein
|
|
88
|
+
Datenverzeichnis nicht mit einer älteren Version öffnet.
|
|
89
|
+
|
|
69
90
|
## API-Adresse
|
|
70
91
|
|
|
71
92
|
Die REST-API liegt direkt unter der Base-URL, ohne `/api`-Präfix: `<KATALON_BASE_URL>/v1/...`
|
|
@@ -159,13 +180,14 @@ uv run pytest
|
|
|
159
180
|
nach (`--yes` bricht dann ab), tragen neue optionale Variablen auskommentiert in `.env` ein und
|
|
160
181
|
warnen vor veralteten Variablen, die noch in der `.env` stehen.
|
|
161
182
|
- `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;
|
|
183
|
+
+ `nginx.conf` und bei `tls_mode=caddy` das `Caddyfile` aus Version, Images + TLS-Modus;
|
|
163
184
|
erzeugt bei `tls_mode=standalone` ein selbstsigniertes
|
|
164
185
|
Zertifikat unter `<dir>/certs/` (eigenes Zertifikat dort ablegen, um es zu ersetzen).
|
|
165
186
|
`install` fragt zusätzlich `MEDIA_ROOT` ab und legt `<dir>/compose.override.yaml.example` an
|
|
166
187
|
(instanzspezifische Overrides — umbenennen zu `compose.override.yaml`, wird automatisch eingebunden
|
|
167
188
|
und bei Updates nicht angefasst). Optionale Env-Vars (Admin-Login, SMTP, Geonames, …) landet
|
|
168
189
|
auskommentiert in `.env`.
|
|
190
|
+
- `core/images.py` — prüft die Images der Fremdservices vor einem Update (siehe „Images der Fremdservices“).
|
|
169
191
|
- `core/backup.py` — `pg_dump` vor jedem Update; Rollback restauriert Dump statt `alembic downgrade`.
|
|
170
192
|
- `core/docker.py` — dünner `docker compose`-Subprocess-Wrapper.
|
|
171
193
|
- `main.py` — Typer-Commands, interaktive Teile über `rich.prompt`/`rich.progress`.
|
|
@@ -52,6 +52,27 @@ katalon start
|
|
|
52
52
|
katalon status
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## Images der Fremdservices
|
|
56
|
+
|
|
57
|
+
Welche Images Postgres, Redis, Elasticsearch, Cantaloupe, nginx und Caddy nutzen, legt das
|
|
58
|
+
Katalon-Release fest (`images` in `katalon-release.json`), nicht die CLI. `install` und `update`
|
|
59
|
+
schreiben sie in `compose.yaml`, `update` pullt sie mit. Releases ohne `images` (bis Katalon
|
|
60
|
+
1.46.0) bekommen die Images, die bis katalon-cli 0.3.5 fest im Template standen.
|
|
61
|
+
|
|
62
|
+
Vor einem Update vergleicht `katalon update` die wirksamen Images (inklusive
|
|
63
|
+
`compose.override.yaml`) vor und nach dem Update, zeigt jeden Wechsel an und bricht ab, bevor
|
|
64
|
+
etwas geändert wird, wenn
|
|
65
|
+
|
|
66
|
+
- Postgres oder Elasticsearch die Major-Version wechseln würden (Postgres braucht dafür
|
|
67
|
+
Dump/Restore, Elasticsearch ein neues Datenverzeichnis und einen Reindex), oder
|
|
68
|
+
- ein Postgres- oder Elasticsearch-Image die Anforderung des Releases (`requires`, z. B.
|
|
69
|
+
`>=8.13,<9`) nicht erfüllt, etwa ein eigenes Image aus `compose.override.yaml`. `requires`
|
|
70
|
+
gilt erst ab Releases mit `images`; ältere Releases nannten dort Werte, die nie geprüft wurden.
|
|
71
|
+
|
|
72
|
+
`installation.json` merkt sich die Images je Version. `rollback` nimmt die der vorigen Version,
|
|
73
|
+
behält aber ein neueres Elasticsearch derselben Major-Version, weil Elasticsearch ein
|
|
74
|
+
Datenverzeichnis nicht mit einer älteren Version öffnet.
|
|
75
|
+
|
|
55
76
|
## API-Adresse
|
|
56
77
|
|
|
57
78
|
Die REST-API liegt direkt unter der Base-URL, ohne `/api`-Präfix: `<KATALON_BASE_URL>/v1/...`
|
|
@@ -145,13 +166,14 @@ uv run pytest
|
|
|
145
166
|
nach (`--yes` bricht dann ab), tragen neue optionale Variablen auskommentiert in `.env` ein und
|
|
146
167
|
warnen vor veralteten Variablen, die noch in der `.env` stehen.
|
|
147
168
|
- `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;
|
|
169
|
+
+ `nginx.conf` und bei `tls_mode=caddy` das `Caddyfile` aus Version, Images + TLS-Modus;
|
|
149
170
|
erzeugt bei `tls_mode=standalone` ein selbstsigniertes
|
|
150
171
|
Zertifikat unter `<dir>/certs/` (eigenes Zertifikat dort ablegen, um es zu ersetzen).
|
|
151
172
|
`install` fragt zusätzlich `MEDIA_ROOT` ab und legt `<dir>/compose.override.yaml.example` an
|
|
152
173
|
(instanzspezifische Overrides — umbenennen zu `compose.override.yaml`, wird automatisch eingebunden
|
|
153
174
|
und bei Updates nicht angefasst). Optionale Env-Vars (Admin-Login, SMTP, Geonames, …) landet
|
|
154
175
|
auskommentiert in `.env`.
|
|
176
|
+
- `core/images.py` — prüft die Images der Fremdservices vor einem Update (siehe „Images der Fremdservices“).
|
|
155
177
|
- `core/backup.py` — `pg_dump` vor jedem Update; Rollback restauriert Dump statt `alembic downgrade`.
|
|
156
178
|
- `core/docker.py` — dünner `docker compose`-Subprocess-Wrapper.
|
|
157
179
|
- `main.py` — Typer-Commands, interaktive Teile über `rich.prompt`/`rich.progress`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Rendert compose.yaml + nginx.conf aus Template + Release
|
|
1
|
+
"""Rendert compose.yaml + nginx.conf aus Template + Release (Version, Images) + TLS-Modus."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -10,6 +10,8 @@ from urllib.parse import urlparse
|
|
|
10
10
|
|
|
11
11
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
|
12
12
|
|
|
13
|
+
from .release import ReleaseImages
|
|
14
|
+
|
|
13
15
|
_env = Environment(
|
|
14
16
|
loader=PackageLoader("katalon_cli", "templates"),
|
|
15
17
|
autoescape=select_autoescape(disabled_extensions=(".yaml", ".j2", ".conf")),
|
|
@@ -23,6 +25,7 @@ def render_compose(
|
|
|
23
25
|
version: str,
|
|
24
26
|
base_url: str,
|
|
25
27
|
tls_mode: str,
|
|
28
|
+
images: ReleaseImages | None = None,
|
|
26
29
|
registry: str = "ghcr.io/katalon-collections/katalon",
|
|
27
30
|
) -> str:
|
|
28
31
|
if tls_mode == "caddy":
|
|
@@ -34,6 +37,7 @@ def render_compose(
|
|
|
34
37
|
base_url=base_url,
|
|
35
38
|
tls_mode=tls_mode,
|
|
36
39
|
registry=registry,
|
|
40
|
+
images=images or ReleaseImages(),
|
|
37
41
|
port=parsed_port,
|
|
38
42
|
)
|
|
39
43
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
import time
|
|
@@ -45,6 +46,23 @@ def compose(
|
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
def config_images(instance_dir: Path, compose_file: Path | None = None) -> dict[str, str]:
|
|
50
|
+
"""Images je Service, wie Compose sie startet (inkl. compose.override.yaml)."""
|
|
51
|
+
files = ["-f", str(compose_file or instance_dir / "compose.yaml")]
|
|
52
|
+
override = instance_dir / "compose.override.yaml"
|
|
53
|
+
if override.exists():
|
|
54
|
+
files += ["-f", str(override)]
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
[*compose_command(), "--project-directory", str(instance_dir), *files, "config", "--format", "json"],
|
|
57
|
+
cwd=instance_dir,
|
|
58
|
+
check=True,
|
|
59
|
+
text=True,
|
|
60
|
+
capture_output=True,
|
|
61
|
+
)
|
|
62
|
+
services = json.loads(result.stdout).get("services", {})
|
|
63
|
+
return {name: svc["image"] for name, svc in services.items() if svc.get("image")}
|
|
64
|
+
|
|
65
|
+
|
|
48
66
|
def reload_nginx(instance_dir: Path) -> None:
|
|
49
67
|
"""`up -d` erstellt nginx nicht neu, wenn nur die gemountete nginx.conf sich ändert."""
|
|
50
68
|
compose(instance_dir, "exec", "-T", "nginx", "nginx", "-t")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Prüft die Fremdservice-Images eines Updates: Major-Wechsel und `requires` des Releases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import operator
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from .release import ReleaseImages, ReleaseRequirements
|
|
9
|
+
|
|
10
|
+
# Compose-Service → Rolle in ReleaseImages/ReleaseRequirements, für die ein Major-Wechsel
|
|
11
|
+
# nicht per Image-Tausch geht: Postgres braucht Dump/Restore, Elasticsearch öffnet ein
|
|
12
|
+
# Datenverzeichnis einer anderen Major-Version nicht (Rückweg gar nicht).
|
|
13
|
+
DATA_SERVICES = {"db": "postgres", "elasticsearch": "elasticsearch"}
|
|
14
|
+
|
|
15
|
+
_OPS = {">=": operator.ge, "<=": operator.le, "==": operator.eq, ">": operator.gt, "<": operator.lt}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def image_version(image: str) -> tuple[int, ...] | None:
|
|
19
|
+
"""Version aus dem Tag (`postgis/postgis:16-3.4` → (16,), `…:8.13.4` → (8, 13, 4))."""
|
|
20
|
+
name = image.rsplit("/", 1)[-1]
|
|
21
|
+
if ":" not in name:
|
|
22
|
+
return None
|
|
23
|
+
match = re.match(r"\d+(?:\.\d+)*", name.split(":", 1)[1])
|
|
24
|
+
return tuple(int(part) for part in match.group(0).split(".")) if match else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def satisfies(version: tuple[int, ...], spec: str) -> bool:
|
|
28
|
+
"""Minimaler Specifier-Vergleich für `requires` (z. B. ">=8.13,<9")."""
|
|
29
|
+
for clause in spec.split(","):
|
|
30
|
+
match = re.fullmatch(r"\s*(>=|<=|==|>|<)\s*(\d+(?:\.\d+)*)\s*", clause)
|
|
31
|
+
if not match:
|
|
32
|
+
raise ValueError(f"Ungültige Versionsanforderung: {spec}")
|
|
33
|
+
bound = tuple(int(part) for part in match.group(2).split("."))
|
|
34
|
+
if not _OPS[match.group(1)](version[: len(bound)], bound):
|
|
35
|
+
return False
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def update_problems(
|
|
40
|
+
current: dict[str, str], target: dict[str, str], requires: ReleaseRequirements
|
|
41
|
+
) -> tuple[list[str], list[str]]:
|
|
42
|
+
"""(Fehler, Hinweise) für den Wechsel der wirksamen Images `current` → `target`."""
|
|
43
|
+
errors: list[str] = []
|
|
44
|
+
notes: list[str] = []
|
|
45
|
+
for service, role in DATA_SERVICES.items():
|
|
46
|
+
old, new = current.get(service), target.get(service)
|
|
47
|
+
if not new:
|
|
48
|
+
continue
|
|
49
|
+
new_version = image_version(new)
|
|
50
|
+
spec = getattr(requires, role)
|
|
51
|
+
if spec and new_version and not satisfies(new_version, spec):
|
|
52
|
+
errors.append(f"{service}: {new} erfüllt die Anforderung des Releases nicht ({role} {spec}).")
|
|
53
|
+
elif spec and not new_version:
|
|
54
|
+
notes.append(f"{service}: Version von {new} nicht erkennbar, Anforderung {spec} ungeprüft.")
|
|
55
|
+
old_version = image_version(old) if old else None
|
|
56
|
+
if old_version and new_version and old_version[0] != new_version[0]:
|
|
57
|
+
errors.append(
|
|
58
|
+
f"{service}: Major-Wechsel {old} → {new} geht nicht per Image-Tausch "
|
|
59
|
+
"(Postgres: Dump/Restore, Elasticsearch: neues Datenverzeichnis und Reindex)."
|
|
60
|
+
)
|
|
61
|
+
return errors, notes
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def rollback_images(current: ReleaseImages, previous: ReleaseImages) -> ReleaseImages:
|
|
65
|
+
"""Images für den Rollback: die des vorigen Releases, außer einem neueren Elasticsearch.
|
|
66
|
+
|
|
67
|
+
Elasticsearch öffnet ein Datenverzeichnis nicht mehr mit einer älteren Version; die
|
|
68
|
+
Katalon-Version davor läuft mit dem neueren Minor-Release derselben Major-Version."""
|
|
69
|
+
cur, prev = image_version(current.elasticsearch), image_version(previous.elasticsearch)
|
|
70
|
+
if cur and prev and cur[0] == prev[0] and cur > prev:
|
|
71
|
+
return previous.model_copy(update={"elasticsearch": current.elasticsearch})
|
|
72
|
+
return previous
|
|
@@ -13,6 +13,20 @@ class ReleaseRequirements(BaseModel):
|
|
|
13
13
|
elasticsearch: str | None = None
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class ReleaseImages(BaseModel):
|
|
17
|
+
"""Images der Fremdservices, die das Katalon-Release festlegt (`images` in katalon-release.json).
|
|
18
|
+
|
|
19
|
+
Die Defaults sind der Stand der Releases vor diesem Feld (bis Katalon 1.46.0) und
|
|
20
|
+
bleiben deshalb unverändert; neue Versionen kommen über das Release, nicht hierher."""
|
|
21
|
+
|
|
22
|
+
postgres: str = "postgis/postgis:16-3.4"
|
|
23
|
+
redis: str = "redis:7-alpine"
|
|
24
|
+
elasticsearch: str = "docker.elastic.co/elasticsearch/elasticsearch:8.13.4"
|
|
25
|
+
cantaloupe: str = "islandora/cantaloupe:7.0.22"
|
|
26
|
+
nginx: str = "nginx:alpine"
|
|
27
|
+
caddy: str = "caddy:2-alpine"
|
|
28
|
+
|
|
29
|
+
|
|
16
30
|
class ReleaseEnvVar(BaseModel):
|
|
17
31
|
"""Neu hinzugekommene Umgebungsvariable eines Releases (katalon-release.json)."""
|
|
18
32
|
|
|
@@ -38,10 +52,22 @@ class ReleaseMetadata(BaseModel):
|
|
|
38
52
|
reindex_required: bool = False
|
|
39
53
|
breaking: bool
|
|
40
54
|
compose_revision: int
|
|
55
|
+
# Fehlt bei Releases bis Katalon 1.46.0; dort galten die Images des Templates.
|
|
56
|
+
images: ReleaseImages | None = None
|
|
41
57
|
requires: ReleaseRequirements = ReleaseRequirements()
|
|
42
58
|
env_vars: list[ReleaseEnvVar] = Field(default_factory=list)
|
|
43
59
|
deprecated_env_vars: list[DeprecatedEnvVar] = Field(default_factory=list)
|
|
44
60
|
|
|
61
|
+
@property
|
|
62
|
+
def service_images(self) -> ReleaseImages:
|
|
63
|
+
return self.images or ReleaseImages()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def checked_requires(self) -> ReleaseRequirements:
|
|
67
|
+
"""`requires` gilt erst ab Releases mit `images`; davor prüfte es niemand, und der
|
|
68
|
+
Wert war nicht verlässlich (1.46.0 nennt Elasticsearch >=8.15, läuft aber mit 8.13.4)."""
|
|
69
|
+
return self.requires if self.images else ReleaseRequirements()
|
|
70
|
+
|
|
45
71
|
|
|
46
72
|
def _fetch_release_asset(tag: str | None) -> dict:
|
|
47
73
|
"""Lädt katalon-release.json vom GitHub-Release (tag=None → latest)."""
|
|
@@ -18,6 +18,8 @@ class HistoryEntry(BaseModel):
|
|
|
18
18
|
compose_revision: int
|
|
19
19
|
action: str # "install" | "update" | "rollback"
|
|
20
20
|
at: datetime
|
|
21
|
+
# Images der Fremdservices aus dem Release (Rolle → Image); fehlt bei Einträgen vor katalon-cli 0.4.0.
|
|
22
|
+
images: dict[str, str] = Field(default_factory=dict)
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class InstallationState(BaseModel):
|
|
@@ -41,13 +43,16 @@ class InstallationState(BaseModel):
|
|
|
41
43
|
path = instance_dir / STATE_FILENAME
|
|
42
44
|
path.write_text(self.model_dump_json(indent=2))
|
|
43
45
|
|
|
44
|
-
def record(
|
|
46
|
+
def record(
|
|
47
|
+
self, version: str, compose_revision: int, action: str, images: dict[str, str] | None = None
|
|
48
|
+
) -> None:
|
|
45
49
|
self.history.append(
|
|
46
50
|
HistoryEntry(
|
|
47
51
|
version=version,
|
|
48
52
|
compose_revision=compose_revision,
|
|
49
53
|
action=action,
|
|
50
54
|
at=datetime.now(timezone.utc),
|
|
55
|
+
images=images or {},
|
|
51
56
|
)
|
|
52
57
|
)
|
|
53
58
|
self.version = version
|
|
@@ -29,8 +29,9 @@ from rich.table import Table
|
|
|
29
29
|
|
|
30
30
|
from .core import backup as backup_mod
|
|
31
31
|
from .core import checks, docker, permissions, release, selfupdate
|
|
32
|
+
from .core import images as images_mod
|
|
32
33
|
from .core import reindex as reindex_mod
|
|
33
|
-
from .core.compose_gen import caddy_host, write_compose
|
|
34
|
+
from .core.compose_gen import caddy_host, render_compose, write_compose
|
|
34
35
|
from .core.paths import default_instance_dir, save_instance_dir
|
|
35
36
|
from .core.state import InstallationState, instance_dir_or_raise
|
|
36
37
|
|
|
@@ -76,6 +77,7 @@ OPTIONAL_ENV_BLOCK = f"""
|
|
|
76
77
|
# SMTP_FROM=
|
|
77
78
|
# CORS_ORIGINS=["http://localhost"]
|
|
78
79
|
# MAX_UPLOAD_SIZE_MB=200
|
|
80
|
+
# ATTACHMENT_MAX_UPLOAD_MB=200
|
|
79
81
|
"""
|
|
80
82
|
|
|
81
83
|
|
|
@@ -369,7 +371,7 @@ def install(
|
|
|
369
371
|
progress.add_task("Schreibe compose.yaml + .env …", total=None)
|
|
370
372
|
dir.mkdir(parents=True, exist_ok=True)
|
|
371
373
|
Path(media_root).mkdir(parents=True, exist_ok=True)
|
|
372
|
-
write_compose(dir, version=meta.version, base_url=base_url, tls_mode=tls_mode)
|
|
374
|
+
write_compose(dir, version=meta.version, base_url=base_url, tls_mode=tls_mode, images=meta.service_images)
|
|
373
375
|
|
|
374
376
|
_ensure_env_vars(dir, base_url, media_root=media_root, release_env_vars=meta.env_vars, provided=provided)
|
|
375
377
|
_write_override_example(dir)
|
|
@@ -381,7 +383,7 @@ def install(
|
|
|
381
383
|
tls_mode=tls_mode,
|
|
382
384
|
installed_at=datetime.now(timezone.utc),
|
|
383
385
|
)
|
|
384
|
-
state.record(meta.version, meta.compose_revision, "install")
|
|
386
|
+
state.record(meta.version, meta.compose_revision, "install", images=meta.service_images.model_dump())
|
|
385
387
|
state.save(dir)
|
|
386
388
|
|
|
387
389
|
save_instance_dir(dir)
|
|
@@ -456,6 +458,40 @@ def _media_root(dir: Path) -> str:
|
|
|
456
458
|
return str(path if path.is_absolute() else dir / path)
|
|
457
459
|
|
|
458
460
|
|
|
461
|
+
def _check_update_images(dir: Path, state: InstallationState, meta: release.ReleaseMetadata) -> None:
|
|
462
|
+
"""Zeigt, welche Fremdservice-Images das Update wechselt, und bricht ab, wenn eins nicht passt.
|
|
463
|
+
|
|
464
|
+
Verglichen werden die wirksamen Images (inkl. compose.override.yaml) vor und nach dem
|
|
465
|
+
Update, damit auch ein eigenes Image aus dem Override gegen `requires` geprüft wird."""
|
|
466
|
+
preview = dir / "compose.yaml.next"
|
|
467
|
+
try:
|
|
468
|
+
preview.write_text(
|
|
469
|
+
render_compose(
|
|
470
|
+
version=meta.version, base_url=state.base_url, tls_mode=state.tls_mode, images=meta.service_images
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
current = docker.config_images(dir)
|
|
474
|
+
target = docker.config_images(dir, preview)
|
|
475
|
+
except (docker.DockerError, subprocess.CalledProcessError, ValueError) as exc:
|
|
476
|
+
console.print(f"[yellow]⚠ Images der Fremdservices nicht prüfbar: {exc}[/]")
|
|
477
|
+
return
|
|
478
|
+
finally:
|
|
479
|
+
preview.unlink(missing_ok=True)
|
|
480
|
+
|
|
481
|
+
for service in sorted(target):
|
|
482
|
+
old, new = current.get(service), target[service]
|
|
483
|
+
if old and old != new and "katalon-collections/katalon-" not in new:
|
|
484
|
+
console.print(f"[yellow]⚠ {service}: {old} → {new}[/]")
|
|
485
|
+
errors, notes = images_mod.update_problems(current, target, meta.checked_requires)
|
|
486
|
+
for note in notes:
|
|
487
|
+
console.print(f"[yellow]⚠ {note}[/]")
|
|
488
|
+
if errors:
|
|
489
|
+
for error in errors:
|
|
490
|
+
console.print(f"[red]✖ {error}[/]")
|
|
491
|
+
console.print("[red]Update abgebrochen, nichts geändert.[/]")
|
|
492
|
+
raise typer.Exit(1)
|
|
493
|
+
|
|
494
|
+
|
|
459
495
|
def _require_installer_version(minimum: str) -> None:
|
|
460
496
|
"""Bricht ab, wenn das Release eine neuere CLI braucht (z. B. neues Compose-Template).
|
|
461
497
|
|
|
@@ -730,6 +766,8 @@ def update(
|
|
|
730
766
|
f"(Revision {state.compose_revision} → {meta.compose_revision})[/]"
|
|
731
767
|
)
|
|
732
768
|
|
|
769
|
+
_check_update_images(dir, state, meta)
|
|
770
|
+
|
|
733
771
|
env_existing = _parse_env_file((dir / ".env").read_text() if (dir / ".env").exists() else "")
|
|
734
772
|
_print_new_env_vars(env_existing, env_vars)
|
|
735
773
|
_print_deprecated_env_vars(env_existing, deprecated_env_vars)
|
|
@@ -749,7 +787,9 @@ def update(
|
|
|
749
787
|
),
|
|
750
788
|
(
|
|
751
789
|
"compose.yaml rendern",
|
|
752
|
-
lambda: write_compose(
|
|
790
|
+
lambda: write_compose(
|
|
791
|
+
dir, version=meta.version, base_url=state.base_url, tls_mode=state.tls_mode, images=meta.service_images
|
|
792
|
+
),
|
|
753
793
|
),
|
|
754
794
|
("Images pullen", lambda: docker.compose(dir, "pull")),
|
|
755
795
|
("Medienrechte prüfen", lambda: _ensure_media_permissions(dir)),
|
|
@@ -782,7 +822,7 @@ def update(
|
|
|
782
822
|
console.print("[red]✖ Healthcheck fehlgeschlagen — `katalon rollback` erwägen.[/]")
|
|
783
823
|
raise typer.Exit(1)
|
|
784
824
|
|
|
785
|
-
state.record(meta.version, meta.compose_revision, "update")
|
|
825
|
+
state.record(meta.version, meta.compose_revision, "update", images=meta.service_images.model_dump())
|
|
786
826
|
state.save(dir)
|
|
787
827
|
console.print(f"[green]✔[/] Update auf {meta.version} abgeschlossen.")
|
|
788
828
|
|
|
@@ -840,7 +880,12 @@ def rollback(
|
|
|
840
880
|
)
|
|
841
881
|
|
|
842
882
|
if previous:
|
|
843
|
-
|
|
883
|
+
images = images_mod.rollback_images(
|
|
884
|
+
release.ReleaseImages(**state.history[-1].images), release.ReleaseImages(**previous.images)
|
|
885
|
+
)
|
|
886
|
+
write_compose(
|
|
887
|
+
dir, version=previous.version, base_url=state.base_url, tls_mode=state.tls_mode, images=images
|
|
888
|
+
)
|
|
844
889
|
docker.compose(dir, "pull")
|
|
845
890
|
|
|
846
891
|
_ensure_media_permissions(dir)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
services:
|
|
2
2
|
db:
|
|
3
|
-
image:
|
|
3
|
+
image: {{ images.postgres }}
|
|
4
4
|
platform: linux/amd64
|
|
5
5
|
restart: unless-stopped
|
|
6
6
|
environment:
|
|
@@ -16,7 +16,7 @@ services:
|
|
|
16
16
|
retries: 10
|
|
17
17
|
|
|
18
18
|
redis:
|
|
19
|
-
image: redis
|
|
19
|
+
image: {{ images.redis }}
|
|
20
20
|
restart: unless-stopped
|
|
21
21
|
healthcheck:
|
|
22
22
|
test: ["CMD", "redis-cli", "ping"]
|
|
@@ -25,7 +25,7 @@ services:
|
|
|
25
25
|
retries: 10
|
|
26
26
|
|
|
27
27
|
elasticsearch:
|
|
28
|
-
image:
|
|
28
|
+
image: {{ images.elasticsearch }}
|
|
29
29
|
restart: unless-stopped
|
|
30
30
|
environment:
|
|
31
31
|
discovery.type: single-node
|
|
@@ -42,7 +42,7 @@ services:
|
|
|
42
42
|
# Legt den Derivat-Cache an und übergibt ihn dem Cantaloupe-User des Images.
|
|
43
43
|
# Ändert nur Einträge mit falschem Eigentümer (idempotent, schnell bei vollem Cache).
|
|
44
44
|
cantaloupe-cache-init:
|
|
45
|
-
image:
|
|
45
|
+
image: {{ images.cantaloupe }}
|
|
46
46
|
user: "0:0"
|
|
47
47
|
entrypoint: ["sh", "-c", "mkdir -p /data/cache && find /data ! -user cantaloupe -exec chown cantaloupe:cantaloupe {} +"]
|
|
48
48
|
volumes:
|
|
@@ -50,7 +50,7 @@ services:
|
|
|
50
50
|
restart: "no"
|
|
51
51
|
|
|
52
52
|
cantaloupe:
|
|
53
|
-
image:
|
|
53
|
+
image: {{ images.cantaloupe }}
|
|
54
54
|
restart: unless-stopped
|
|
55
55
|
volumes:
|
|
56
56
|
- ${MEDIA_ROOT:-./media}:/var/lib/cantaloupe/images:ro
|
|
@@ -109,6 +109,7 @@ services:
|
|
|
109
109
|
CANTALOUPE_URL: ${CANTALOUPE_URL:-http://cantaloupe:8182}
|
|
110
110
|
CANTALOUPE_PUBLIC_URL: ${CANTALOUPE_PUBLIC_URL:-http://localhost}
|
|
111
111
|
MEDIA_ROOT: /var/lib/katalon/media
|
|
112
|
+
ATTACHMENT_MAX_UPLOAD_MB: ${ATTACHMENT_MAX_UPLOAD_MB:-200}
|
|
112
113
|
FIRST_RUN_CREDENTIALS_PATH: ${FIRST_RUN_CREDENTIALS_PATH:-/var/lib/katalon/first-run-credentials.txt}
|
|
113
114
|
DEBUG: "false"
|
|
114
115
|
volumes:
|
|
@@ -136,6 +137,7 @@ services:
|
|
|
136
137
|
CANTALOUPE_URL: ${CANTALOUPE_URL:-http://cantaloupe:8182}
|
|
137
138
|
CANTALOUPE_PUBLIC_URL: ${CANTALOUPE_PUBLIC_URL:-http://localhost}
|
|
138
139
|
MEDIA_ROOT: /var/lib/katalon/media
|
|
140
|
+
ATTACHMENT_MAX_UPLOAD_MB: ${ATTACHMENT_MAX_UPLOAD_MB:-200}
|
|
139
141
|
volumes:
|
|
140
142
|
- ${MEDIA_ROOT:-./media}:/var/lib/katalon/media
|
|
141
143
|
depends_on:
|
|
@@ -185,7 +187,7 @@ services:
|
|
|
185
187
|
api: { condition: service_healthy }
|
|
186
188
|
|
|
187
189
|
nginx:
|
|
188
|
-
image: nginx
|
|
190
|
+
image: {{ images.nginx }}
|
|
189
191
|
restart: unless-stopped
|
|
190
192
|
volumes:
|
|
191
193
|
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
|
@@ -210,7 +212,7 @@ services:
|
|
|
210
212
|
|
|
211
213
|
{% if tls_mode == "caddy" %}
|
|
212
214
|
caddy:
|
|
213
|
-
image: caddy
|
|
215
|
+
image: {{ images.caddy }}
|
|
214
216
|
restart: unless-stopped
|
|
215
217
|
ports:
|
|
216
218
|
- "80:80"
|
|
@@ -38,6 +38,25 @@ server {
|
|
|
38
38
|
proxy_set_header X-Forwarded-Proto {{ '$http_x_forwarded_proto' if tls_mode == 'caddy' else '$scheme' }};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
# Attachment and media files bring their own CSP per content type from the API (sandbox for image/audio/video,
|
|
42
|
+
# PDF-capable for PDF). The server-level page CSP (object-src 'none') blocks the browser PDF viewer,
|
|
43
|
+
# so this location omits CSP and X-Content-Type-Options, which the API sets itself.
|
|
44
|
+
# An add_header here drops the server-level ones: the remaining headers are repeated.
|
|
45
|
+
location ~ "^/(portal/)?v1/(attachments/[0-9a-fA-F-]{36}/(file|stream)|objects/[0-9a-fA-F-]{36}/media/[0-9a-fA-F-]{36}/(file|stream)|media/[0-9a-fA-F-]{36}/(file|download))$" {
|
|
46
|
+
set $upstream_api api:8000;
|
|
47
|
+
proxy_pass http://$upstream_api;
|
|
48
|
+
proxy_set_header Host $host;
|
|
49
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
50
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
51
|
+
proxy_set_header X-Forwarded-Proto {{ '$http_x_forwarded_proto' if tls_mode == 'caddy' else '$scheme' }};
|
|
52
|
+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
53
|
+
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
54
|
+
add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always;
|
|
55
|
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
56
|
+
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
|
57
|
+
add_header Cross-Origin-Resource-Policy "same-origin" always;
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
location /v1/ {
|
|
42
61
|
set $upstream_api api:8000;
|
|
43
62
|
proxy_pass http://$upstream_api;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|