G8 실제 rollback 증명 종료와 비-secure origin 회기 리뷰 크래시 수정
G8 마지막 게이트인 receipt-bound 실제 image rollback을 격리 NAS vignette-preview-20260807 에서 실행해 종료했다. Gate6 계약 정정: 감사 대상 current API 이미지가 com.docker.compose.project/service/version image label 을 갖고 있어 "helper 의 compose label 0개" 계약은 감사되지 않은 다른 이미지를 쓰지 않는 한 성립하지 않는다. 계약을 key 부재가 아니라 소속(membership) 으로 바꿔 launch-nas-preview-g8-helpers.py 에 구현했다. image 상속 label 을 baseline 으로 읽고 container 의 모든 compose label 이 baseline 과 같거나 선언된 격리 override 인지 검사하며, 최종 project 는 target 이 아니고 service 는 api/web/db/proxy 가 아니어야 한다. docker run argv 에 target label 을 주입하면 fake-runner 테스트가 먼저 깨진다 (37/37). 실행 결과: - rollback-old receipt nas-g8-723eeef22eab05e63e3fafb0 -> 79ec../c530.. - restore-current receipt nas-g8-2738846cf2cf4fbe8ce0fc26 -> 52e0../6fdb.. - release gate/approval 각 2회 멱등, audit.ci_lifecycle_event rollback/executed 2, audit.ci_human_approval_event authorize_rollback 2, silent auto-promotion 0 - HMAC journal 6-record 체인 검증, health 3/3, OpenAPI 126, auth 401, Web 200 - helper 0, listener 0, 비밀 env 파기. down/volume rm/prune 미실행, 공개 런타임 미접촉 - 계획했던 Windows SSH 터널은 NAS sshd 가 direct-tcpip 를 거부해 사용할 수 없어 sshd 설정 변경 대신 같은 격리 계약의 NAS-side probe 컨테이너로 실행했다 비-secure origin 크래시 수정: 배포된 NAS 프리뷰(평문 HTTP, 비-localhost)에 회기 스펙을 돌려 24건 실패를 확인했고 원인은 하나였다. crypto.randomUUID 는 secure context 전용인데 제품 코드 18곳이 fallback 없이 호출했고 RuptureRepairCard 는 렌더 시점 호출이라 회기 리뷰 라우트 전체가 error boundary 로 떨어졌다. 릴리스 게이트 108/108 은 localhost 후보 스택에서만 돌아 이 경로를 밟은 적이 없다. src/lib/uuid.ts 의 randomUuid() 로 통일하고 fallback 도 crypto.getRandomValues 를 우선 사용해 idempotency key 의 예측 불가능성을 유지했다. 회귀는 insecure-context-uuid.spec.ts 6/6 으로 고정했다(직접 호출 0건 검사 포함). 이 수정은 아직 NAS 에 배포하지 않았다. 검증: API 898, gateway 58, executor 28, probe 11, helper launcher 37, release agent 21, ruff clean, web api-types/typecheck/build, SSOT FAIL 0, SSOT unit 5/5, dashboard E2E 10/10, 학생 폐루프 실 DB 브라우저 4/4(일회용 클론), crypto 수정 후 기존 스펙 회귀 70/70, 복원된 NAS 실제 브라우저 SSE->DB 리뷰 PASS. 부수 발견(열린 항목): 공개 API 가 engine=false 로 degraded 인데 워치독이 이를 감지하지 못한다. engine 판정이 게이트웨이 /health 의 ok 만 보고 claude readiness probe 를 돌리지 않기 때문이다. 같은 .env 와 같은 CLI 로 새 게이트웨이를 다른 포트에 띄우면 즉시 ready 이므로 상주 프로세스의 세션만 죽은 형태다. TODO A절과 대시보드에 기록했다. 이 커밋은 파일 단위로 담겼다. 위 파일들에는 이전 세션의 미커밋 G0~G8 작업이 함께 들어 있으며, hunk 를 쪼개면 대시보드/체커/TODO 정합성이 깨져 SSOT 체커가 실패한다.
This commit is contained in:
parent
76d0b9ae9b
commit
93dd8f82d7
22 changed files with 10057 additions and 473 deletions
534
scripts/test_launch_nas_preview_g8_helpers.py
Normal file
534
scripts/test_launch_nas_preview_g8_helpers.py
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).with_name("launch-nas-preview-g8-helpers.py")
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"launch_nas_preview_g8_helpers", SCRIPT_PATH
|
||||
)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = MODULE
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
RUN_ID = "20260807a1"
|
||||
IMAGE = "sha256:" + "5e" * 32
|
||||
MANIFEST_SHA = "e" * 64
|
||||
BASELINE_LABELS = {
|
||||
"com.docker.compose.project": MODULE.TARGET_PROJECT,
|
||||
"com.docker.compose.service": "api",
|
||||
"com.docker.compose.version": "2.20.1",
|
||||
}
|
||||
|
||||
|
||||
def executor_spec() -> object:
|
||||
return MODULE.executor_spec(
|
||||
run_id=RUN_ID, image=IMAGE, manifest_sha256=MANIFEST_SHA
|
||||
)
|
||||
|
||||
|
||||
def control_plane_spec() -> object:
|
||||
return MODULE.control_plane_spec(run_id=RUN_ID, image=IMAGE)
|
||||
|
||||
|
||||
def probe_spec(plan_name: str = "rollback-old") -> object:
|
||||
return MODULE.probe_spec(run_id=RUN_ID, image=IMAGE, plan_name=plan_name)
|
||||
|
||||
|
||||
def container_labels(spec: object, **overrides: str) -> dict[str, str]:
|
||||
labels = dict(BASELINE_LABELS)
|
||||
labels.update(spec.isolated_labels)
|
||||
labels.update(spec.provenance_labels)
|
||||
labels.update(overrides)
|
||||
return labels
|
||||
|
||||
|
||||
class FakeRunner:
|
||||
"""Record every argv and answer from a canned, operation-keyed map."""
|
||||
|
||||
def __init__(self, responses: dict[str, str] | None = None):
|
||||
self.calls: list[tuple[str, list[str]]] = []
|
||||
self.responses = responses or {}
|
||||
|
||||
def run(self, operation: str, argv: list[str], *, timeout: float) -> str:
|
||||
assert isinstance(argv, list) and argv
|
||||
assert all(isinstance(item, str) for item in argv)
|
||||
assert timeout > 0
|
||||
self.calls.append((operation, list(argv)))
|
||||
return self.responses.get(operation, "")
|
||||
|
||||
def argv_for(self, operation: str) -> list[str]:
|
||||
matches = [argv for name, argv in self.calls if name == operation]
|
||||
assert len(matches) == 1, operation
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _label_values(argv: list[str]) -> dict[str, str]:
|
||||
values: dict[str, str] = {}
|
||||
for index, item in enumerate(argv):
|
||||
if item == "--label":
|
||||
key, _, value = argv[index + 1].partition("=")
|
||||
values[key] = value
|
||||
return values
|
||||
|
||||
|
||||
class SpecContractTest(unittest.TestCase):
|
||||
def test_isolated_project_is_never_the_target_project(self) -> None:
|
||||
for spec in (executor_spec(), control_plane_spec(), probe_spec()):
|
||||
self.assertNotEqual(spec.isolated_project, MODULE.TARGET_PROJECT)
|
||||
self.assertTrue(
|
||||
spec.isolated_project.startswith(MODULE.HELPER_PROJECT_PREFIX)
|
||||
)
|
||||
|
||||
def test_isolated_service_is_never_a_target_service(self) -> None:
|
||||
for spec in (executor_spec(), control_plane_spec(), probe_spec()):
|
||||
self.assertNotIn(spec.isolated_service, MODULE.TARGET_SERVICES)
|
||||
|
||||
def test_container_name_is_not_a_target_compose_container(self) -> None:
|
||||
for spec in (executor_spec(), control_plane_spec(), probe_spec()):
|
||||
for service in MODULE.TARGET_SERVICES:
|
||||
self.assertNotEqual(
|
||||
spec.container_name, f"{MODULE.TARGET_PROJECT}-{service}-1"
|
||||
)
|
||||
|
||||
def test_invalid_run_id_and_image_fail_closed(self) -> None:
|
||||
with self.assertRaises(MODULE.HelperError):
|
||||
MODULE.control_plane_spec(run_id="BAD ID", image=IMAGE)
|
||||
with self.assertRaises(MODULE.HelperError):
|
||||
MODULE.control_plane_spec(run_id=RUN_ID, image="latest")
|
||||
with self.assertRaises(MODULE.HelperError):
|
||||
MODULE.executor_spec(run_id=RUN_ID, image=IMAGE, manifest_sha256="nope")
|
||||
|
||||
|
||||
class RunArgvContractTest(unittest.TestCase):
|
||||
def test_argv_never_labels_the_target_project_or_service(self) -> None:
|
||||
for spec in (executor_spec(), control_plane_spec(), probe_spec()):
|
||||
argv = MODULE.build_run_argv(spec)
|
||||
labels = _label_values(argv)
|
||||
self.assertEqual(
|
||||
labels[MODULE.PROJECT_LABEL], spec.isolated_project
|
||||
)
|
||||
self.assertEqual(
|
||||
labels[MODULE.SERVICE_LABEL], spec.isolated_service
|
||||
)
|
||||
self.assertNotIn(MODULE.TARGET_PROJECT, labels.values())
|
||||
self.assertNotIn(labels[MODULE.SERVICE_LABEL], MODULE.TARGET_SERVICES)
|
||||
|
||||
def test_argv_declares_only_the_two_isolated_compose_labels(self) -> None:
|
||||
argv = MODULE.build_run_argv(executor_spec())
|
||||
compose_labels = {
|
||||
key
|
||||
for key in _label_values(argv)
|
||||
if key.startswith(MODULE.COMPOSE_LABEL_PREFIX)
|
||||
}
|
||||
self.assertEqual(
|
||||
compose_labels, {MODULE.PROJECT_LABEL, MODULE.SERVICE_LABEL}
|
||||
)
|
||||
|
||||
def test_injected_target_project_label_is_rejected(self) -> None:
|
||||
spec = executor_spec()
|
||||
argv = MODULE.build_run_argv(spec)
|
||||
tampered = argv + [
|
||||
"--label",
|
||||
f"{MODULE.PROJECT_LABEL}={MODULE.TARGET_PROJECT}",
|
||||
]
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
MODULE.assert_argv_isolation(tampered, spec)
|
||||
self.assertEqual(ctx.exception.code, "helper_argv_targets_project")
|
||||
|
||||
def test_injected_target_service_label_is_rejected(self) -> None:
|
||||
spec = control_plane_spec()
|
||||
tampered = MODULE.build_run_argv(spec) + [
|
||||
"--label",
|
||||
f"{MODULE.SERVICE_LABEL}=api",
|
||||
]
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
MODULE.assert_argv_isolation(tampered, spec)
|
||||
self.assertEqual(ctx.exception.code, "helper_argv_targets_service")
|
||||
|
||||
def test_undeclared_compose_label_is_rejected(self) -> None:
|
||||
spec = executor_spec()
|
||||
tampered = MODULE.build_run_argv(spec) + [
|
||||
"--label",
|
||||
f"{MODULE.COMPOSE_LABEL_PREFIX}config-hash=deadbeef",
|
||||
]
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
MODULE.assert_argv_isolation(tampered, spec)
|
||||
self.assertEqual(ctx.exception.code, "helper_argv_undeclared_compose_label")
|
||||
|
||||
def test_label_file_and_non_host_network_are_rejected(self) -> None:
|
||||
spec = executor_spec()
|
||||
with self.assertRaises(MODULE.HelperError):
|
||||
MODULE.assert_argv_isolation(
|
||||
MODULE.build_run_argv(spec) + ["--label-file", "/tmp/labels"], spec
|
||||
)
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
MODULE.assert_argv_isolation(
|
||||
[
|
||||
"docker",
|
||||
"run",
|
||||
"--network",
|
||||
f"{MODULE.TARGET_PROJECT}_vignette",
|
||||
spec.image,
|
||||
],
|
||||
spec,
|
||||
)
|
||||
self.assertEqual(ctx.exception.code, "helper_argv_network_not_isolated")
|
||||
|
||||
def test_executor_argv_carries_the_hardening_flags(self) -> None:
|
||||
argv = MODULE.build_run_argv(executor_spec())
|
||||
self.assertIn("--read-only", argv)
|
||||
self.assertIn("--cap-drop", argv)
|
||||
self.assertEqual(argv[argv.index("--cap-drop") + 1], "ALL")
|
||||
self.assertIn("--security-opt", argv)
|
||||
self.assertEqual(
|
||||
argv[argv.index("--security-opt") + 1], "no-new-privileges"
|
||||
)
|
||||
self.assertEqual(
|
||||
argv[argv.index("--user") + 1],
|
||||
f"{MODULE.HELPER_UID}:{MODULE.HELPER_GID}",
|
||||
)
|
||||
self.assertEqual(
|
||||
argv[argv.index("--group-add") + 1],
|
||||
str(MODULE.HELPER_SUPPLEMENTAL_GID),
|
||||
)
|
||||
self.assertEqual(argv[argv.index("--network") + 1], "host")
|
||||
|
||||
def test_executor_argv_mounts_target_root_read_only_and_state_rw(self) -> None:
|
||||
argv = MODULE.build_run_argv(executor_spec())
|
||||
mounts = [
|
||||
argv[index + 1]
|
||||
for index, item in enumerate(argv)
|
||||
if item == "--mount"
|
||||
]
|
||||
self.assertIn(
|
||||
f"type=bind,source={MODULE.TARGET_ROOT},"
|
||||
f"target={MODULE.TARGET_ROOT},readonly",
|
||||
mounts,
|
||||
)
|
||||
self.assertIn(
|
||||
f"type=bind,source={MODULE.STATE_DIR},target={MODULE.STATE_DIR}",
|
||||
mounts,
|
||||
)
|
||||
self.assertIn(
|
||||
f"type=bind,source={MODULE.DOCKER_BINARY_SOURCE},"
|
||||
f"target={MODULE.DOCKER_BINARY_TARGET},readonly",
|
||||
mounts,
|
||||
)
|
||||
self.assertIn(
|
||||
f"type=bind,source={MODULE.COMPOSE_PLUGIN_SOURCE},"
|
||||
f"target={MODULE.COMPOSE_PLUGIN_TARGET},readonly",
|
||||
mounts,
|
||||
)
|
||||
self.assertIn(
|
||||
f"type=bind,source={MODULE.TARGET_DOCKER_SOCKET},"
|
||||
f"target={MODULE.TARGET_DOCKER_SOCKET}",
|
||||
mounts,
|
||||
)
|
||||
|
||||
def test_executor_argv_binds_loopback_and_the_audited_manifest(self) -> None:
|
||||
argv = MODULE.build_run_argv(executor_spec())
|
||||
self.assertEqual(argv[argv.index("--entrypoint") + 1], "python3.11")
|
||||
self.assertEqual(argv[argv.index("--host") + 1], MODULE.EXECUTOR_HOST)
|
||||
self.assertEqual(argv[argv.index("--port") + 1], str(MODULE.EXECUTOR_PORT))
|
||||
self.assertEqual(argv[argv.index("--manifest") + 1], str(MODULE.MANIFEST_PATH))
|
||||
self.assertEqual(argv[argv.index("--manifest-sha256") + 1], MANIFEST_SHA)
|
||||
self.assertIn("--enable", argv)
|
||||
|
||||
def test_control_plane_argv_binds_its_own_loopback_port(self) -> None:
|
||||
argv = MODULE.build_run_argv(control_plane_spec())
|
||||
self.assertEqual(argv[argv.index("--entrypoint") + 1], "uvicorn")
|
||||
self.assertEqual(argv[argv.index("--host") + 1], MODULE.CONTROL_PLANE_HOST)
|
||||
self.assertEqual(
|
||||
argv[argv.index("--port") + 1], str(MODULE.CONTROL_PLANE_PORT)
|
||||
)
|
||||
self.assertNotIn("--publish", argv)
|
||||
|
||||
def test_argv_never_mutates_the_target_compose_project(self) -> None:
|
||||
for spec in (executor_spec(), control_plane_spec(), probe_spec()):
|
||||
argv = MODULE.build_run_argv(spec)
|
||||
self.assertEqual(argv[1], "run")
|
||||
for forbidden in ("compose", "down", "prune", "rm", "stop", "kill"):
|
||||
self.assertNotIn(forbidden, argv[1:2])
|
||||
self.assertNotIn(MODULE.TARGET_PROJECT, argv[3:])
|
||||
|
||||
|
||||
class ProbeRoleTest(unittest.TestCase):
|
||||
def test_probe_never_mounts_the_docker_socket(self) -> None:
|
||||
argv = MODULE.build_run_argv(probe_spec())
|
||||
mounts = [
|
||||
argv[index + 1] for index, item in enumerate(argv) if item == "--mount"
|
||||
]
|
||||
self.assertFalse(
|
||||
any(MODULE.TARGET_DOCKER_SOCKET in mount for mount in mounts)
|
||||
)
|
||||
self.assertNotIn("--group-add", argv)
|
||||
|
||||
def test_probe_targets_the_separate_control_plane_origin(self) -> None:
|
||||
argv = MODULE.build_run_argv(probe_spec())
|
||||
control = argv[argv.index("--control-plane-base-url") + 1]
|
||||
preview = argv[argv.index("--preview-base-url") + 1]
|
||||
self.assertEqual(control, MODULE.CONTROL_PLANE_BASE_URL)
|
||||
self.assertEqual(preview, MODULE.PREVIEW_BASE_URL)
|
||||
self.assertNotEqual(control, preview)
|
||||
|
||||
def test_probe_plan_and_output_stay_inside_the_state_dir(self) -> None:
|
||||
for plan_name in ("rollback-old", "restore-current"):
|
||||
argv = MODULE.build_run_argv(probe_spec(plan_name))
|
||||
plan = argv[argv.index("--plan") + 1]
|
||||
output = argv[argv.index("--output") + 1]
|
||||
self.assertEqual(
|
||||
plan, f"{MODULE.PLANS_DIR}/{plan_name}.plan.json"
|
||||
)
|
||||
self.assertTrue(output.startswith(f"{MODULE.EVIDENCE_DIR}/"))
|
||||
self.assertIn("execute", argv)
|
||||
|
||||
def test_probe_plan_name_is_validated(self) -> None:
|
||||
for bad in ("../escape", "Rollback", "a", "x" * 80):
|
||||
with self.assertRaises(MODULE.HelperError):
|
||||
MODULE.probe_spec(run_id=RUN_ID, image=IMAGE, plan_name=bad)
|
||||
|
||||
def test_wait_reports_the_exit_code(self) -> None:
|
||||
spec = probe_spec()
|
||||
runner = FakeRunner(
|
||||
{
|
||||
"wait_helper": "0\n",
|
||||
"inspect_container_id": "b" * 64 + "\n",
|
||||
"inspect_container_state": "false\n",
|
||||
}
|
||||
)
|
||||
result = MODULE.HelperLauncher(runner).wait(spec, timeout_seconds=60)
|
||||
self.assertEqual(result["exit_code"], 0)
|
||||
self.assertFalse(result["container_running"])
|
||||
self.assertEqual(
|
||||
runner.argv_for("wait_helper"), ["docker", "wait", spec.container_name]
|
||||
)
|
||||
|
||||
def test_wait_rejects_an_unbounded_timeout(self) -> None:
|
||||
with self.assertRaises(MODULE.HelperError):
|
||||
MODULE.HelperLauncher(FakeRunner()).wait(probe_spec(), timeout_seconds=0)
|
||||
|
||||
|
||||
class IsolationVerificationTest(unittest.TestCase):
|
||||
def test_inherited_baseline_labels_are_accepted(self) -> None:
|
||||
spec = executor_spec()
|
||||
evidence = MODULE.verify_container_isolation(
|
||||
spec=spec,
|
||||
baseline_labels=BASELINE_LABELS,
|
||||
container_labels=container_labels(spec),
|
||||
target_project_members=[f"{MODULE.TARGET_PROJECT}-api-1"],
|
||||
)
|
||||
self.assertEqual(
|
||||
evidence["inherited_compose_labels"],
|
||||
{"com.docker.compose.version": "2.20.1"},
|
||||
)
|
||||
self.assertEqual(
|
||||
evidence["isolated_override_labels"],
|
||||
{
|
||||
MODULE.PROJECT_LABEL: spec.isolated_project,
|
||||
MODULE.SERVICE_LABEL: spec.isolated_service,
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
evidence["baseline_compose_labels"]["com.docker.compose.project"],
|
||||
MODULE.TARGET_PROJECT,
|
||||
)
|
||||
|
||||
def test_zero_compose_labels_is_not_required(self) -> None:
|
||||
spec = executor_spec()
|
||||
labels = container_labels(spec)
|
||||
self.assertTrue(
|
||||
any(key.startswith(MODULE.COMPOSE_LABEL_PREFIX) for key in labels)
|
||||
)
|
||||
MODULE.verify_container_isolation(
|
||||
spec=spec,
|
||||
baseline_labels=BASELINE_LABELS,
|
||||
container_labels=labels,
|
||||
target_project_members=[],
|
||||
)
|
||||
|
||||
def test_target_project_membership_is_rejected(self) -> None:
|
||||
spec = executor_spec()
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
MODULE.verify_container_isolation(
|
||||
spec=spec,
|
||||
baseline_labels=BASELINE_LABELS,
|
||||
container_labels=container_labels(
|
||||
spec, **{MODULE.PROJECT_LABEL: MODULE.TARGET_PROJECT}
|
||||
),
|
||||
target_project_members=[],
|
||||
)
|
||||
self.assertEqual(ctx.exception.code, "helper_isolated_label_mismatch")
|
||||
|
||||
def test_target_service_claim_is_rejected(self) -> None:
|
||||
spec = MODULE.HelperSpec(
|
||||
role="executor",
|
||||
run_id=RUN_ID,
|
||||
image=IMAGE,
|
||||
env_file=str(MODULE.STATE_DIR / "x.env"),
|
||||
entrypoint="python3.11",
|
||||
command=("-c", "pass"),
|
||||
)
|
||||
labels = container_labels(spec)
|
||||
labels[MODULE.SERVICE_LABEL] = "db"
|
||||
with self.assertRaises(MODULE.HelperError):
|
||||
MODULE.verify_container_isolation(
|
||||
spec=spec,
|
||||
baseline_labels=BASELINE_LABELS,
|
||||
container_labels=labels,
|
||||
target_project_members=[],
|
||||
)
|
||||
|
||||
def test_unexpected_compose_label_drift_is_rejected(self) -> None:
|
||||
spec = executor_spec()
|
||||
labels = container_labels(spec)
|
||||
labels["com.docker.compose.version"] = "9.9.9"
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
MODULE.verify_container_isolation(
|
||||
spec=spec,
|
||||
baseline_labels=BASELINE_LABELS,
|
||||
container_labels=labels,
|
||||
target_project_members=[],
|
||||
)
|
||||
self.assertEqual(ctx.exception.code, "helper_compose_label_drift")
|
||||
|
||||
def test_missing_isolated_override_is_rejected(self) -> None:
|
||||
spec = executor_spec()
|
||||
labels = container_labels(spec)
|
||||
del labels[MODULE.SERVICE_LABEL]
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
MODULE.verify_container_isolation(
|
||||
spec=spec,
|
||||
baseline_labels=BASELINE_LABELS,
|
||||
container_labels=labels,
|
||||
target_project_members=[],
|
||||
)
|
||||
self.assertEqual(ctx.exception.code, "helper_isolated_label_missing")
|
||||
|
||||
def test_helper_listed_in_target_project_is_rejected(self) -> None:
|
||||
spec = executor_spec()
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
MODULE.verify_container_isolation(
|
||||
spec=spec,
|
||||
baseline_labels=BASELINE_LABELS,
|
||||
container_labels=container_labels(spec),
|
||||
target_project_members=[spec.container_name],
|
||||
)
|
||||
self.assertEqual(ctx.exception.code, "helper_listed_in_target_project")
|
||||
|
||||
def test_missing_provenance_label_is_rejected(self) -> None:
|
||||
spec = executor_spec()
|
||||
labels = container_labels(spec)
|
||||
del labels[MODULE.HELPER_LABEL]
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
MODULE.verify_container_isolation(
|
||||
spec=spec,
|
||||
baseline_labels=BASELINE_LABELS,
|
||||
container_labels=labels,
|
||||
target_project_members=[],
|
||||
)
|
||||
self.assertEqual(ctx.exception.code, "helper_provenance_label_missing")
|
||||
|
||||
|
||||
class LauncherRunnerTest(unittest.TestCase):
|
||||
def _responses(self, spec: object) -> dict[str, str]:
|
||||
return {
|
||||
"inspect_image_labels": json.dumps(BASELINE_LABELS),
|
||||
"inspect_container_labels": json.dumps(container_labels(spec)),
|
||||
"inspect_container_id": "a" * 64 + "\n",
|
||||
"inspect_container_state": "true\n",
|
||||
"list_target_project_members": (
|
||||
f"{MODULE.TARGET_PROJECT}-api-1\n{MODULE.TARGET_PROJECT}-web-1\n"
|
||||
),
|
||||
}
|
||||
|
||||
def test_start_uses_the_exact_argv_and_returns_the_container_id(self) -> None:
|
||||
spec = executor_spec()
|
||||
runner = FakeRunner(self._responses(spec))
|
||||
launcher = MODULE.HelperLauncher(runner)
|
||||
container_id = launcher.start(spec)
|
||||
self.assertEqual(container_id, "a" * 64)
|
||||
self.assertEqual(
|
||||
runner.argv_for("run_helper"), MODULE.build_run_argv(spec)
|
||||
)
|
||||
|
||||
def test_verify_emits_exact_container_identity_evidence(self) -> None:
|
||||
spec = control_plane_spec()
|
||||
runner = FakeRunner(self._responses(spec))
|
||||
evidence = MODULE.HelperLauncher(runner).verify(spec)
|
||||
self.assertEqual(evidence["schema_version"], MODULE.EVIDENCE_SCHEMA)
|
||||
self.assertEqual(evidence["container_name"], spec.container_name)
|
||||
self.assertEqual(evidence["container_id"], "a" * 64)
|
||||
self.assertTrue(evidence["container_running"])
|
||||
self.assertEqual(evidence["target_project"], MODULE.TARGET_PROJECT)
|
||||
self.assertNotIn(spec.container_name, evidence["target_project_members"])
|
||||
|
||||
def test_membership_query_filters_on_the_target_project_label(self) -> None:
|
||||
spec = executor_spec()
|
||||
runner = FakeRunner(self._responses(spec))
|
||||
MODULE.HelperLauncher(runner).verify(spec)
|
||||
argv = runner.argv_for("list_target_project_members")
|
||||
self.assertIn("ps", argv)
|
||||
self.assertIn(
|
||||
f"label={MODULE.PROJECT_LABEL}={MODULE.TARGET_PROJECT}", argv
|
||||
)
|
||||
|
||||
def test_stop_refuses_a_container_outside_the_isolated_project(self) -> None:
|
||||
spec = executor_spec()
|
||||
responses = self._responses(spec)
|
||||
foreign = container_labels(spec)
|
||||
foreign[MODULE.PROJECT_LABEL] = "someone-else"
|
||||
responses["inspect_container_labels"] = json.dumps(foreign)
|
||||
launcher = MODULE.HelperLauncher(FakeRunner(responses))
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
launcher.stop(spec)
|
||||
self.assertEqual(ctx.exception.code, "helper_stop_project_mismatch")
|
||||
|
||||
def test_stop_only_touches_the_exact_helper_name(self) -> None:
|
||||
spec = executor_spec()
|
||||
runner = FakeRunner(self._responses(spec))
|
||||
MODULE.HelperLauncher(runner).stop(spec)
|
||||
stop_argv = runner.argv_for("stop_helper")
|
||||
remove_argv = runner.argv_for("remove_helper")
|
||||
self.assertEqual(stop_argv[-1], spec.container_name)
|
||||
self.assertEqual(remove_argv, ["docker", "rm", spec.container_name])
|
||||
self.assertNotIn("--force", remove_argv)
|
||||
self.assertNotIn("--volumes", remove_argv)
|
||||
|
||||
def test_invalid_label_json_fails_closed(self) -> None:
|
||||
spec = executor_spec()
|
||||
responses = self._responses(spec)
|
||||
responses["inspect_image_labels"] = "not-json"
|
||||
launcher = MODULE.HelperLauncher(FakeRunner(responses))
|
||||
with self.assertRaises(MODULE.HelperError) as ctx:
|
||||
launcher.image_labels(spec.image)
|
||||
self.assertEqual(ctx.exception.code, "helper_labels_unreadable")
|
||||
|
||||
def test_null_image_labels_are_read_as_empty(self) -> None:
|
||||
runner = FakeRunner({"inspect_image_labels": "null\n"})
|
||||
launcher = MODULE.HelperLauncher(runner)
|
||||
self.assertEqual(launcher.image_labels(IMAGE), {})
|
||||
|
||||
def test_ssh_runner_quotes_the_argv_into_one_remote_command(self) -> None:
|
||||
inner = FakeRunner()
|
||||
runner = MODULE.SshCommandRunner(
|
||||
destination="user@host",
|
||||
identity_file="C:/key",
|
||||
inner=inner,
|
||||
)
|
||||
runner.run("probe", ["docker", "ps", "--filter", "label=a=b c"], timeout=5)
|
||||
argv = inner.argv_for("probe")
|
||||
self.assertEqual(argv[0], "ssh")
|
||||
self.assertIn("BatchMode=yes", argv)
|
||||
self.assertEqual(argv[-2], "user@host")
|
||||
self.assertEqual(argv[-1], "docker ps --filter 'label=a=b c'")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue