Exercise packaged runtime topology
This commit is contained in:
@@ -157,6 +157,9 @@ migrations against the pinned PostgreSQL image, reach API and WebUI readiness
|
|||||||
as non-root/read-only processes, and complete a task through the pinned Redis
|
as non-root/read-only processes, and complete a task through the pinned Redis
|
||||||
image and packaged worker. Sanitized per-platform smoke receipts are retained
|
image and packaged worker. Sanitized per-platform smoke receipts are retained
|
||||||
as immutable release assets.
|
as immutable release assets.
|
||||||
|
The smoke also proves a bounded post-migration table contract and aborts as
|
||||||
|
soon as a required container exits, rather than allowing a dead process to
|
||||||
|
consume the full readiness timeout.
|
||||||
|
|
||||||
Ingress acceptance streams generated configuration into Docker-managed
|
Ingress acceptance streams generated configuration into Docker-managed
|
||||||
volumes before starting the read-only containers. It therefore also works when
|
volumes before starting the read-only containers. It therefore also works when
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
@@ -52,6 +54,28 @@ class RuntimeImageSmokeTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(MODULE.SmokeError, "exact sha256"):
|
with self.assertRaisesRegex(MODULE.SmokeError, "exact sha256"):
|
||||||
MODULE.platform_image(path, "linux/arm64", "API")
|
MODULE.platform_image(path, "linux/arm64", "API")
|
||||||
|
|
||||||
|
def test_readiness_fails_immediately_when_container_exits(self) -> None:
|
||||||
|
exited = subprocess.CompletedProcess([], 0, "false\n", "")
|
||||||
|
logs = subprocess.CompletedProcess([], 0, "fatal startup error\n", "")
|
||||||
|
with patch.object(MODULE, "_run", side_effect=(exited, logs)):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
MODULE.SmokeError,
|
||||||
|
"container exited before readiness: fatal startup error",
|
||||||
|
):
|
||||||
|
MODULE._wait_for(
|
||||||
|
"WebUI",
|
||||||
|
lambda: self.fail("probe must not run for an exited container"),
|
||||||
|
timeout=60,
|
||||||
|
container="web",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_smoke_supplies_the_packaged_web_upstream_and_schema_contract(self) -> None:
|
||||||
|
source = SCRIPT.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn('"--network-alias",\n "load-balancer"', source)
|
||||||
|
self.assertIn("'core_system_settings'", source)
|
||||||
|
self.assertIn("'core_runtime_nodes'", source)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -104,10 +104,28 @@ def _wait_for(
|
|||||||
probe: Callable[[], subprocess.CompletedProcess[str]],
|
probe: Callable[[], subprocess.CompletedProcess[str]],
|
||||||
*,
|
*,
|
||||||
timeout: float,
|
timeout: float,
|
||||||
|
container: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
deadline = time.monotonic() + timeout
|
deadline = time.monotonic() + timeout
|
||||||
last = ""
|
last = ""
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
|
if container is not None:
|
||||||
|
state = _run(
|
||||||
|
("docker", "inspect", "--format", "{{.State.Running}}", container),
|
||||||
|
check=False,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
if state.returncode != 0 or state.stdout.strip() != "true":
|
||||||
|
logs = _run(
|
||||||
|
("docker", "logs", "--tail", "100", container),
|
||||||
|
check=False,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
detail = _tail(logs.stdout + logs.stderr, limit=8000)
|
||||||
|
raise SmokeError(
|
||||||
|
f"{label} container exited before readiness: "
|
||||||
|
f"{detail or 'no diagnostic output'}"
|
||||||
|
)
|
||||||
result = probe()
|
result = probe()
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
return
|
return
|
||||||
@@ -212,13 +230,15 @@ def run_smoke(
|
|||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
|
|
||||||
def record(check_id: str, began: float) -> None:
|
def record(check_id: str, began: float) -> None:
|
||||||
|
duration = round(time.monotonic() - began, 3)
|
||||||
checks.append(
|
checks.append(
|
||||||
{
|
{
|
||||||
"id": check_id,
|
"id": check_id,
|
||||||
"state": "passed",
|
"state": "passed",
|
||||||
"duration_seconds": round(time.monotonic() - began, 3),
|
"duration_seconds": duration,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
print(f"PASS {platform} {check_id} ({duration}s)", flush=True)
|
||||||
|
|
||||||
common_runtime = [
|
common_runtime = [
|
||||||
"--platform",
|
"--platform",
|
||||||
@@ -309,6 +329,7 @@ def run_smoke(
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
),
|
),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
container=names["postgres"],
|
||||||
)
|
)
|
||||||
_wait_for(
|
_wait_for(
|
||||||
"Redis",
|
"Redis",
|
||||||
@@ -318,6 +339,7 @@ def run_smoke(
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
),
|
),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
container=names["redis"],
|
||||||
)
|
)
|
||||||
record("managed_dependencies_ready", began)
|
record("managed_dependencies_ready", began)
|
||||||
|
|
||||||
@@ -347,6 +369,40 @@ def run_smoke(
|
|||||||
)
|
)
|
||||||
record("release_migrations", began)
|
record("release_migrations", began)
|
||||||
|
|
||||||
|
began = time.monotonic()
|
||||||
|
_run(
|
||||||
|
(
|
||||||
|
"docker",
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"--name",
|
||||||
|
f"{prefix}-schema",
|
||||||
|
*common_runtime,
|
||||||
|
*_env_arguments(
|
||||||
|
environment,
|
||||||
|
role="migration",
|
||||||
|
node_id=f"runtime-smoke-{slug}-schema",
|
||||||
|
),
|
||||||
|
api_image,
|
||||||
|
"python",
|
||||||
|
"-c",
|
||||||
|
(
|
||||||
|
"import os;"
|
||||||
|
"from sqlalchemy import create_engine,inspect;"
|
||||||
|
"engine=create_engine(os.environ['DATABASE_URL']);"
|
||||||
|
"tables=set(inspect(engine).get_table_names());"
|
||||||
|
"required={'alembic_version','core_scopes','core_system_settings',"
|
||||||
|
"'core_runtime_nodes'};"
|
||||||
|
"missing=required-tables;"
|
||||||
|
"assert not missing, f'missing release tables: {sorted(missing)}';"
|
||||||
|
"engine.dispose()"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
timeout=timeout,
|
||||||
|
redactions=redactions,
|
||||||
|
)
|
||||||
|
record("release_schema_contract", began)
|
||||||
|
|
||||||
began = time.monotonic()
|
began = time.monotonic()
|
||||||
_run(
|
_run(
|
||||||
(
|
(
|
||||||
@@ -357,6 +413,8 @@ def run_smoke(
|
|||||||
names["api"],
|
names["api"],
|
||||||
"--network-alias",
|
"--network-alias",
|
||||||
"api",
|
"api",
|
||||||
|
"--network-alias",
|
||||||
|
"load-balancer",
|
||||||
*common_runtime,
|
*common_runtime,
|
||||||
*_env_arguments(
|
*_env_arguments(
|
||||||
environment,
|
environment,
|
||||||
@@ -388,6 +446,7 @@ def run_smoke(
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
),
|
),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
container=names["api"],
|
||||||
)
|
)
|
||||||
_run(
|
_run(
|
||||||
(
|
(
|
||||||
@@ -446,6 +505,7 @@ def run_smoke(
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
),
|
),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
container=names["web"],
|
||||||
)
|
)
|
||||||
_run(
|
_run(
|
||||||
("docker", "exec", names["web"], "sh", "-c", "test \"$(id -u)\" = 101"),
|
("docker", "exec", names["web"], "sh", "-c", "test \"$(id -u)\" = 101"),
|
||||||
@@ -507,6 +567,7 @@ def run_smoke(
|
|||||||
timeout=30,
|
timeout=30,
|
||||||
),
|
),
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
|
container=names["worker"],
|
||||||
)
|
)
|
||||||
_run(("docker", "stop", "--time", "20", names["worker"]), timeout=30)
|
_run(("docker", "stop", "--time", "20", names["worker"]), timeout=30)
|
||||||
record("worker_delivery_and_shutdown", began)
|
record("worker_delivery_and_shutdown", began)
|
||||||
|
|||||||
Reference in New Issue
Block a user