katalon-cli 0.1.2__tar.gz → 0.1.4__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.4}/PKG-INFO +8 -2
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/README.md +7 -1
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/pyproject.toml +1 -1
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/pyproject.toml.orig +1 -1
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/src/katalon_cli/core/backup.py +1 -1
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/src/katalon_cli/core/checks.py +18 -7
- katalon_cli-0.1.4/src/katalon_cli/core/compose_gen.py +85 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/src/katalon_cli/core/docker.py +34 -2
- katalon_cli-0.1.4/src/katalon_cli/main.py +505 -0
- katalon_cli-0.1.4/src/katalon_cli/templates/compose.yaml.j2 +175 -0
- katalon_cli-0.1.4/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/main.py +0 -300
- 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.4}/src/katalon_cli/__init__.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/src/katalon_cli/core/__init__.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/src/katalon_cli/core/paths.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/src/katalon_cli/core/release.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/src/katalon_cli/core/state.py +0 -0
- {katalon_cli-0.1.2 → katalon_cli-0.1.4}/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.4
|
|
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,
|
|
@@ -42,20 +42,31 @@ def check_disk_space(path: Path, min_gb: int = MIN_FREE_DISK_GB) -> CheckResult:
|
|
|
42
42
|
"Diskspace", ok, f"{free_gb:.1f} GB frei (min. {min_gb} GB) in {path}"
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
+
def check_openssl() -> CheckResult:
|
|
46
|
+
if not shutil.which("openssl"):
|
|
47
|
+
return CheckResult("OpenSSL", False, "nicht gefunden — für TLS-Modus 'standalone' erforderlich")
|
|
48
|
+
return CheckResult("OpenSSL", True, "gefunden")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_port_in_use(port: int) -> bool:
|
|
52
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
53
|
+
s.settimeout(0.5)
|
|
54
|
+
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
55
|
+
|
|
56
|
+
|
|
45
57
|
def check_ports(ports: list[int]) -> CheckResult:
|
|
46
|
-
busy = []
|
|
47
|
-
for port in ports:
|
|
48
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
49
|
-
s.settimeout(0.5)
|
|
50
|
-
if s.connect_ex(("127.0.0.1", port)) == 0:
|
|
51
|
-
busy.append(port)
|
|
58
|
+
busy = [port for port in ports if is_port_in_use(port)]
|
|
52
59
|
if busy:
|
|
53
60
|
return CheckResult("Ports", False, f"belegt: {', '.join(map(str, busy))}")
|
|
54
61
|
return CheckResult("Ports", True, f"frei: {', '.join(map(str, ports))}")
|
|
55
62
|
|
|
56
63
|
|
|
57
|
-
def run_all(
|
|
64
|
+
def run_all(
|
|
65
|
+
instance_dir: Path, ports: list[int] | None = None, need_openssl: bool = False
|
|
66
|
+
) -> list[CheckResult]:
|
|
58
67
|
results = [check_docker(), check_compose(), check_disk_space(instance_dir)]
|
|
59
68
|
if ports:
|
|
60
69
|
results.append(check_ports(ports))
|
|
70
|
+
if need_openssl:
|
|
71
|
+
results.append(check_openssl())
|
|
61
72
|
return results
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
from urllib.parse import urlparse
|
|
26
|
+
|
|
27
|
+
parsed_port = urlparse(base_url).port
|
|
28
|
+
template = _env.get_template("compose.yaml.j2")
|
|
29
|
+
return template.render(
|
|
30
|
+
version=version,
|
|
31
|
+
base_url=base_url,
|
|
32
|
+
tls_mode=tls_mode,
|
|
33
|
+
registry=registry,
|
|
34
|
+
port=parsed_port,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def render_nginx_conf(*, tls_mode: str) -> str:
|
|
38
|
+
template = _env.get_template("nginx.conf.j2")
|
|
39
|
+
return template.render(tls_mode=tls_mode)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def write_compose(instance_dir: Path, **kwargs) -> Path:
|
|
43
|
+
content = render_compose(**kwargs)
|
|
44
|
+
path = instance_dir / "compose.yaml"
|
|
45
|
+
path.write_text(content)
|
|
46
|
+
|
|
47
|
+
nginx_path = instance_dir / "nginx.conf"
|
|
48
|
+
nginx_path.write_text(render_nginx_conf(tls_mode=kwargs["tls_mode"]))
|
|
49
|
+
|
|
50
|
+
if kwargs["tls_mode"] == "standalone":
|
|
51
|
+
ensure_self_signed_cert(instance_dir, host=_host_of(kwargs["base_url"]))
|
|
52
|
+
|
|
53
|
+
return path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ensure_self_signed_cert(instance_dir: Path, *, host: str) -> None:
|
|
57
|
+
"""Legt ein selbstsigniertes Zertifikat an, falls keins vorhanden ist.
|
|
58
|
+
|
|
59
|
+
Für echtes TLS eigenes Zertifikat (z.B. Let's Encrypt) unter
|
|
60
|
+
<instance_dir>/certs/fullchain.pem + privkey.pem ablegen — wird nicht überschrieben.
|
|
61
|
+
"""
|
|
62
|
+
certs_dir = instance_dir / "certs"
|
|
63
|
+
fullchain = certs_dir / "fullchain.pem"
|
|
64
|
+
privkey = certs_dir / "privkey.pem"
|
|
65
|
+
if fullchain.exists() and privkey.exists():
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
certs_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
subprocess.run(
|
|
70
|
+
[
|
|
71
|
+
"openssl", "req", "-x509", "-nodes", "-newkey", "rsa:2048",
|
|
72
|
+
"-days", "825",
|
|
73
|
+
"-keyout", str(privkey),
|
|
74
|
+
"-out", str(fullchain),
|
|
75
|
+
"-subj", f"/CN={host}",
|
|
76
|
+
],
|
|
77
|
+
check=True,
|
|
78
|
+
capture_output=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _host_of(base_url: str) -> str:
|
|
83
|
+
from urllib.parse import urlparse
|
|
84
|
+
|
|
85
|
+
return urlparse(base_url).hostname or "localhost"
|
|
@@ -4,9 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import shutil
|
|
6
6
|
import subprocess
|
|
7
|
+
import time
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
|
-
|
|
10
10
|
class DockerError(RuntimeError):
|
|
11
11
|
pass
|
|
12
12
|
|
|
@@ -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,
|
|
@@ -42,3 +50,27 @@ def is_healthy(instance_dir: Path, service: str = "api") -> bool:
|
|
|
42
50
|
instance_dir, "ps", "--format", "json", service, check=False, capture=True
|
|
43
51
|
)
|
|
44
52
|
return result.returncode == 0 and '"Health":"healthy"' in (result.stdout or "")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_running(instance_dir: Path, service: str = "db") -> bool:
|
|
56
|
+
result = compose(
|
|
57
|
+
instance_dir, "ps", "--status", "running", "-q", service, check=False, capture=True
|
|
58
|
+
)
|
|
59
|
+
return result.returncode == 0 and bool(result.stdout.strip())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def wait_for_db(instance_dir: Path, timeout: int = 30) -> bool:
|
|
63
|
+
for _ in range(timeout):
|
|
64
|
+
result = compose(
|
|
65
|
+
instance_dir,
|
|
66
|
+
"exec",
|
|
67
|
+
"-T",
|
|
68
|
+
"db",
|
|
69
|
+
"pg_isready",
|
|
70
|
+
check=False,
|
|
71
|
+
capture=True,
|
|
72
|
+
)
|
|
73
|
+
if result.returncode == 0:
|
|
74
|
+
return True
|
|
75
|
+
time.sleep(1)
|
|
76
|
+
return False
|