katalon-cli 0.3.4__tar.gz → 0.4.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 (22) hide show
  1. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/PKG-INFO +24 -2
  2. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/README.md +23 -1
  3. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/pyproject.toml +1 -1
  4. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/pyproject.toml.orig +1 -1
  5. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/compose_gen.py +5 -1
  6. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/docker.py +18 -0
  7. katalon_cli-0.4.0/src/katalon_cli/core/images.py +72 -0
  8. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/release.py +26 -0
  9. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/state.py +6 -1
  10. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/main.py +50 -6
  11. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/templates/compose.yaml.j2 +15 -7
  12. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/__init__.py +0 -0
  13. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/__init__.py +0 -0
  14. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/backup.py +0 -0
  15. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/checks.py +0 -0
  16. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/paths.py +0 -0
  17. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/permissions.py +0 -0
  18. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/reindex.py +0 -0
  19. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/core/selfupdate.py +0 -0
  20. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/py.typed +0 -0
  21. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/templates/Caddyfile.j2 +0 -0
  22. {katalon_cli-0.3.4 → katalon_cli-0.4.0}/src/katalon_cli/templates/nginx.conf.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: katalon-cli
3
- Version: 0.3.4
3
+ Version: 0.4.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,6 +1,6 @@
1
1
  [project]
2
2
  name = "katalon-cli"
3
- version = "0.3.4"
3
+ version = "0.4.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.3.4"
3
+ version = "0.4.0"
4
4
  description = "Installer & Updater CLI für Katalon Production-Instanzen"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,4 +1,4 @@
1
- """Rendert compose.yaml + nginx.conf aus Template + Release-Version + TLS-Modus."""
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(self, version: str, compose_revision: int, action: str) -> None:
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
 
@@ -369,7 +370,7 @@ def install(
369
370
  progress.add_task("Schreibe compose.yaml + .env …", total=None)
370
371
  dir.mkdir(parents=True, exist_ok=True)
371
372
  Path(media_root).mkdir(parents=True, exist_ok=True)
372
- write_compose(dir, version=meta.version, base_url=base_url, tls_mode=tls_mode)
373
+ write_compose(dir, version=meta.version, base_url=base_url, tls_mode=tls_mode, images=meta.service_images)
373
374
 
374
375
  _ensure_env_vars(dir, base_url, media_root=media_root, release_env_vars=meta.env_vars, provided=provided)
375
376
  _write_override_example(dir)
@@ -381,7 +382,7 @@ def install(
381
382
  tls_mode=tls_mode,
382
383
  installed_at=datetime.now(timezone.utc),
383
384
  )
384
- state.record(meta.version, meta.compose_revision, "install")
385
+ state.record(meta.version, meta.compose_revision, "install", images=meta.service_images.model_dump())
385
386
  state.save(dir)
386
387
 
387
388
  save_instance_dir(dir)
@@ -456,6 +457,40 @@ def _media_root(dir: Path) -> str:
456
457
  return str(path if path.is_absolute() else dir / path)
457
458
 
458
459
 
460
+ def _check_update_images(dir: Path, state: InstallationState, meta: release.ReleaseMetadata) -> None:
461
+ """Zeigt, welche Fremdservice-Images das Update wechselt, und bricht ab, wenn eins nicht passt.
462
+
463
+ Verglichen werden die wirksamen Images (inkl. compose.override.yaml) vor und nach dem
464
+ Update, damit auch ein eigenes Image aus dem Override gegen `requires` geprüft wird."""
465
+ preview = dir / "compose.yaml.next"
466
+ try:
467
+ preview.write_text(
468
+ render_compose(
469
+ version=meta.version, base_url=state.base_url, tls_mode=state.tls_mode, images=meta.service_images
470
+ )
471
+ )
472
+ current = docker.config_images(dir)
473
+ target = docker.config_images(dir, preview)
474
+ except (docker.DockerError, subprocess.CalledProcessError, ValueError) as exc:
475
+ console.print(f"[yellow]⚠ Images der Fremdservices nicht prüfbar: {exc}[/]")
476
+ return
477
+ finally:
478
+ preview.unlink(missing_ok=True)
479
+
480
+ for service in sorted(target):
481
+ old, new = current.get(service), target[service]
482
+ if old and old != new and "katalon-collections/katalon-" not in new:
483
+ console.print(f"[yellow]⚠ {service}: {old} → {new}[/]")
484
+ errors, notes = images_mod.update_problems(current, target, meta.checked_requires)
485
+ for note in notes:
486
+ console.print(f"[yellow]⚠ {note}[/]")
487
+ if errors:
488
+ for error in errors:
489
+ console.print(f"[red]✖ {error}[/]")
490
+ console.print("[red]Update abgebrochen, nichts geändert.[/]")
491
+ raise typer.Exit(1)
492
+
493
+
459
494
  def _require_installer_version(minimum: str) -> None:
460
495
  """Bricht ab, wenn das Release eine neuere CLI braucht (z. B. neues Compose-Template).
461
496
 
@@ -730,6 +765,8 @@ def update(
730
765
  f"(Revision {state.compose_revision} → {meta.compose_revision})[/]"
731
766
  )
732
767
 
768
+ _check_update_images(dir, state, meta)
769
+
733
770
  env_existing = _parse_env_file((dir / ".env").read_text() if (dir / ".env").exists() else "")
734
771
  _print_new_env_vars(env_existing, env_vars)
735
772
  _print_deprecated_env_vars(env_existing, deprecated_env_vars)
@@ -749,7 +786,9 @@ def update(
749
786
  ),
750
787
  (
751
788
  "compose.yaml rendern",
752
- lambda: write_compose(dir, version=meta.version, base_url=state.base_url, tls_mode=state.tls_mode),
789
+ lambda: write_compose(
790
+ dir, version=meta.version, base_url=state.base_url, tls_mode=state.tls_mode, images=meta.service_images
791
+ ),
753
792
  ),
754
793
  ("Images pullen", lambda: docker.compose(dir, "pull")),
755
794
  ("Medienrechte prüfen", lambda: _ensure_media_permissions(dir)),
@@ -782,7 +821,7 @@ def update(
782
821
  console.print("[red]✖ Healthcheck fehlgeschlagen — `katalon rollback` erwägen.[/]")
783
822
  raise typer.Exit(1)
784
823
 
785
- state.record(meta.version, meta.compose_revision, "update")
824
+ state.record(meta.version, meta.compose_revision, "update", images=meta.service_images.model_dump())
786
825
  state.save(dir)
787
826
  console.print(f"[green]✔[/] Update auf {meta.version} abgeschlossen.")
788
827
 
@@ -840,7 +879,12 @@ def rollback(
840
879
  )
841
880
 
842
881
  if previous:
843
- write_compose(dir, version=previous.version, base_url=state.base_url, tls_mode=state.tls_mode)
882
+ images = images_mod.rollback_images(
883
+ release.ReleaseImages(**state.history[-1].images), release.ReleaseImages(**previous.images)
884
+ )
885
+ write_compose(
886
+ dir, version=previous.version, base_url=state.base_url, tls_mode=state.tls_mode, images=images
887
+ )
844
888
  docker.compose(dir, "pull")
845
889
 
846
890
  _ensure_media_permissions(dir)
@@ -1,6 +1,6 @@
1
1
  services:
2
2
  db:
3
- image: postgis/postgis:16-3.4
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:7-alpine
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: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
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: islandora/cantaloupe:main
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: islandora/cantaloupe:main
53
+ image: {{ images.cantaloupe }}
54
54
  restart: unless-stopped
55
55
  volumes:
56
56
  - ${MEDIA_ROOT:-./media}:/var/lib/cantaloupe/images:ro
@@ -65,6 +65,10 @@ services:
65
65
  depends_on:
66
66
  cantaloupe-cache-init: { condition: service_completed_successfully }
67
67
  environment:
68
+ # Image-Default wäre 3G/5G fest reserviert; gemessen reichen unter paralleler
69
+ # Last ~0,6 GB. Große Scans: CANTALOUPE_HEAP_MAX in .env erhöhen.
70
+ CANTALOUPE_HEAP_MIN: ${CANTALOUPE_HEAP_MIN:-256m}
71
+ CANTALOUPE_HEAP_MAX: ${CANTALOUPE_HEAP_MAX:-1g}
68
72
  CANTALOUPE_SOURCE_STATIC: FilesystemSource
69
73
  CANTALOUPE_FILESYSTEMSOURCE_LOOKUP_STRATEGY: BasicLookupStrategy
70
74
  CANTALOUPE_FILESYSTEMSOURCE_BASICLOOKUPSTRATEGY_PATH_PREFIX: /var/lib/cantaloupe/images/
@@ -80,6 +84,10 @@ services:
80
84
  # nach Sichtbarkeit (X-Media-Visibility aus /v1/media/_authorize).
81
85
  CANTALOUPE_CACHE_CLIENT_PUBLIC: "false"
82
86
  CANTALOUPE_CACHE_CLIENT_PRIVATE: "true"
87
+ # Das Image loggt ab Werk auf debug (mehrere Zeilen pro Bild und Healthcheck);
88
+ # Zugriffe protokolliert nginx bereits. Zur Fehlersuche CANTALOUPE_LOG_LEVEL=debug.
89
+ CANTALOUPE_LOG_APPLICATION_LEVEL: ${CANTALOUPE_LOG_LEVEL:-info}
90
+ CANTALOUPE_LOG_ACCESS_CONSOLEAPPENDER_ENABLED: "false"
83
91
  healthcheck:
84
92
  # Prüft neben der Erreichbarkeit, dass der Cantaloupe-Prozess in seinen Cache
85
93
  # schreiben kann — sonst liefert er Bilder mit 0 Bytes bei HTTP 200.
@@ -177,7 +185,7 @@ services:
177
185
  api: { condition: service_healthy }
178
186
 
179
187
  nginx:
180
- image: nginx:alpine
188
+ image: {{ images.nginx }}
181
189
  restart: unless-stopped
182
190
  volumes:
183
191
  - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
@@ -202,7 +210,7 @@ services:
202
210
 
203
211
  {% if tls_mode == "caddy" %}
204
212
  caddy:
205
- image: caddy:2-alpine
213
+ image: {{ images.caddy }}
206
214
  restart: unless-stopped
207
215
  ports:
208
216
  - "80:80"