Lars Talian commited on
Commit
83f2912
·
1 Parent(s): 307d729

Tighten manifest-grounded live admission

Browse files
src/open_range/builder/builder.py CHANGED
@@ -1039,6 +1039,9 @@ class TemplateOnlyBuilder:
1039
  ),
1040
  ]
1041
 
 
 
 
1042
  task = TaskSpec(
1043
  red_briefing=(
1044
  f"{topology['org_name']} is a {manifest.get('company', {}).get('industry', 'corporate')} "
 
1039
  ),
1040
  ]
1041
 
1042
+ company = manifest.get("company", {}) if isinstance(manifest.get("company"), dict) else {}
1043
+ company_name = str(company.get("name", "the company"))
1044
+ industry = str(company.get("industry", "corporate"))
1045
  task = TaskSpec(
1046
  red_briefing=(
1047
  f"{topology['org_name']} is a {manifest.get('company', {}).get('industry', 'corporate')} "
src/open_range/protocols.py CHANGED
@@ -267,13 +267,17 @@ class ContainerSet(BaseModel):
267
 
268
  cid = self.container_ids.get(container, container)
269
  proc = await asyncio.create_subprocess_exec(
270
- "docker", "inspect", "--format", "{{.State.Status}}", cid,
 
 
 
 
271
  stdout=asyncio.subprocess.PIPE,
272
  stderr=asyncio.subprocess.PIPE,
273
  )
274
  stdout, _ = await proc.communicate()
275
  status = (stdout or b"").decode().strip()
276
- return status == "running"
277
 
278
  async def cp(self, container: str, src: str, dest: str) -> None:
279
  """Copy a file into a container: ``docker cp src container:dest``."""
 
267
 
268
  cid = self.container_ids.get(container, container)
269
  proc = await asyncio.create_subprocess_exec(
270
+ "docker",
271
+ "inspect",
272
+ "--format",
273
+ "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}",
274
+ cid,
275
  stdout=asyncio.subprocess.PIPE,
276
  stderr=asyncio.subprocess.PIPE,
277
  )
278
  stdout, _ = await proc.communicate()
279
  status = (stdout or b"").decode().strip()
280
+ return status in {"running", "healthy"}
281
 
282
  async def cp(self, container: str, src: str, dest: str) -> None:
283
  """Copy a file into a container: ``docker cp src container:dest``."""
src/open_range/server/compose_runner.py CHANGED
@@ -2,6 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  import subprocess
6
  from dataclasses import dataclass
7
  from pathlib import Path
@@ -29,11 +30,15 @@ class ComposeProjectRunner:
29
  build_timeout_s: float = 300.0,
30
  up_timeout_s: float = 300.0,
31
  down_timeout_s: float = 120.0,
 
 
32
  remove_volumes: bool = True,
33
  ) -> None:
34
  self.build_timeout_s = build_timeout_s
35
  self.up_timeout_s = up_timeout_s
36
  self.down_timeout_s = down_timeout_s
 
 
37
  self.remove_volumes = remove_volumes
38
 
39
  def boot(
@@ -96,7 +101,7 @@ class ComposeProjectRunner:
96
  if container_id:
97
  container_ids[service] = container_id
98
 
99
- return BootedSnapshotProject(
100
  project_name=project_name,
101
  compose_file=compose_file,
102
  artifacts_dir=artifacts_dir,
@@ -105,6 +110,8 @@ class ComposeProjectRunner:
105
  container_ids=container_ids,
106
  ),
107
  )
 
 
108
 
109
  def teardown(self, project: BootedSnapshotProject) -> None:
110
  args = [
@@ -129,6 +136,32 @@ class ComposeProjectRunner:
129
  safe = "".join(ch.lower() if ch.isalnum() else "-" for ch in snapshot_id).strip("-")
130
  return f"openrange-{safe}"[:63]
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  @staticmethod
133
  def _run(
134
  args: list[str],
@@ -152,3 +185,9 @@ class ComposeProjectRunner:
152
  f"{' '.join(args)} failed with exit code {result.returncode}: {detail}"
153
  )
154
  return result
 
 
 
 
 
 
 
2
 
3
  from __future__ import annotations
4
 
5
+ import time
6
  import subprocess
7
  from dataclasses import dataclass
8
  from pathlib import Path
 
30
  build_timeout_s: float = 300.0,
31
  up_timeout_s: float = 300.0,
32
  down_timeout_s: float = 120.0,
33
+ health_timeout_s: float = 120.0,
34
+ health_poll_interval_s: float = 2.0,
35
  remove_volumes: bool = True,
36
  ) -> None:
37
  self.build_timeout_s = build_timeout_s
38
  self.up_timeout_s = up_timeout_s
39
  self.down_timeout_s = down_timeout_s
40
+ self.health_timeout_s = health_timeout_s
41
+ self.health_poll_interval_s = health_poll_interval_s
42
  self.remove_volumes = remove_volumes
43
 
44
  def boot(
 
101
  if container_id:
102
  container_ids[service] = container_id
103
 
104
+ project = BootedSnapshotProject(
105
  project_name=project_name,
106
  compose_file=compose_file,
107
  artifacts_dir=artifacts_dir,
 
110
  container_ids=container_ids,
111
  ),
112
  )
113
+ self._wait_until_healthy(project, services)
114
+ return project
115
 
116
  def teardown(self, project: BootedSnapshotProject) -> None:
117
  args = [
 
136
  safe = "".join(ch.lower() if ch.isalnum() else "-" for ch in snapshot_id).strip("-")
137
  return f"openrange-{safe}"[:63]
138
 
139
+ def _wait_until_healthy(
140
+ self,
141
+ project: BootedSnapshotProject,
142
+ services: list[str],
143
+ ) -> None:
144
+ deadline = time.monotonic() + self.health_timeout_s
145
+ pending = list(services)
146
+ while pending and time.monotonic() < deadline:
147
+ still_pending: list[str] = []
148
+ for service in pending:
149
+ try:
150
+ healthy = _run_async(project.containers.is_healthy(service))
151
+ except Exception:
152
+ healthy = False
153
+ if not healthy:
154
+ still_pending.append(service)
155
+ if not still_pending:
156
+ return
157
+ pending = still_pending
158
+ time.sleep(self.health_poll_interval_s)
159
+ if pending:
160
+ raise RuntimeError(
161
+ "Timed out waiting for healthy services: "
162
+ + ", ".join(pending)
163
+ )
164
+
165
  @staticmethod
166
  def _run(
167
  args: list[str],
 
185
  f"{' '.join(args)} failed with exit code {result.returncode}: {detail}"
186
  )
187
  return result
188
+
189
+
190
+ def _run_async(coro):
191
+ import asyncio
192
+
193
+ return asyncio.run(coro)