Repository navigation
Expand file tree
/
Copy pathdocker_compose.py
More file actions
132 lines (109 loc) · 4.42 KB
/
Copy pathdocker_compose.py
File metadata and controls
132 lines (109 loc) · 4.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env python
"""Run Docker Compose with port mappings rendered from JSON settings.
Docker Compose cannot read ``data/user/settings/system.json`` directly for
host port interpolation. This wrapper renders a tiny compose env file from the
JSON settings and then invokes ``docker compose --env-file``. It intentionally
does not read or migrate the project-root ``.env`` file.
"""
from __future__ import annotations
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
from typing import Any
PROJECT_ROOT = Path(__file__).resolve().parent.parent
SETTINGS_DIR = PROJECT_ROOT / "data" / "user" / "settings"
DOCKER_ENV_PATH = SETTINGS_DIR / "docker.env"
DEFAULT_BACKEND_PORT = 8001
DEFAULT_FRONTEND_PORT = 3782
DEFAULT_POCKETBASE_PORT = 8090
DEFAULT_WORKSPACE_HOST = Path("data/user/workspace")
def _read_json_object(path: Path) -> dict[str, Any]:
try:
loaded = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
return loaded if isinstance(loaded, dict) else {}
def _coerce_port(value: Any, default: int) -> int:
try:
port = int(str(value).strip())
except (TypeError, ValueError):
return default
return port if 1 <= port <= 65535 else default
def render_docker_env(
settings_dir: Path = SETTINGS_DIR,
output_path: Path = DOCKER_ENV_PATH,
) -> dict[str, str]:
"""Render compose interpolation vars from JSON settings only."""
system = _read_json_object(settings_dir / "system.json")
integrations = _read_json_object(settings_dir / "integrations.json")
values = {
"DEEPTUTOR_DOCKER_BACKEND_PORT": str(
_coerce_port(system.get("backend_port"), DEFAULT_BACKEND_PORT)
),
"DEEPTUTOR_DOCKER_FRONTEND_PORT": str(
_coerce_port(system.get("frontend_port"), DEFAULT_FRONTEND_PORT)
),
"DEEPTUTOR_DOCKER_POCKETBASE_PORT": str(
_coerce_port(integrations.get("pocketbase_port"), DEFAULT_POCKETBASE_PORT)
),
}
output_path.parent.mkdir(parents=True, exist_ok=True)
lines = [
"# Auto-generated by scripts/docker_compose.py from data/user/settings/*.json.",
"# Do not edit manually; update system.json/integrations.json instead.",
]
lines.extend(f"{key}={value}" for key, value in values.items())
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return values
def _compose_command(args: list[str]) -> list[str]:
docker = shutil.which("docker")
if not docker:
raise SystemExit("docker was not found on PATH")
return [docker, "compose", "--env-file", str(DOCKER_ENV_PATH), *args]
def ensure_workspace_host(raw_path: str | None = None) -> Path:
"""Create the host workspace and nested writable outputs bind.
Compose can create a missing bind source as root, which leaves the
unprivileged app/runner unable to write it. Preparing both directories as
the invoking user also makes the effective path explicit before startup.
"""
raw = str(raw_path or os.environ.get("DEEPTUTOR_WORKSPACE_HOST") or "").strip()
candidate = Path(raw).expanduser() if raw else DEFAULT_WORKSPACE_HOST
if not candidate.is_absolute():
candidate = PROJECT_ROOT / candidate
candidate = candidate.resolve()
candidate.mkdir(parents=True, exist_ok=True)
(candidate / "outputs").mkdir(parents=True, exist_ok=True)
return candidate
def main(argv: list[str] | None = None) -> int:
args = list(argv if argv is not None else sys.argv[1:])
if not args:
args = ["up", "-d"]
values = render_docker_env()
workspace_host = ensure_workspace_host()
print(
"Docker settings: "
f"backend={values['DEEPTUTOR_DOCKER_BACKEND_PORT']} "
f"frontend={values['DEEPTUTOR_DOCKER_FRONTEND_PORT']} "
f"pocketbase={values['DEEPTUTOR_DOCKER_POCKETBASE_PORT']} "
f"workspace={workspace_host}",
file=sys.stderr,
)
env = os.environ.copy()
# Keep Docker execution detached from host process overrides.
for key in (
"BACKEND_PORT",
"FRONTEND_PORT",
"POCKETBASE_PORT",
"AUTH_ENABLED",
"POCKETBASE_URL",
"NEXT_PUBLIC_API_BASE",
"NEXT_PUBLIC_API_BASE_EXTERNAL",
):
env.pop(key, None)
result = subprocess.run(_compose_command(args), cwd=str(PROJECT_ROOT), env=env, check=False)
return int(result.returncode)
if __name__ == "__main__":
raise SystemExit(main())