@le-space/rootfs 0.1.51 → 0.2.0
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.
- package/README.md +5 -1
- package/index.d.ts +6 -1
- package/index.js +71 -31
- package/package.json +1 -1
- package/reference/orbitdb-relay-pinner/contract.json +70 -0
- package/reference/orbitdb-relay-pinner/rootfs/Dockerfile.rootfs +19 -0
- package/reference/orbitdb-relay-pinner/rootfs/build-rootfs-image.sh +335 -0
- package/reference/orbitdb-relay-pinner/rootfs/build-rootfs.sh +521 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-autotls-refresh.py +224 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-autotls-refresh.service +24 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-bootstrap.conf +2 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-bootstrap.service +21 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-bootstrap.sh +121 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-configure.sh +240 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-describe.py +165 -0
- package/reference/orbitdb-relay-pinner/rootfs/orbitdb-relay-pinner-setup-server.py +237 -0
- package/reference/orbitdb-relay-pinner/rootfs/read-rootfs-contract.py +72 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/orbitdb-relay-pinner")
|
|
10
|
+
METRICS_PORT = int(os.environ.get("METRICS_PORT", "9090"))
|
|
11
|
+
WAIT_TIMEOUT_SECONDS = int(os.environ.get("DESCRIBE_WAIT_TIMEOUT_SECONDS", "240"))
|
|
12
|
+
WAIT_INTERVAL_SECONDS = float(os.environ.get("DESCRIBE_WAIT_INTERVAL_SECONDS", "2"))
|
|
13
|
+
AUTOTLS_EXTRA_WAIT_SECONDS = int(os.environ.get("DESCRIBE_AUTOTLS_EXTRA_WAIT_SECONDS", "120"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_env_file(path: str) -> dict[str, str]:
|
|
17
|
+
values: dict[str, str] = {}
|
|
18
|
+
if not os.path.exists(path):
|
|
19
|
+
return values
|
|
20
|
+
|
|
21
|
+
with open(path, encoding="utf-8") as handle:
|
|
22
|
+
for line in handle:
|
|
23
|
+
stripped = line.strip()
|
|
24
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
25
|
+
continue
|
|
26
|
+
key, value = stripped.split("=", 1)
|
|
27
|
+
values[key.strip()] = value.strip()
|
|
28
|
+
return values
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def fetch_json(path: str) -> dict:
|
|
32
|
+
url = f"http://127.0.0.1:{METRICS_PORT}{path}"
|
|
33
|
+
with urllib.request.urlopen(url, timeout=5) as response:
|
|
34
|
+
return json.loads(response.read().decode("utf-8"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dedupe(values: list[str]) -> list[str]:
|
|
38
|
+
seen: set[str] = set()
|
|
39
|
+
result: list[str] = []
|
|
40
|
+
for value in values:
|
|
41
|
+
if value and value not in seen:
|
|
42
|
+
seen.add(value)
|
|
43
|
+
result.append(value)
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def normalize_multiaddrs(payload: dict) -> list[str]:
|
|
48
|
+
values = payload.get("all")
|
|
49
|
+
if not isinstance(values, list):
|
|
50
|
+
return []
|
|
51
|
+
return [entry for entry in values if isinstance(entry, str) and entry.strip()]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_grouped_multiaddrs(env_values: dict[str, str], all_multiaddrs: list[str], peer_id: str) -> dict[str, list[str]]:
|
|
55
|
+
proxy_hostname = env_values.get("PROXY_HOSTNAME", "").strip().lower()
|
|
56
|
+
direct_tcp_multiaddrs = [addr for addr in all_multiaddrs if "/tcp/" in addr and "/ws" not in addr]
|
|
57
|
+
autotls_wss_multiaddrs = [
|
|
58
|
+
addr
|
|
59
|
+
for addr in all_multiaddrs
|
|
60
|
+
if addr.endswith("/ws/p2p/" + peer_id) and "/tls/ws" in addr and proxy_hostname not in addr.lower()
|
|
61
|
+
]
|
|
62
|
+
plain_ws_multiaddrs = [
|
|
63
|
+
addr for addr in all_multiaddrs if addr.endswith("/ws/p2p/" + peer_id) and "/tls/ws" not in addr
|
|
64
|
+
]
|
|
65
|
+
proxy_wss_multiaddrs: list[str] = []
|
|
66
|
+
if proxy_hostname:
|
|
67
|
+
proxy_wss_multiaddrs = [
|
|
68
|
+
f"/dns4/{proxy_hostname}/tcp/443/tls/ws/p2p/{peer_id}",
|
|
69
|
+
f"/dns6/{proxy_hostname}/tcp/443/tls/ws/p2p/{peer_id}",
|
|
70
|
+
]
|
|
71
|
+
quic_multiaddrs = [addr for addr in all_multiaddrs if "/quic-v1" in addr and "/webtransport" not in addr]
|
|
72
|
+
webtransport_multiaddrs = [addr for addr in all_multiaddrs if "/webtransport" in addr]
|
|
73
|
+
webrtc_direct_multiaddrs = [addr for addr in all_multiaddrs if "/webrtc-direct" in addr]
|
|
74
|
+
|
|
75
|
+
browser_bootstrap_multiaddrs = dedupe(
|
|
76
|
+
proxy_wss_multiaddrs
|
|
77
|
+
+ autotls_wss_multiaddrs
|
|
78
|
+
+ plain_ws_multiaddrs
|
|
79
|
+
+ webtransport_multiaddrs
|
|
80
|
+
+ webrtc_direct_multiaddrs
|
|
81
|
+
)
|
|
82
|
+
probe_multiaddrs = dedupe(
|
|
83
|
+
direct_tcp_multiaddrs
|
|
84
|
+
+ proxy_wss_multiaddrs
|
|
85
|
+
+ autotls_wss_multiaddrs
|
|
86
|
+
+ quic_multiaddrs
|
|
87
|
+
+ webtransport_multiaddrs
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"direct_tcp_multiaddrs": dedupe(direct_tcp_multiaddrs),
|
|
92
|
+
"autotls_wss_multiaddrs": dedupe(autotls_wss_multiaddrs),
|
|
93
|
+
"proxy_wss_multiaddrs": dedupe(proxy_wss_multiaddrs),
|
|
94
|
+
"plain_ws_multiaddrs": dedupe(plain_ws_multiaddrs),
|
|
95
|
+
"quic_multiaddrs": dedupe(quic_multiaddrs),
|
|
96
|
+
"webtransport_multiaddrs": dedupe(webtransport_multiaddrs),
|
|
97
|
+
"webrtc_direct_multiaddrs": dedupe(webrtc_direct_multiaddrs),
|
|
98
|
+
"browser_bootstrap_multiaddrs": browser_bootstrap_multiaddrs,
|
|
99
|
+
"probe_multiaddrs": probe_multiaddrs,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main() -> None:
|
|
104
|
+
started_at = time.monotonic()
|
|
105
|
+
deadline = started_at + WAIT_TIMEOUT_SECONDS
|
|
106
|
+
health: dict = {}
|
|
107
|
+
multiaddrs_payload: dict = {}
|
|
108
|
+
grouped = {
|
|
109
|
+
"direct_tcp_multiaddrs": [],
|
|
110
|
+
"autotls_wss_multiaddrs": [],
|
|
111
|
+
"proxy_wss_multiaddrs": [],
|
|
112
|
+
"plain_ws_multiaddrs": [],
|
|
113
|
+
"quic_multiaddrs": [],
|
|
114
|
+
"webtransport_multiaddrs": [],
|
|
115
|
+
"webrtc_direct_multiaddrs": [],
|
|
116
|
+
"browser_bootstrap_multiaddrs": [],
|
|
117
|
+
"probe_multiaddrs": [],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
while time.monotonic() < deadline:
|
|
121
|
+
try:
|
|
122
|
+
env_values = parse_env_file(ENV_FILE)
|
|
123
|
+
health = fetch_json("/health")
|
|
124
|
+
multiaddrs_payload = fetch_json("/multiaddrs")
|
|
125
|
+
peer_id = health.get("peerId") or multiaddrs_payload.get("peerId")
|
|
126
|
+
if not isinstance(peer_id, str) or not peer_id.strip():
|
|
127
|
+
time.sleep(WAIT_INTERVAL_SECONDS)
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
all_multiaddrs = normalize_multiaddrs(multiaddrs_payload)
|
|
131
|
+
grouped = build_grouped_multiaddrs(env_values, all_multiaddrs, peer_id)
|
|
132
|
+
proxy_hostname = env_values.get("PROXY_HOSTNAME", "").strip()
|
|
133
|
+
if grouped["autotls_wss_multiaddrs"]:
|
|
134
|
+
break
|
|
135
|
+
if proxy_hostname and grouped["proxy_wss_multiaddrs"] and time.monotonic() - started_at >= AUTOTLS_EXTRA_WAIT_SECONDS:
|
|
136
|
+
break
|
|
137
|
+
if not proxy_hostname and time.monotonic() - started_at >= AUTOTLS_EXTRA_WAIT_SECONDS:
|
|
138
|
+
break
|
|
139
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError):
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
time.sleep(WAIT_INTERVAL_SECONDS)
|
|
143
|
+
|
|
144
|
+
peer_id = health.get("peerId") or multiaddrs_payload.get("peerId")
|
|
145
|
+
if not isinstance(peer_id, str) or not peer_id.strip():
|
|
146
|
+
raise SystemExit("unable to discover orbitdb relay peer ID from metrics endpoints")
|
|
147
|
+
|
|
148
|
+
env_values = parse_env_file(ENV_FILE)
|
|
149
|
+
payload = {
|
|
150
|
+
"peer_id": peer_id,
|
|
151
|
+
"announce_addrs": [
|
|
152
|
+
entry.strip()
|
|
153
|
+
for entry in env_values.get("VITE_APPEND_ANNOUNCE", "").split(",")
|
|
154
|
+
if entry.strip()
|
|
155
|
+
],
|
|
156
|
+
"listening_addrs": normalize_multiaddrs(multiaddrs_payload),
|
|
157
|
+
"auto_tls_serving_zone": health.get("autoTlsServingZone"),
|
|
158
|
+
"metrics_https": health.get("metricsHttps"),
|
|
159
|
+
**grouped,
|
|
160
|
+
}
|
|
161
|
+
print(json.dumps(payload))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import ipaddress
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
9
|
+
from urllib.parse import urlsplit
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/orbitdb-relay-pinner")
|
|
13
|
+
READY_FILE = os.environ.get("READY_FILE", "/etc/default/orbitdb-relay-pinner.ready")
|
|
14
|
+
SERVICE_NAME = os.environ.get("SERVICE_NAME", "orbitdb-relay-pinner.service")
|
|
15
|
+
BOOTSTRAP_SERVICE = os.environ.get("BOOTSTRAP_SERVICE", "orbitdb-relay-pinner-bootstrap.service")
|
|
16
|
+
CONFIGURE_SCRIPT = "/usr/local/sbin/orbitdb-relay-pinner-configure.sh"
|
|
17
|
+
DESCRIBE_SCRIPT = "/usr/local/sbin/orbitdb-relay-pinner-describe.py"
|
|
18
|
+
METADATA_FILE = os.environ.get("METADATA_FILE", "/run/orbitdb-relay-pinner-setup-metadata.json")
|
|
19
|
+
METADATA_ERROR_FILE = os.environ.get("METADATA_ERROR_FILE", "/run/orbitdb-relay-pinner-setup-metadata.error")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _cors_headers(handler: BaseHTTPRequestHandler) -> None:
|
|
23
|
+
handler.send_header("Access-Control-Allow-Origin", "*")
|
|
24
|
+
handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
25
|
+
handler.send_header("Access-Control-Allow-Headers", "content-type")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _validate_port(value: object, field_name: str) -> str:
|
|
29
|
+
if not isinstance(value, int) or value < 1 or value > 65535:
|
|
30
|
+
raise ValueError(f"{field_name} must be an integer TCP/UDP port between 1 and 65535")
|
|
31
|
+
return str(value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _validate_proxy_hostname(value: object) -> str | None:
|
|
35
|
+
if value is None:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
if not isinstance(value, str):
|
|
39
|
+
raise ValueError("proxy_url must be a string when provided")
|
|
40
|
+
|
|
41
|
+
candidate = value.strip()
|
|
42
|
+
if not candidate:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
parsed = urlsplit(candidate if "://" in candidate else f"https://{candidate}")
|
|
46
|
+
if not parsed.hostname:
|
|
47
|
+
raise ValueError("proxy_url must include a valid hostname")
|
|
48
|
+
|
|
49
|
+
return parsed.hostname
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Handler(BaseHTTPRequestHandler):
|
|
53
|
+
server_version = "OrbitdbRelaySetup/1.0"
|
|
54
|
+
|
|
55
|
+
def _request_path(self) -> str:
|
|
56
|
+
return urlsplit(self.path).path
|
|
57
|
+
|
|
58
|
+
def _send_json(self, status: int, payload: dict) -> None:
|
|
59
|
+
body = json.dumps(payload).encode("utf-8")
|
|
60
|
+
self.send_response(status)
|
|
61
|
+
_cors_headers(self)
|
|
62
|
+
self.send_header("Content-Type", "application/json")
|
|
63
|
+
self.send_header("Content-Length", str(len(body)))
|
|
64
|
+
self.end_headers()
|
|
65
|
+
self.wfile.write(body)
|
|
66
|
+
|
|
67
|
+
def log_message(self, format: str, *args) -> None: # noqa: A003
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
def do_OPTIONS(self) -> None: # noqa: N802
|
|
71
|
+
self.send_response(204)
|
|
72
|
+
_cors_headers(self)
|
|
73
|
+
self.end_headers()
|
|
74
|
+
|
|
75
|
+
def do_GET(self) -> None: # noqa: N802
|
|
76
|
+
if self._request_path() == "/metadata":
|
|
77
|
+
self._handle_metadata()
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if self._request_path() not in ("/", "/health"):
|
|
81
|
+
self._send_json(404, {"status": "not-found"})
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
self._send_json(
|
|
85
|
+
200,
|
|
86
|
+
{
|
|
87
|
+
"status": "waiting-for-port-mapping",
|
|
88
|
+
"ready": os.path.exists(READY_FILE),
|
|
89
|
+
"env_file": ENV_FILE,
|
|
90
|
+
"metadata_ready": os.path.exists(METADATA_FILE),
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _handle_metadata(self) -> None:
|
|
95
|
+
if os.path.exists(METADATA_FILE):
|
|
96
|
+
with open(METADATA_FILE, encoding="utf-8") as handle:
|
|
97
|
+
metadata = json.load(handle)
|
|
98
|
+
self._send_json(200, {"status": "ready", "metadata": metadata})
|
|
99
|
+
threading.Thread(target=self.server.shutdown, daemon=True).start() # type: ignore[arg-type]
|
|
100
|
+
threading.Thread(target=_stop_bootstrap_service, daemon=True).start()
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if os.path.exists(METADATA_ERROR_FILE):
|
|
104
|
+
with open(METADATA_ERROR_FILE, encoding="utf-8") as handle:
|
|
105
|
+
error_message = handle.read().strip() or "metadata generation failed"
|
|
106
|
+
self._send_json(500, {"status": "error", "error": error_message})
|
|
107
|
+
threading.Thread(target=self.server.shutdown, daemon=True).start() # type: ignore[arg-type]
|
|
108
|
+
threading.Thread(target=_stop_bootstrap_service, daemon=True).start()
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
self._send_json(202, {"status": "pending"})
|
|
112
|
+
|
|
113
|
+
def do_POST(self) -> None: # noqa: N802
|
|
114
|
+
if self._request_path() != "/configure":
|
|
115
|
+
self._send_json(404, {"status": "not-found"})
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
content_length = int(self.headers.get("Content-Length", "0"))
|
|
120
|
+
except ValueError:
|
|
121
|
+
self._send_json(400, {"status": "bad-request", "error": "Invalid Content-Length"})
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
payload = json.loads(self.rfile.read(content_length).decode("utf-8") or "{}")
|
|
126
|
+
except json.JSONDecodeError as error:
|
|
127
|
+
self._send_json(400, {"status": "bad-request", "error": f"Invalid JSON body: {error}"})
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
public_ipv4 = str(ipaddress.ip_address(payload.get("public_ipv4")))
|
|
132
|
+
public_ipv6 = payload.get("public_ipv6")
|
|
133
|
+
if public_ipv6 is not None:
|
|
134
|
+
public_ipv6 = str(ipaddress.ip_address(public_ipv6))
|
|
135
|
+
tcp_port = _validate_port(payload.get("tcp_port"), "tcp_port")
|
|
136
|
+
ws_port = _validate_port(payload.get("ws_port"), "ws_port")
|
|
137
|
+
proxy_hostname = _validate_proxy_hostname(payload.get("proxy_url"))
|
|
138
|
+
metrics_port = payload.get("metrics_port")
|
|
139
|
+
metrics_https_port = payload.get("metrics_https_port")
|
|
140
|
+
webrtc_port = payload.get("webrtc_port")
|
|
141
|
+
quic_port = payload.get("quic_port")
|
|
142
|
+
args = [
|
|
143
|
+
CONFIGURE_SCRIPT,
|
|
144
|
+
"--public-ipv4",
|
|
145
|
+
public_ipv4,
|
|
146
|
+
"--tcp-port",
|
|
147
|
+
tcp_port,
|
|
148
|
+
"--ws-port",
|
|
149
|
+
ws_port,
|
|
150
|
+
]
|
|
151
|
+
if proxy_hostname is not None:
|
|
152
|
+
args.extend(["--proxy-hostname", proxy_hostname])
|
|
153
|
+
if public_ipv6 is not None:
|
|
154
|
+
args.extend(["--public-ipv6", public_ipv6])
|
|
155
|
+
if metrics_port is not None:
|
|
156
|
+
args.extend(["--metrics-port", _validate_port(metrics_port, "metrics_port")])
|
|
157
|
+
if metrics_https_port is not None:
|
|
158
|
+
args.extend(
|
|
159
|
+
[
|
|
160
|
+
"--metrics-https-port",
|
|
161
|
+
_validate_port(metrics_https_port, "metrics_https_port"),
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
if webrtc_port is not None:
|
|
165
|
+
args.extend(["--webrtc-port", _validate_port(webrtc_port, "webrtc_port")])
|
|
166
|
+
if quic_port is not None:
|
|
167
|
+
args.extend(["--quic-port", _validate_port(quic_port, "quic_port")])
|
|
168
|
+
except ValueError as error:
|
|
169
|
+
self._send_json(400, {"status": "bad-request", "error": str(error)})
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
result = subprocess.run(
|
|
174
|
+
args,
|
|
175
|
+
check=True,
|
|
176
|
+
capture_output=True,
|
|
177
|
+
text=True,
|
|
178
|
+
)
|
|
179
|
+
except subprocess.CalledProcessError as error:
|
|
180
|
+
self._send_json(
|
|
181
|
+
500,
|
|
182
|
+
{
|
|
183
|
+
"status": "error",
|
|
184
|
+
"error": error.stderr.strip() or error.stdout.strip() or str(error),
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
_clear_metadata_state()
|
|
190
|
+
threading.Thread(target=_generate_metadata_files, daemon=True).start()
|
|
191
|
+
|
|
192
|
+
self._send_json(
|
|
193
|
+
200,
|
|
194
|
+
{
|
|
195
|
+
"status": "configured",
|
|
196
|
+
"stdout": result.stdout.strip(),
|
|
197
|
+
"metadata_pending": True,
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _stop_bootstrap_service() -> None:
|
|
203
|
+
time.sleep(1)
|
|
204
|
+
subprocess.run(["systemctl", "stop", BOOTSTRAP_SERVICE], check=False)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _clear_metadata_state() -> None:
|
|
208
|
+
for path in (METADATA_FILE, METADATA_ERROR_FILE):
|
|
209
|
+
try:
|
|
210
|
+
os.remove(path)
|
|
211
|
+
except FileNotFoundError:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _generate_metadata_files() -> None:
|
|
216
|
+
try:
|
|
217
|
+
describe = subprocess.run(
|
|
218
|
+
[DESCRIBE_SCRIPT],
|
|
219
|
+
check=True,
|
|
220
|
+
capture_output=True,
|
|
221
|
+
text=True,
|
|
222
|
+
)
|
|
223
|
+
payload = json.loads(describe.stdout.strip() or "{}")
|
|
224
|
+
with open(METADATA_FILE, "w", encoding="utf-8") as handle:
|
|
225
|
+
json.dump(payload, handle)
|
|
226
|
+
except (subprocess.CalledProcessError, json.JSONDecodeError) as error:
|
|
227
|
+
with open(METADATA_ERROR_FILE, "w", encoding="utf-8") as handle:
|
|
228
|
+
handle.write(str(error))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def main() -> None:
|
|
232
|
+
server = ThreadingHTTPServer(("0.0.0.0", 80), Handler)
|
|
233
|
+
server.serve_forever()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
main()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import shlex
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def shell_assign(name: str, value: str) -> str:
|
|
13
|
+
return f"{name}={shlex.quote(value)}"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> int:
|
|
17
|
+
parser = argparse.ArgumentParser(description="Read a relay rootfs contract and emit shell exports.")
|
|
18
|
+
parser.add_argument("contract", type=Path, help="Path to the rootfs contract JSON file")
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
payload = json.loads(args.contract.read_text())
|
|
23
|
+
except FileNotFoundError:
|
|
24
|
+
print(f"Missing rootfs contract: {args.contract}", file=sys.stderr)
|
|
25
|
+
return 1
|
|
26
|
+
except json.JSONDecodeError as exc:
|
|
27
|
+
print(f"Invalid rootfs contract JSON in {args.contract}: {exc}", file=sys.stderr)
|
|
28
|
+
return 1
|
|
29
|
+
|
|
30
|
+
rootfs = payload.get("rootfs") or {}
|
|
31
|
+
services = payload.get("services") or {}
|
|
32
|
+
source = payload.get("source") or {}
|
|
33
|
+
manifest = payload.get("manifest") or {}
|
|
34
|
+
ports = payload.get("ports") or []
|
|
35
|
+
|
|
36
|
+
profile = rootfs.get("profile")
|
|
37
|
+
install_mode = rootfs.get("installMode")
|
|
38
|
+
|
|
39
|
+
if not isinstance(profile, str) or not profile.strip():
|
|
40
|
+
print("Rootfs contract is missing rootfs.profile", file=sys.stderr)
|
|
41
|
+
return 1
|
|
42
|
+
if not isinstance(install_mode, str) or not install_mode.strip():
|
|
43
|
+
print("Rootfs contract is missing rootfs.installMode", file=sys.stderr)
|
|
44
|
+
return 1
|
|
45
|
+
if not isinstance(ports, list):
|
|
46
|
+
print("Rootfs contract field ports must be a list", file=sys.stderr)
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
lines = [
|
|
50
|
+
shell_assign("ROOTFS_CONTRACT_PATH", str(args.contract.resolve())),
|
|
51
|
+
shell_assign("ROOTFS_CONTRACT_ID", str(payload.get("id", ""))),
|
|
52
|
+
shell_assign("ROOTFS_CONTRACT_PROFILE", profile.strip()),
|
|
53
|
+
shell_assign("ROOTFS_CONTRACT_INSTALL_MODE", install_mode.strip()),
|
|
54
|
+
shell_assign("ROOTFS_CONTRACT_SOURCE_SUBDIRECTORY", str(source.get("subdirectory", ""))),
|
|
55
|
+
shell_assign("ROOTFS_CONTRACT_INSTALL_DIR", str(rootfs.get("installDir", ""))),
|
|
56
|
+
shell_assign("ROOTFS_CONTRACT_BINARY_PATH", str(rootfs.get("binaryPath", "/usr/local/bin/universal-chat-go"))),
|
|
57
|
+
shell_assign("ROOTFS_CONTRACT_DATA_DIR", str(rootfs.get("dataDir", ""))),
|
|
58
|
+
shell_assign("ROOTFS_CONTRACT_ENV_FILE", str(rootfs.get("envFile", ""))),
|
|
59
|
+
shell_assign("ROOTFS_CONTRACT_MAIN_SERVICE", str(services.get("main", ""))),
|
|
60
|
+
shell_assign("ROOTFS_CONTRACT_BOOTSTRAP_SERVICE", str(services.get("bootstrap", ""))),
|
|
61
|
+
shell_assign("ROOTFS_CONTRACT_AUTOTLS_SERVICE", str(services.get("autotlsRefresh", ""))),
|
|
62
|
+
shell_assign("ROOTFS_CONTRACT_MANIFEST_COPY_TARGET", str(manifest.get("copyTarget", ""))),
|
|
63
|
+
shell_assign("ROOTFS_CONTRACT_MANIFEST_NOTES", str(manifest.get("notes", ""))),
|
|
64
|
+
shell_assign("ROOTFS_CONTRACT_PORT_FORWARDS_JSON", json.dumps(ports, separators=(",", ":"))),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
print("\n".join(lines))
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
raise SystemExit(main())
|