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.
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/PKG-INFO +8 -2
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/README.md +7 -1
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/pyproject.toml +1 -1
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/pyproject.toml.orig +1 -1
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/src/katalon_cli/core/backup.py +1 -1
- katalon_cli-0.1.3/src/katalon_cli/core/compose_gen.py +82 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/src/katalon_cli/core/docker.py +9 -1
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/src/katalon_cli/main.py +88 -9
- katalon_cli-0.1.3/src/katalon_cli/templates/compose.yaml.j2 +165 -0
- katalon_cli-0.1.3/src/katalon_cli/templates/nginx.conf.j2 +106 -0
- katalon_cli-0.1.2/src/katalon_cli/core/compose_gen.py +0 -37
- katalon_cli-0.1.2/src/katalon_cli/templates/Caddyfile.j2 +0 -11
- katalon_cli-0.1.2/src/katalon_cli/templates/compose.yaml.j2 +0 -99
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/src/katalon_cli/__init__.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/src/katalon_cli/core/__init__.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/src/katalon_cli/core/checks.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/src/katalon_cli/core/paths.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/src/katalon_cli/core/release.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/src/katalon_cli/core/state.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.3}/src/katalon_cli/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: katalon-cli
|
|
3
|
-
Version: 0.1.
|
|
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`
|
|
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`
|
|
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`.
|
|
@@ -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(),
|
|
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(),
|
|
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 —
|
|
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
|
-
|
|
98
|
-
|
|
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,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 %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|