katalon-cli 0.1.2__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: katalon-cli
3
- Version: 0.1.2
3
+ Version: 0.1.3
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,7 +66,13 @@ uv run pytest
66
66
 
67
67
  - `core/state.py` — `installation.json`, Single Source of Truth für laufende Version.
68
68
  - `core/release.py` — GitHub-Release-Metadaten (`katalon-release.json` Asset), 1h Cache.
69
- - `core/compose_gen.py` + `templates/compose.yaml.j2` — rendert `compose.yaml` aus Version + TLS-Modus.
69
+ - `core/compose_gen.py` + `templates/compose.yaml.j2` + `templates/nginx.conf.j2` — rendert `compose.yaml`
70
+ + `nginx.conf` aus Version + TLS-Modus; erzeugt bei `tls_mode=standalone` ein selbstsigniertes
71
+ Zertifikat unter `<dir>/certs/` (eigenes Zertifikat dort ablegen, um es zu ersetzen).
72
+ `install` fragt zusätzlich `MEDIA_ROOT` ab und legt `<dir>/compose.override.yaml.example` an
73
+ (instanzspezifische Overrides — umbenennen zu `compose.override.yaml`, wird automatisch eingebunden
74
+ und bei Updates nicht angefasst). Optionale Env-Vars (Admin-Login, SMTP, Geonames, …) landet
75
+ auskommentiert in `.env`.
70
76
  - `core/backup.py` — `pg_dump` vor jedem Update; Rollback restauriert Dump statt `alembic downgrade`.
71
77
  - `core/docker.py` — dünner `docker compose`-Subprocess-Wrapper.
72
78
  - `main.py` — Typer-Commands, interaktive Teile über `rich.prompt`/`rich.progress`.
@@ -52,7 +52,13 @@ uv run pytest
52
52
 
53
53
  - `core/state.py` — `installation.json`, Single Source of Truth für laufende Version.
54
54
  - `core/release.py` — GitHub-Release-Metadaten (`katalon-release.json` Asset), 1h Cache.
55
- - `core/compose_gen.py` + `templates/compose.yaml.j2` — rendert `compose.yaml` aus Version + TLS-Modus.
55
+ - `core/compose_gen.py` + `templates/compose.yaml.j2` + `templates/nginx.conf.j2` — rendert `compose.yaml`
56
+ + `nginx.conf` aus Version + TLS-Modus; erzeugt bei `tls_mode=standalone` ein selbstsigniertes
57
+ Zertifikat unter `<dir>/certs/` (eigenes Zertifikat dort ablegen, um es zu ersetzen).
58
+ `install` fragt zusätzlich `MEDIA_ROOT` ab und legt `<dir>/compose.override.yaml.example` an
59
+ (instanzspezifische Overrides — umbenennen zu `compose.override.yaml`, wird automatisch eingebunden
60
+ und bei Updates nicht angefasst). Optionale Env-Vars (Admin-Login, SMTP, Geonames, …) landet
61
+ auskommentiert in `.env`.
56
62
  - `core/backup.py` — `pg_dump` vor jedem Update; Rollback restauriert Dump statt `alembic downgrade`.
57
63
  - `core/docker.py` — dünner `docker compose`-Subprocess-Wrapper.
58
64
  - `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.2"
3
+ version = "0.1.3"
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.2"
3
+ version = "0.1.3"
4
4
  description = "Installer & Updater CLI für Katalon Production-Instanzen"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -49,7 +49,7 @@ def restore_backup(instance_dir: Path, backup_dir: Path) -> None:
49
49
  docker.compose(instance_dir, "exec", "-T", "db", "dropdb", "-U", "katalon", "katalon")
50
50
  docker.compose(instance_dir, "exec", "-T", "db", "createdb", "-U", "katalon", "katalon")
51
51
  subprocess.run(
52
- [*docker.compose_command(), "-f", str(instance_dir / "compose.yaml"),
52
+ [*docker.compose_command(), *docker.compose_files(instance_dir),
53
53
  "exec", "-T", "db", "psql", "-U", "katalon", "katalon"],
54
54
  input=dump_file.read_text(),
55
55
  cwd=instance_dir,
@@ -0,0 +1,82 @@
1
+ """Rendert compose.yaml + nginx.conf aus Template + Release-Version + TLS-Modus."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from jinja2 import Environment, PackageLoader, select_autoescape
9
+
10
+ _env = Environment(
11
+ loader=PackageLoader("katalon_cli", "templates"),
12
+ autoescape=select_autoescape(disabled_extensions=(".yaml", ".j2", ".conf")),
13
+ trim_blocks=True,
14
+ lstrip_blocks=True,
15
+ )
16
+
17
+
18
+ def render_compose(
19
+ *,
20
+ version: str,
21
+ base_url: str,
22
+ tls_mode: str,
23
+ registry: str = "ghcr.io/katalon-collections/katalon",
24
+ ) -> str:
25
+ template = _env.get_template("compose.yaml.j2")
26
+ return template.render(
27
+ version=version,
28
+ base_url=base_url,
29
+ tls_mode=tls_mode,
30
+ registry=registry,
31
+ )
32
+
33
+
34
+ def render_nginx_conf(*, tls_mode: str) -> str:
35
+ template = _env.get_template("nginx.conf.j2")
36
+ return template.render(tls_mode=tls_mode)
37
+
38
+
39
+ def write_compose(instance_dir: Path, **kwargs) -> Path:
40
+ content = render_compose(**kwargs)
41
+ path = instance_dir / "compose.yaml"
42
+ path.write_text(content)
43
+
44
+ nginx_path = instance_dir / "nginx.conf"
45
+ nginx_path.write_text(render_nginx_conf(tls_mode=kwargs["tls_mode"]))
46
+
47
+ if kwargs["tls_mode"] == "standalone":
48
+ ensure_self_signed_cert(instance_dir, host=_host_of(kwargs["base_url"]))
49
+
50
+ return path
51
+
52
+
53
+ def ensure_self_signed_cert(instance_dir: Path, *, host: str) -> None:
54
+ """Legt ein selbstsigniertes Zertifikat an, falls keins vorhanden ist.
55
+
56
+ Für echtes TLS eigenes Zertifikat (z.B. Let's Encrypt) unter
57
+ <instance_dir>/certs/fullchain.pem + privkey.pem ablegen — wird nicht überschrieben.
58
+ """
59
+ certs_dir = instance_dir / "certs"
60
+ fullchain = certs_dir / "fullchain.pem"
61
+ privkey = certs_dir / "privkey.pem"
62
+ if fullchain.exists() and privkey.exists():
63
+ return
64
+
65
+ certs_dir.mkdir(parents=True, exist_ok=True)
66
+ subprocess.run(
67
+ [
68
+ "openssl", "req", "-x509", "-nodes", "-newkey", "rsa:2048",
69
+ "-days", "825",
70
+ "-keyout", str(privkey),
71
+ "-out", str(fullchain),
72
+ "-subj", f"/CN={host}",
73
+ ],
74
+ check=True,
75
+ capture_output=True,
76
+ )
77
+
78
+
79
+ def _host_of(base_url: str) -> str:
80
+ from urllib.parse import urlparse
81
+
82
+ return urlparse(base_url).hostname or "localhost"
@@ -24,10 +24,18 @@ def compose_command() -> list[str]:
24
24
  raise DockerError("Docker Compose ist nicht installiert.")
25
25
 
26
26
 
27
+ def compose_files(instance_dir: Path) -> list[str]:
28
+ files = ["-f", str(instance_dir / "compose.yaml")]
29
+ override = instance_dir / "compose.override.yaml"
30
+ if override.exists():
31
+ files += ["-f", str(override)]
32
+ return files
33
+
34
+
27
35
  def compose(
28
36
  instance_dir: Path, *args: str, check: bool = True, capture: bool = False
29
37
  ) -> subprocess.CompletedProcess:
30
- cmd = [*compose_command(), "-f", str(instance_dir / "compose.yaml"), *args]
38
+ cmd = [*compose_command(), *compose_files(instance_dir), *args]
31
39
  return subprocess.run(
32
40
  cmd,
33
41
  cwd=instance_dir,
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import platform
6
6
  import secrets
7
+ import subprocess
7
8
  from datetime import datetime, timezone
8
9
  from pathlib import Path
9
10
  from urllib.parse import urlparse
@@ -26,12 +27,81 @@ console = Console()
26
27
  DEFAULT_DIR = default_instance_dir()
27
28
 
28
29
  TLS_CHOICES = {
29
- "standalone": "Standalone — Katalon verwaltet HTTPS via Caddy (Ports 80+443)",
30
+ "standalone": "Standalone — eigenes nginx mit selbstsigniertem Zertifikat (Ports 80+443)",
30
31
  "behind-proxy": "Hinter eigenem Reverse-Proxy — kein TLS hier",
31
32
  "none": "Kein TLS (lokal / IP)",
32
33
  }
33
34
 
34
35
 
36
+ OPTIONAL_ENV_MARKER = "# --- optionale Config (auskommentiert, bei Bedarf aktivieren) ---"
37
+ OPTIONAL_ENV_BLOCK = f"""
38
+ {OPTIONAL_ENV_MARKER}
39
+ # DEFAULT_ADMIN_EMAIL=admin@example.org
40
+ # DEFAULT_ADMIN_PASSWORD=changeme
41
+ # GEONAMES_USERNAME=demo
42
+ # SMTP_ENABLED=false
43
+ # SMTP_HOST=
44
+ # SMTP_PORT=587
45
+ # SMTP_USERNAME=
46
+ # SMTP_PASSWORD=
47
+ # SMTP_FROM=
48
+ # CORS_ORIGINS=["http://localhost"]
49
+ # MAX_UPLOAD_SIZE_MB=200
50
+ """
51
+
52
+
53
+ def _ensure_env_vars(dir: Path, base_url: str, media_root: str | None = None) -> None:
54
+ """Schreibt fehlende Pflicht-Env-Vars nach — idempotent, überschreibt nichts Vorhandenes."""
55
+ env_path = dir / ".env"
56
+ raw = env_path.read_text() if env_path.exists() else ""
57
+ existing: dict[str, str] = {}
58
+ for line in raw.splitlines():
59
+ if "=" in line and not line.startswith("#"):
60
+ key, _, value = line.partition("=")
61
+ existing[key] = value
62
+
63
+ postgres_password = existing.get("POSTGRES_PASSWORD", secrets.token_urlsafe(24))
64
+ defaults = {
65
+ "KATALON_BASE_URL": base_url,
66
+ "POSTGRES_PASSWORD": postgres_password,
67
+ "DATABASE_URL": f"postgresql://katalon:{postgres_password}@db:5432/katalon",
68
+ "SECRET_KEY": secrets.token_urlsafe(32),
69
+ "KATALON_SECRETS_KEY": secrets.token_urlsafe(32),
70
+ "CANTALOUPE_PUBLIC_URL": base_url,
71
+ }
72
+ if media_root is not None:
73
+ defaults["MEDIA_ROOT"] = media_root
74
+ for key, value in defaults.items():
75
+ existing.setdefault(key, value)
76
+
77
+ content = "".join(f"{key}={value}\n" for key, value in existing.items())
78
+ if OPTIONAL_ENV_MARKER not in raw:
79
+ content += OPTIONAL_ENV_BLOCK
80
+ env_path.write_text(content)
81
+
82
+
83
+ OVERRIDE_EXAMPLE = """\
84
+ # compose.override.yaml
85
+ # Instanzspezifische Anpassungen hier eintragen, dann in "compose.override.yaml"
86
+ # umbenennen — docker compose bindet sie automatisch neben compose.yaml ein und
87
+ # katalon-cli überschreibt sie bei Updates nicht.
88
+ services:
89
+ api:
90
+ environment:
91
+ - EXTRA_VAR=value
92
+
93
+ db:
94
+ volumes:
95
+ - /mnt/external-disk/katalon-db:/var/lib/postgresql/data
96
+ """
97
+
98
+
99
+ def _write_override_example(dir: Path) -> None:
100
+ path = dir / "compose.override.yaml.example"
101
+ if not path.exists():
102
+ path.write_text(OVERRIDE_EXAMPLE)
103
+
104
+
35
105
  def _run_checks(dir: Path, ports: list[int] | None = None) -> bool:
36
106
  ok = True
37
107
  for result in checks.run_all(dir, ports=ports):
@@ -65,6 +135,10 @@ def install(
65
135
  )
66
136
  base_url = Prompt.ask("KATALON_BASE_URL", default="http://localhost")
67
137
 
138
+ console.print()
139
+ console.print("MEDIA_ROOT — Verzeichnis auf dem Host, in dem Katalon Mediendateien ablegt.")
140
+ media_root = Prompt.ask("MEDIA_ROOT", default=str(dir / "media"))
141
+
68
142
  tls_mode = "none"
69
143
  if urlparse(base_url).hostname not in (None, "localhost", "127.0.0.1"):
70
144
  console.print()
@@ -92,15 +166,11 @@ def install(
92
166
 
93
167
  progress.add_task("Schreibe compose.yaml + .env …", total=None)
94
168
  dir.mkdir(parents=True, exist_ok=True)
169
+ Path(media_root).mkdir(parents=True, exist_ok=True)
95
170
  write_compose(dir, version=meta.version, base_url=base_url, tls_mode=tls_mode)
96
171
 
97
- env_path = dir / ".env"
98
- if not env_path.exists():
99
- env_path.write_text(
100
- f"KATALON_BASE_URL={base_url}\n"
101
- f"POSTGRES_PASSWORD={secrets.token_urlsafe(24)}\n"
102
- f"SECRET_KEY={secrets.token_urlsafe(32)}\n"
103
- )
172
+ _ensure_env_vars(dir, base_url, media_root=media_root)
173
+ _write_override_example(dir)
104
174
 
105
175
  state = InstallationState(
106
176
  version=meta.version,
@@ -113,6 +183,14 @@ def install(
113
183
  state.save(dir)
114
184
 
115
185
  console.print(f"[green]✔[/] Instanz eingerichtet in [bold]{dir}[/]")
186
+ console.print(
187
+ f"Optionale Config-Variablen sind auskommentiert in [bold]{dir / '.env'}[/] angelegt "
188
+ "(Admin-Login, SMTP, Geonames, …) — bei Bedarf einkommentieren."
189
+ )
190
+ console.print(
191
+ f"Instanzspezifische compose-Overrides: [bold]{dir / 'compose.override.yaml.example'}[/] "
192
+ "nach compose.override.yaml umbenennen."
193
+ )
116
194
  console.print("Starten mit: [bold]katalon start --dir " + str(dir) + "[/]")
117
195
 
118
196
 
@@ -219,6 +297,7 @@ def update(
219
297
 
220
298
  steps = [
221
299
  ("Backup erstellen", lambda: backup_mod.create_backup(dir)),
300
+ ("Env-Vars ergänzen", lambda: _ensure_env_vars(dir, state.base_url)),
222
301
  (
223
302
  "compose.yaml rendern",
224
303
  lambda: write_compose(dir, version=meta.version, base_url=state.base_url, tls_mode=state.tls_mode),
@@ -291,7 +370,7 @@ def rollback(
291
370
  def main() -> None:
292
371
  try:
293
372
  app()
294
- except (FileNotFoundError, docker.DockerError, backup_mod.BackupError) as exc:
373
+ except (FileNotFoundError, docker.DockerError, backup_mod.BackupError, subprocess.CalledProcessError) as exc:
295
374
  console.print(f"[red]✖[/] {exc}")
296
375
  raise SystemExit(1) from exc
297
376
 
@@ -0,0 +1,165 @@
1
+ services:
2
+ db:
3
+ image: postgis/postgis:16-3.4
4
+ restart: unless-stopped
5
+ environment:
6
+ POSTGRES_USER: ${POSTGRES_USER:-katalon}
7
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
8
+ POSTGRES_DB: ${POSTGRES_DB:-katalon}
9
+ volumes:
10
+ - db_data:/var/lib/postgresql/data
11
+ healthcheck:
12
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-katalon}"]
13
+ interval: 5s
14
+ timeout: 5s
15
+ retries: 10
16
+
17
+ redis:
18
+ image: redis:7-alpine
19
+ restart: unless-stopped
20
+ healthcheck:
21
+ test: ["CMD", "redis-cli", "ping"]
22
+ interval: 5s
23
+ timeout: 3s
24
+ retries: 10
25
+
26
+ elasticsearch:
27
+ image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
28
+ restart: unless-stopped
29
+ environment:
30
+ discovery.type: single-node
31
+ xpack.security.enabled: "false"
32
+ ES_JAVA_OPTS: "-Xms${ES_HEAP_SIZE:-512m} -Xmx${ES_HEAP_SIZE:-512m}"
33
+ volumes:
34
+ - es_data:/usr/share/elasticsearch/data
35
+ healthcheck:
36
+ test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health | grep -v red"]
37
+ interval: 15s
38
+ timeout: 10s
39
+ retries: 20
40
+
41
+ cantaloupe:
42
+ image: islandora/cantaloupe:main
43
+ restart: unless-stopped
44
+ volumes:
45
+ - ${MEDIA_ROOT:-./media}:/var/lib/cantaloupe/images:ro
46
+ - type: tmpfs
47
+ target: /var/lib/cantaloupe/cache
48
+ tmpfs:
49
+ size: ${CANTALOUPE_CACHE_TMPFS_SIZE:-512m}
50
+ environment:
51
+ CANTALOUPE_SOURCE_STATIC: FilesystemSource
52
+ CANTALOUPE_FILESYSTEMSOURCE_LOOKUP_STRATEGY: BasicLookupStrategy
53
+ CANTALOUPE_FILESYSTEMSOURCE_BASICLOOKUPSTRATEGY_PATH_PREFIX: /var/lib/cantaloupe/images/
54
+ CANTALOUPE_BASE_URI: ${CANTALOUPE_PUBLIC_URL:-http://localhost}
55
+ CANTALOUPE_CACHE_SERVER_DERIVATIVE: FilesystemCache
56
+ CANTALOUPE_CACHE_SERVER_DERIVATIVE_ENABLED: "true"
57
+ CANTALOUPE_FILESYSTEMCACHE_PATHNAME: /var/lib/cantaloupe/cache
58
+ CANTALOUPE_PROCESSOR_MANUALSELECTIONSTRATEGY_JPG: Java2dProcessor
59
+ healthcheck:
60
+ test: ["CMD-SHELL", "curl -sf http://localhost:8182/iiif/3"]
61
+ interval: 10s
62
+ timeout: 5s
63
+ retries: 10
64
+
65
+ api:
66
+ image: {{ registry }}-api:{{ version }}
67
+ restart: unless-stopped
68
+ env_file: .env
69
+ environment:
70
+ REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
71
+ ELASTICSEARCH_URL: ${ELASTICSEARCH_URL:-http://elasticsearch:9200}
72
+ CANTALOUPE_URL: ${CANTALOUPE_URL:-http://cantaloupe:8182}
73
+ CANTALOUPE_PUBLIC_URL: ${CANTALOUPE_PUBLIC_URL:-http://localhost}
74
+ MEDIA_ROOT: /var/lib/katalon/media
75
+ FIRST_RUN_CREDENTIALS_PATH: ${FIRST_RUN_CREDENTIALS_PATH:-/var/lib/katalon/first-run-credentials.txt}
76
+ DEBUG: "false"
77
+ volumes:
78
+ - ${MEDIA_ROOT:-./media}:/var/lib/katalon/media
79
+ depends_on:
80
+ db: { condition: service_healthy }
81
+ redis: { condition: service_healthy }
82
+ cantaloupe: { condition: service_healthy }
83
+ healthcheck:
84
+ test: ["CMD", "curl", "-sf", "http://localhost:8000/health"]
85
+ interval: 10s
86
+ timeout: 5s
87
+ retries: 10
88
+
89
+ worker:
90
+ image: {{ registry }}-worker:{{ version }}
91
+ restart: unless-stopped
92
+ env_file: .env
93
+ environment:
94
+ REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
95
+ CANTALOUPE_URL: ${CANTALOUPE_URL:-http://cantaloupe:8182}
96
+ CANTALOUPE_PUBLIC_URL: ${CANTALOUPE_PUBLIC_URL:-http://localhost}
97
+ MEDIA_ROOT: /var/lib/katalon/media
98
+ volumes:
99
+ - ${MEDIA_ROOT:-./media}:/var/lib/katalon/media
100
+ depends_on:
101
+ db: { condition: service_healthy }
102
+ redis: { condition: service_healthy }
103
+ cantaloupe: { condition: service_healthy }
104
+
105
+ beat:
106
+ image: {{ registry }}-worker:{{ version }}
107
+ restart: unless-stopped
108
+ command: ["celery", "-A", "katalon.workers.celery_app", "beat", "--loglevel=info"]
109
+ env_file: .env
110
+ environment:
111
+ REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
112
+ CANTALOUPE_URL: ${CANTALOUPE_URL:-http://cantaloupe:8182}
113
+ CANTALOUPE_PUBLIC_URL: ${CANTALOUPE_PUBLIC_URL:-http://localhost}
114
+ depends_on:
115
+ db: { condition: service_healthy }
116
+ redis: { condition: service_healthy }
117
+ cantaloupe: { condition: service_healthy }
118
+
119
+ admin:
120
+ image: {{ registry }}-admin:{{ version }}
121
+ restart: unless-stopped
122
+ healthcheck:
123
+ test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1/index.html > /dev/null 2>&1 || exit 1"]
124
+ interval: 10s
125
+ timeout: 5s
126
+ retries: 10
127
+ depends_on:
128
+ api: { condition: service_healthy }
129
+
130
+ portal:
131
+ image: {{ registry }}-portal:{{ version }}
132
+ restart: unless-stopped
133
+ healthcheck:
134
+ test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1/index.html > /dev/null 2>&1 || exit 1"]
135
+ interval: 10s
136
+ timeout: 5s
137
+ retries: 10
138
+ depends_on:
139
+ api: { condition: service_healthy }
140
+
141
+ nginx:
142
+ image: nginx:alpine
143
+ restart: unless-stopped
144
+ volumes:
145
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
146
+ {% if tls_mode == "standalone" %}
147
+ - ./certs:/etc/nginx/certs:ro
148
+ {% endif %}
149
+ ports:
150
+ {% if tls_mode == "standalone" %}
151
+ - "80:80"
152
+ - "443:443"
153
+ {% elif tls_mode == "behind-proxy" %}
154
+ - "127.0.0.1:8080:80"
155
+ {% else %}
156
+ - "80:80"
157
+ {% endif %}
158
+ depends_on:
159
+ api: { condition: service_healthy }
160
+ admin: { condition: service_healthy }
161
+ portal: { condition: service_healthy }
162
+
163
+ volumes:
164
+ db_data:
165
+ es_data:
@@ -0,0 +1,106 @@
1
+ resolver 127.0.0.11 valid=10s ipv6=off;
2
+
3
+ server {
4
+ {% if tls_mode == "standalone" %}
5
+ listen 80;
6
+ listen 443 ssl;
7
+ ssl_certificate /etc/nginx/certs/fullchain.pem;
8
+ ssl_certificate_key /etc/nginx/certs/privkey.pem;
9
+ {% else %}
10
+ listen 80;
11
+ {% endif %}
12
+ client_max_body_size 500m;
13
+
14
+ 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;
15
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
16
+ add_header X-Content-Type-Options "nosniff" always;
17
+ add_header X-Frame-Options "SAMEORIGIN" always;
18
+ add_header Permissions-Policy "camera=(), geolocation=(), microphone=()" always;
19
+
20
+ location /api/ {
21
+ set $upstream_api api:8000;
22
+ proxy_pass http://$upstream_api;
23
+ proxy_set_header Host $host;
24
+ proxy_set_header X-Real-IP $remote_addr;
25
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
26
+ proxy_set_header X-Forwarded-Proto $scheme;
27
+ }
28
+
29
+ location /v1/ {
30
+ set $upstream_api api:8000;
31
+ proxy_pass http://$upstream_api;
32
+ proxy_set_header Host $host;
33
+ proxy_set_header X-Real-IP $remote_addr;
34
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
35
+ proxy_set_header X-Forwarded-Proto $scheme;
36
+ }
37
+
38
+ location /portal/v1/ {
39
+ set $upstream_api api:8000;
40
+ proxy_pass http://$upstream_api;
41
+ proxy_set_header Host $host;
42
+ proxy_set_header X-Real-IP $remote_addr;
43
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44
+ proxy_set_header X-Forwarded-Proto $scheme;
45
+ }
46
+
47
+ location = /health {
48
+ set $upstream_api api:8000;
49
+ proxy_pass http://$upstream_api;
50
+ proxy_set_header Host $host;
51
+ proxy_set_header X-Real-IP $remote_addr;
52
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
53
+ proxy_set_header X-Forwarded-Proto $scheme;
54
+ }
55
+
56
+ location = /oai {
57
+ set $upstream_api api:8000;
58
+ proxy_pass http://$upstream_api;
59
+ proxy_set_header Host $host;
60
+ proxy_set_header X-Real-IP $remote_addr;
61
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
62
+ proxy_set_header X-Forwarded-Proto $scheme;
63
+ }
64
+
65
+ location ^~ /ark:/ {
66
+ set $upstream_api api:8000;
67
+ proxy_pass http://$upstream_api;
68
+ proxy_set_header Host $host;
69
+ proxy_set_header X-Real-IP $remote_addr;
70
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
71
+ proxy_set_header X-Forwarded-Proto $scheme;
72
+ }
73
+
74
+ location /iiif/ {
75
+ auth_request /internal/media-authorize;
76
+ set $upstream_cantaloupe cantaloupe:8182;
77
+ proxy_pass http://$upstream_cantaloupe;
78
+ proxy_set_header Host $host;
79
+ proxy_set_header X-Real-IP $remote_addr;
80
+ proxy_read_timeout 300s;
81
+ proxy_buffering off;
82
+ }
83
+
84
+ location = /internal/media-authorize {
85
+ internal;
86
+ set $upstream_api api:8000;
87
+ proxy_pass http://$upstream_api/v1/media/_authorize;
88
+ proxy_pass_request_body off;
89
+ proxy_set_header Content-Length "";
90
+ proxy_set_header X-Original-URI $request_uri;
91
+ proxy_set_header Authorization $http_authorization;
92
+ }
93
+
94
+ location /admin/ {
95
+ set $upstream_admin admin;
96
+ rewrite ^/admin/(.*)$ /$1 break;
97
+ proxy_pass http://$upstream_admin;
98
+ proxy_set_header Host $host;
99
+ }
100
+
101
+ location / {
102
+ set $upstream_portal portal;
103
+ proxy_pass http://$upstream_portal;
104
+ proxy_set_header Host $host;
105
+ }
106
+ }
@@ -1,37 +0,0 @@
1
- """Rendert compose.yaml aus Template + Release-Version + TLS-Modus."""
2
-
3
- from __future__ import annotations
4
-
5
- from pathlib import Path
6
-
7
- from jinja2 import Environment, PackageLoader, select_autoescape
8
-
9
- _env = Environment(
10
- loader=PackageLoader("katalon_cli", "templates"),
11
- autoescape=select_autoescape(disabled_extensions=(".yaml", ".j2")),
12
- trim_blocks=True,
13
- lstrip_blocks=True,
14
- )
15
-
16
-
17
- def render_compose(
18
- *,
19
- version: str,
20
- base_url: str,
21
- tls_mode: str,
22
- registry: str = "ghcr.io/karkraeg/katalon",
23
- ) -> str:
24
- template = _env.get_template("compose.yaml.j2")
25
- return template.render(
26
- version=version,
27
- base_url=base_url,
28
- tls_mode=tls_mode,
29
- registry=registry,
30
- )
31
-
32
-
33
- def write_compose(instance_dir: Path, **kwargs) -> Path:
34
- content = render_compose(**kwargs)
35
- path = instance_dir / "compose.yaml"
36
- path.write_text(content)
37
- return path
@@ -1,11 +0,0 @@
1
- {{ base_url_host }} {
2
- handle_path /api/* {
3
- reverse_proxy api:8000
4
- }
5
- handle_path /admin/* {
6
- reverse_proxy admin:80
7
- }
8
- handle {
9
- reverse_proxy portal:80
10
- }
11
- }
@@ -1,99 +0,0 @@
1
-
2
- services:
3
- db:
4
- image: postgis/postgis:16-3.4
5
- restart: unless-stopped
6
- environment:
7
- POSTGRES_USER: katalon
8
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
9
- POSTGRES_DB: katalon
10
- volumes:
11
- - db_data:/var/lib/postgresql/data
12
- healthcheck:
13
- test: ["CMD-SHELL", "pg_isready -U katalon"]
14
- interval: 5s
15
- timeout: 5s
16
- retries: 10
17
-
18
- redis:
19
- image: redis:7-alpine
20
- restart: unless-stopped
21
-
22
- elasticsearch:
23
- image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0
24
- restart: unless-stopped
25
- environment:
26
- discovery.type: single-node
27
- xpack.security.enabled: "false"
28
- ES_JAVA_OPTS: "-Xms512m -Xmx512m"
29
- volumes:
30
- - es_data:/usr/share/elasticsearch/data
31
-
32
- cantaloupe:
33
- image: {{ registry }}-cantaloupe:{{ version }}
34
- restart: unless-stopped
35
- volumes:
36
- - ./media:/media:ro
37
-
38
- api:
39
- image: {{ registry }}-api:{{ version }}
40
- restart: unless-stopped
41
- env_file: .env
42
- depends_on:
43
- db: { condition: service_healthy }
44
- volumes:
45
- - ./media:/var/lib/katalon/media
46
- healthcheck:
47
- test: ["CMD", "curl", "-sf", "http://localhost:8000/health"]
48
- interval: 10s
49
- timeout: 5s
50
- retries: 10
51
-
52
- admin:
53
- image: {{ registry }}-web:{{ version }}
54
- restart: unless-stopped
55
- environment:
56
- KATALON_WEB_MODE: admin
57
-
58
- portal:
59
- image: {{ registry }}-web:{{ version }}
60
- restart: unless-stopped
61
- environment:
62
- KATALON_WEB_MODE: portal
63
-
64
- {% if tls_mode == "standalone" %}
65
- proxy:
66
- image: caddy:2-alpine
67
- restart: unless-stopped
68
- ports:
69
- - "80:80"
70
- - "443:443"
71
- environment:
72
- KATALON_BASE_URL: {{ base_url }}
73
- volumes:
74
- - ./Caddyfile:/etc/caddy/Caddyfile:ro
75
- - caddy_data:/data
76
- depends_on:
77
- - api
78
- - admin
79
- - portal
80
- {% elif tls_mode == "behind-proxy" %}
81
- proxy:
82
- image: nginx:alpine
83
- restart: unless-stopped
84
- ports:
85
- - "127.0.0.1:8080:80"
86
- volumes:
87
- - ./nginx.conf:/etc/nginx/nginx.conf:ro
88
- depends_on:
89
- - api
90
- - admin
91
- - portal
92
- {% endif %}
93
-
94
- volumes:
95
- db_data:
96
- es_data:
97
- {% if tls_mode == "standalone" %}
98
- caddy_data:
99
- {% endif %}