from __future__ import annotations import copy import importlib.util import json import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] MODULE_PATH = ROOT / "ops" / "gitea-deploy-broker" / "registered_legacy_adoption.py" REPOSITORY = "jens/quiz-monitor" REQUEST_ID = "6f9619ff-8b86-4011-b42d-00c04fc964ff" TARGET_SHA = "a" * 40 BASELINE_IMAGE = "sha256:" + "b" * 64 CANDIDATE_IMAGE = "sha256:" + "c" * 64 CONTAINER_NAME = "quiz-monitor-quiz-server-1" KEY = b"k" * 32 CONFIG_BIND = "/srv/example/appdata/quiz-monitor/config" TEMPLATE_VALUE = "keep literal $DOLLAR and ${BRACED}" QUIZ_IMAGE_CMD = [ "uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000", ] def _load_module(): spec = importlib.util.spec_from_file_location( "registered_legacy_runtime_test", MODULE_PATH ) assert spec and spec.loader module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module @pytest.fixture def module(): return _load_module() def _project(source: Path) -> dict[str, object]: return { "slug": "quiz-monitor", "branch": "main", "compose_project": "quiz-monitor", "compose_files": ["docker-compose.yml"], "containers": [CONTAINER_NAME], "source_dir": str(source), "verify_oci_revision": True, } def _compose_model() -> dict[str, object]: return { "name": "quiz-monitor", "services": { "quiz-server": { "image": "quiz-monitor:latest", "command": None, "entrypoint": None, "environment": { "OPENRGB_HOST": "198.19.1.2", "QUIZ_TEMPLATE": TEMPLATE_VALUE, }, "volumes": [ { "type": "bind", "source": CONFIG_BIND, "target": "/config", "read_only": True, }, { "type": "volume", "source": "quiz-monitor-data", "target": "/data", "read_only": False, }, ], "ports": [ { "target": 8000, "published": "1205", "protocol": "tcp", "host_ip": "0.0.0.0", } ], "networks": {"default": None}, "labels": {"org.example.app": "quiz-monitor"}, "cap_drop": ["ALL"], "security_opt": ["no-new-privileges:true"], "read_only": True, "mem_limit": "536870912", "cpus": "0.5", "restart": "unless-stopped", } }, "networks": {"default": {"name": "quiz-monitor_default"}}, "volumes": {"quiz-monitor-data": {"name": "quiz-monitor_quiz-monitor-data"}}, } def _container( *, image_id: str = BASELINE_IMAGE, container_id: str = "d" * 64, hostname: str = "a1b2c3d4e5f6", config_hash: str = "old-config-hash", environment: str = TEMPLATE_VALUE, config_bind: str = CONFIG_BIND, host_port: str = "1205", security_opt: list[str] | None = None, memory: int = 536870912, network_mode: str = "quiz-monitor_default", ) -> dict[str, object]: labels = { "com.docker.compose.project": "quiz-monitor", "com.docker.compose.service": "quiz-server", "com.docker.compose.container-number": "1", "com.docker.compose.config-hash": config_hash, "com.docker.compose.project.config_files": "/srv/example/appdata/quiz-monitor/source/docker-compose.yml", "com.docker.compose.project.working_dir": "/srv/example/appdata/quiz-monitor/source", "com.docker.compose.version": "2.39.1", "org.example.app": "quiz-monitor", } return { "Id": container_id, "Image": image_id, "Config": { "Hostname": hostname, "Image": "quiz-monitor:latest", "Env": ["OPENRGB_HOST=198.19.1.2", f"QUIZ_TEMPLATE={environment}"], "Labels": labels, "ExposedPorts": {"8000/tcp": {}}, "WorkingDir": "/app", "Cmd": list(QUIZ_IMAGE_CMD), "Entrypoint": None, }, "HostConfig": { "Binds": [f"{config_bind}:/config:ro"], "NetworkMode": network_mode, "PortBindings": { "8000/tcp": [{"HostIp": "0.0.0.0", "HostPort": host_port}] }, "RestartPolicy": {"Name": "unless-stopped", "MaximumRetryCount": 0}, "CapDrop": ["ALL"], "SecurityOpt": list(security_opt or ["no-new-privileges:true"]), "ReadonlyRootfs": True, "Memory": memory, "NanoCpus": 500000000, "ShmSize": 67108864, }, "Mounts": [ { "Type": "bind", "Source": config_bind, "Destination": "/config", "RW": False, }, { "Type": "volume", "Name": "quiz-monitor_quiz-monitor-data", "Source": "/var/lib/docker/volumes/quiz-monitor_quiz-monitor-data/_data", "Destination": "/data", "RW": True, }, ], "NetworkSettings": { "Ports": {"8000/tcp": [{"HostIp": "0.0.0.0", "HostPort": host_port}]}, "Networks": {"quiz-monitor_default": {"NetworkID": "e" * 64}}, }, "State": {"Running": True}, } class _DockerComposeBoundary: """Mock the bounded subprocess boundary while exercising the real adapter.""" def __init__(self, module, project, model): self.module, self.project, self.model = module, project, model self.live = _container() self.images = { BASELINE_IMAGE: { "Id": BASELINE_IMAGE, "Config": { "Labels": {}, "Cmd": list(QUIZ_IMAGE_CMD), "Entrypoint": None, "WorkingDir": "/app", }, }, CANDIDATE_IMAGE: { "Id": CANDIDATE_IMAGE, "Config": { "Labels": { "org.opencontainers.image.revision": TARGET_SHA, "org.opencontainers.image.source": REPOSITORY, }, "Cmd": list(QUIZ_IMAGE_CMD), "Entrypoint": None, "WorkingDir": "/app", }, }, } self.commands: list[list[str]] = [] self.compose_config_commands: list[list[str]] = [] self.compose_ups = 0 self.source = Path(project["source_dir"]) def install(self, monkeypatch): def compose_config(command, **_kwargs): self.compose_config_commands.append(list(command)) if command[-3:] == ["config", "--format", "json"]: output = json.dumps(self.model) elif command[-2:] == ["config", "--images"]: output = "quiz-monitor:latest\n" else: raise AssertionError(f"unexpected Compose config command: {command}") return subprocess.CompletedProcess(command, 0, stdout=output) monkeypatch.setattr(self.module.subprocess, "run", compose_config) def docker_run(command, *, cwd=None, timeout=45, env=None): del cwd, timeout, env self.commands.append(list(command)) if command[:2] == ["docker", "ps"]: return CONTAINER_NAME + "\n" if command[:2] == ["docker", "inspect"]: return json.dumps([copy.deepcopy(self.live)]) if command[:3] == ["docker", "image", "inspect"]: image = self.images.get(command[3]) return json.dumps([copy.deepcopy(image)]) if image else "[]" if command[:3] == ["docker", "image", "tag"]: image = self.images.get(command[3]) if image is None: raise AssertionError("tag source image was not inspected") self.images[command[4]] = copy.deepcopy(image) return "" if command[:3] == ["docker", "compose", "--project-name"]: if command[-3:] == ["config", "--format", "json"]: return json.dumps(self.model) if command[-2:] == ["config", "--images"]: return "quiz-monitor:latest\n" if ( command[:3] == ["docker", "compose", "--project-name"] and command[-1] == "--force-recreate" ): self.compose_ups += 1 rollback_file = Path(command[command.index("--file") + 1]) model = json.loads(rollback_file.read_text(encoding="utf-8")) service = model["services"]["quiz-server"] assert service["image"].startswith("gitea-deploy-rollback/") assert service["environment"][ "QUIZ_TEMPLATE" ] == TEMPLATE_VALUE.replace("$", "$$") self.live = _container( container_id="f" * 64, hostname="new-generated-hostname", config_hash="new-config-hash", ) self.live["Config"]["Image"] = service["image"] return "" raise AssertionError(f"unexpected Docker command: {command}") monkeypatch.setattr( self.module._ProductionRuntime, "_run", staticmethod(docker_run) ) self.images["quiz-monitor:latest"] = copy.deepcopy(self.images[CANDIDATE_IMAGE]) @pytest.fixture def production_runtime(module, monkeypatch, tmp_path): source = tmp_path / "quiz-source" source.mkdir() (source / "docker-compose.yml").write_text("services: {}\n", encoding="utf-8") project = _project(source) model = _compose_model() boundary = _DockerComposeBoundary(module, project, model) boundary.install(monkeypatch) return module._ProductionRuntime(tmp_path / "state"), boundary, project, model def test_production_observe_binds_rendered_model_and_runtime_contract( production_runtime, ): runtime, boundary, project, model = production_runtime observed = runtime.observe(REPOSITORY, project, receipt_key=KEY) contract = runtime._runtime_contract(boundary.live) effective_model = copy.deepcopy(model) effective_model["services"]["quiz-server"]["logging"] = { "driver": "json-file", "options": {}, } assert observed["runtimeConfiguration"]["composeModel"] == effective_model assert ( observed["runtimeConfiguration"]["containers"][CONTAINER_NAME] == boundary.live ) assert observed["imageIDs"] == {CONTAINER_NAME: BASELINE_IMAGE} assert contract["config"]["Env"] == [ "OPENRGB_HOST=198.19.1.2", f"QUIZ_TEMPLATE={TEMPLATE_VALUE}", ] assert contract["mounts"] == [ ("bind", CONFIG_BIND, "/config", False, ""), ( "volume", "/var/lib/docker/volumes/quiz-monitor_quiz-monitor-data/_data", "/data", True, "quiz-monitor_quiz-monitor-data", ), ] assert contract["ports"] == { "8000/tcp": [{"HostIp": "0.0.0.0", "HostPort": "1205"}], } assert contract["host"]["NetworkMode"] == "quiz-monitor_default" assert contract["host"]["SecurityOpt"] == ["no-new-privileges:true"] assert contract["host"]["CapDrop"] == ["ALL"] assert contract["host"]["ReadonlyRootfs"] is True assert contract["host"]["Memory"] == 536870912 assert contract["host"]["NanoCpus"] == 500000000 assert contract["host"]["ShmSize"] == 67108864 assert contract["config"]["Cmd"] == QUIZ_IMAGE_CMD assert contract["config"]["Entrypoint"] is None assert model["services"]["quiz-server"]["command"] is None assert model["services"]["quiz-server"]["entrypoint"] is None assert "Hostname" not in contract["config"] assert "com.docker.compose.config-hash" not in contract["config"]["Labels"] recreated = _container( container_id="f" * 64, hostname="regenerated-hostname", config_hash="regenerated-config-hash", ) recreated["Config"]["Labels"].update( { "com.docker.compose.container-number": "7", "com.docker.compose.project.config_files": "/staged/release/docker-compose.yml", "com.docker.compose.project.working_dir": "/staged/release", "com.docker.compose.version": "2.40.0", } ) assert runtime._runtime_contract(recreated) == contract @pytest.mark.parametrize( "change", [ lambda item: item["Config"]["Env"].__setitem__(1, "QUIZ_TEMPLATE=drift"), lambda item: item["Mounts"][0].__setitem__("Source", "/other/config"), lambda item: item["NetworkSettings"]["Ports"]["8000/tcp"][0].__setitem__( "HostPort", "1206" ), lambda item: item["HostConfig"]["SecurityOpt"].clear(), lambda item: item["HostConfig"].__setitem__("Memory", 1073741824), lambda item: item["HostConfig"].__setitem__("NetworkMode", "host"), lambda item: item["NetworkSettings"]["Networks"].__setitem__( "rogue", {"NetworkID": "9" * 64} ), ], ) def test_production_observe_rejects_runtime_drift_before_baseline_capture( production_runtime, change, module ): runtime, boundary, project, _model = production_runtime change(boundary.live) with pytest.raises(module.RegisteredLegacyAdoptionError): runtime.observe(REPOSITORY, project, receipt_key=KEY) assert boundary.compose_ups == 0 assert not any( command[:3] == ["docker", "image", "tag"] for command in boundary.commands ) @pytest.mark.parametrize( ("compose_option", "host_option", "unsafe_value"), [ ("privileged", "Privileged", True), ("security_opt", "SecurityOpt", ["seccomp=unconfined"]), ("cap_add", "CapAdd", ["SYS_ADMIN"]), ], ) def test_production_observe_rejects_unsafe_host_options_omitted_from_compose( production_runtime, compose_option, host_option, unsafe_value, module ): runtime, boundary, project, _model = production_runtime service = boundary.model["services"]["quiz-server"] service.pop(compose_option, None) boundary.live["HostConfig"][host_option] = unsafe_value with pytest.raises(module.RegisteredLegacyAdoptionError): runtime.observe(REPOSITORY, project, receipt_key=KEY) assert boundary.compose_ups == 0 assert not any( command[:3] == ["docker", "image", "tag"] for command in boundary.commands ) @pytest.mark.parametrize( ("inspect_key", "unsafe_value"), [ ("Cmd", ["/bin/sh", "-c", "exit 0"]), ("Entrypoint", ["/bin/sh"]), ], ) def test_production_observe_rejects_process_args_that_differ_from_image_defaults( production_runtime, inspect_key, unsafe_value, module ): runtime, boundary, project, _model = production_runtime boundary.live["Config"][inspect_key] = unsafe_value with pytest.raises(module.RegisteredLegacyAdoptionError): runtime.observe(REPOSITORY, project, receipt_key=KEY) assert boundary.compose_ups == 0 def test_production_snapshot_restore_preserves_literal_env_mount_ports_and_known_image( adopted_production_runtime, tmp_path ): ( runtime, boundary, project, _model, registry, _read_state, state_bytes, snapshot, ) = adopted_production_runtime module = boundary.module prior_contract = runtime._runtime_contract(boundary.live) prior_mounts = copy.deepcopy(boundary.live["Mounts"]) prior_ports = copy.deepcopy(boundary.live["NetworkSettings"]["Ports"]) prior_environment = copy.deepcopy(boundary.live["Config"]["Env"]) live, _controller_state = _candidate_deployment( module, runtime, boundary, project, registry, state_bytes, tmp_path ) boundary.commands.clear() result = module.restore_deployment_snapshot( REPOSITORY, project, {**snapshot, "candidateSha256": live["candidateSha256"]}, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, expected_current=live, ) assert boundary.compose_ups == 1 assert result["imageIDs"] == {CONTAINER_NAME: BASELINE_IMAGE} assert boundary.live["Config"]["Env"] == prior_environment assert boundary.live["Mounts"] == prior_mounts assert boundary.live["NetworkSettings"]["Ports"] == prior_ports assert runtime._runtime_contract(boundary.live) == prior_contract def test_production_restore_restarts_a_stopped_signed_baseline( adopted_production_runtime, tmp_path ): runtime, boundary, project, _model, _registry, _read_state, _state_bytes, snapshot = ( adopted_production_runtime ) module = boundary.module boundary.live["State"]["Running"] = False boundary.commands.clear() restored = module.restore_deployment_snapshot( REPOSITORY, project, snapshot, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) assert boundary.compose_ups == 1 assert any(command[-1:] == ["--force-recreate"] for command in boundary.commands) assert restored["imageIDs"] == {CONTAINER_NAME: BASELINE_IMAGE} assert boundary.live["State"]["Running"] is True assert restored["artifacts"]["containers"][CONTAINER_NAME]["running"] is True def test_production_restore_rejects_unattested_candidate_before_compose_mutation( production_runtime, module ): runtime, boundary, project, model = production_runtime observation = runtime.observe(REPOSITORY, project, receipt_key=KEY) snapshot = runtime.snapshot( REPOSITORY, project, observation, REQUEST_ID, receipt_key=KEY ) snapshot.update( { "requestId": REQUEST_ID, "runtimeConfiguration": { "composeModel": copy.deepcopy(model), "containers": {CONTAINER_NAME: copy.deepcopy(boundary.live)}, "composeDirectory": str(boundary.source), }, "artifacts": copy.deepcopy(observation["artifacts"]), } ) boundary.live = _container(image_id=CANDIDATE_IMAGE, container_id="e" * 64) boundary.images[CANDIDATE_IMAGE]["Config"]["Labels"][ "org.opencontainers.image.revision" ] = "f" * 40 boundary.commands.clear() with pytest.raises( module.RegisteredLegacyAdoptionError, match="candidate|signed baseline|current registered", ): runtime.restore(REPOSITORY, project, snapshot, TARGET_SHA, receipt_key=KEY) assert boundary.compose_ups == 0 assert not any( command[:3] == ["docker", "compose", "--project-name"] for command in boundary.commands ) def _validation_api(path, *, query): if path == f"/repos/{REPOSITORY}/branches/main": return {"protected": True, "commit": {"id": TARGET_SHA}} if path == f"/repos/{REPOSITORY}/actions/runs": return { "total_count": 1, "workflow_runs": [ { "id": 7, "run_number": 19, "run_attempt": 1, "event": "push", "head_sha": TARGET_SHA, "head_branch": "main", "path": "managed-validation.yml@refs/heads/main", "created_at": "2026-10-07T12:00:00Z", "status": "completed", "conclusion": "success", "repository": {"full_name": REPOSITORY}, "head_repository": {"full_name": REPOSITORY}, "pull_requests": [], } ], } if path == f"/repos/{REPOSITORY}/actions/runs/7/jobs": return { "total_count": 1, "jobs": [ { "id": 9, "name": "full", "run_id": 7, "run_attempt": 1, "head_sha": TARGET_SHA, "head_branch": "main", "status": "completed", "conclusion": "success", } ], } raise AssertionError(f"unexpected validation request: {path} {query}") def _adopt_with_production_runtime(module, boundary, project, state_root, monkeypatch): original_validate = module.validate_registered_legacy_target def validate_temp_target(repository, registered_project): if ( repository == REPOSITORY and registered_project.get("slug") == "quiz-monitor" ): return "quiz-monitor" return original_validate(repository, registered_project) # The policy tests cover the fixed production source prefix separately. This # adapter test maps that already-validated registration to the temporary tree. monkeypatch.setattr( module, "validate_registered_legacy_target", validate_temp_target ) state_bytes = ( b'{"schema_version":3,"live_revision":"unknown","live_status":"unknown"}\n' ) (state_root / "quiz-monitor.json").write_bytes(state_bytes) def read_state(_repository, _project): return json.loads(state_bytes.decode("utf-8")), state_bytes runtime = module._ProductionRuntime(state_root) registry = {REPOSITORY: project} baseline = module.baseline( {"repository": REPOSITORY, "requestId": REQUEST_ID}, registry=registry, read_state=read_state, runtime=runtime, receipt_key=KEY, state_root=state_root, ) preflight = module.preflight( { "repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID, "expectedStateSha256": baseline["expectedStateSha256"], }, registry=registry, read_state=read_state, runtime=runtime, api_json=_validation_api, receipt_key=KEY, state_root=state_root, ) module.adopt( { "repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID, "expectedStateSha256": baseline["expectedStateSha256"], "preflightSha256": preflight["preflightSha256"], }, registry=registry, read_state=read_state, runtime=runtime, api_json=_validation_api, receipt_key=KEY, state_root=state_root, ) snapshot = module.capture_deployment_snapshot( REPOSITORY, project, json.loads(state_bytes.decode("utf-8")), request_id=REQUEST_ID, target_revision=TARGET_SHA, receipt_key=KEY, state_root=state_root, runtime=runtime, ) assert boundary.live["Image"] == BASELINE_IMAGE return runtime, registry, read_state, state_bytes, snapshot @pytest.fixture def adopted_production_runtime(production_runtime, tmp_path, monkeypatch): _runtime, boundary, project, model = production_runtime runtime, registry, read_state, state_bytes, snapshot = ( _adopt_with_production_runtime( boundary.module, boundary, project, tmp_path, monkeypatch, ) ) return ( runtime, boundary, project, model, registry, read_state, state_bytes, snapshot, ) def _prepare_candidate_overlay(module, boundary, project, tmp_path): bound_project = dict(project) bound_project.update( { "_registered_legacy_request_id": REQUEST_ID, "_registered_legacy_repository": REPOSITORY, } ) module.prepare_build_configuration( REPOSITORY, bound_project, REQUEST_ID, TARGET_SHA, copy.deepcopy(boundary.model), {}, { "APPOPS_SOURCE_REVISION": TARGET_SHA, "APPOPS_SOURCE_REPOSITORY": REPOSITORY, "APPOPS_SOURCE_TREE": "d" * 40, "APPOPS_BUILD_ID": "test-build", "APPOPS_BUILD_DATE": "2026-10-07T12:00:00Z", }, receipt_key=KEY, state_root=tmp_path, ) return bound_project def _candidate_deployment( module, runtime, boundary, project, registry, state_bytes, tmp_path, *, receipt_container_id=None, receipt_image_id=None, ): bound_project = _prepare_candidate_overlay(module, boundary, project, tmp_path) sealed = module.prepare_candidate_deployment( REPOSITORY, bound_project, REQUEST_ID, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) module.verify_candidate_pre_mutation( REPOSITORY, bound_project, REQUEST_ID, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) boundary.live = _container( image_id=CANDIDATE_IMAGE, container_id="e" * 64, hostname="candidate-generated-hostname", config_hash="candidate-config-hash", ) boundary.live["Config"]["Image"] = "quiz-monitor:latest" module.record_candidate_runtime( REPOSITORY, bound_project, REQUEST_ID, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) deployment_receipt = { "schema": "gitea-deploy-receipt.v1", "repository": REPOSITORY, "revision": TARGET_SHA, "containers": { CONTAINER_NAME: { "container_id": receipt_container_id or boundary.live["Id"], "image_id": receipt_image_id or CANDIDATE_IMAGE, } }, } live_binding = module.record_live_runtime( REPOSITORY, bound_project, REQUEST_ID, TARGET_SHA, deployment_receipt, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) artifacts = { "registered_legacy": { "requestId": REQUEST_ID, "revision": TARGET_SHA, **live_binding, }, "containers": { CONTAINER_NAME: { "container_id": boundary.live["Id"], "image_id": CANDIDATE_IMAGE, } }, } controller_state = { "live_status": "healthy", "live_revision": TARGET_SHA, "live_artifacts": artifacts, "live_receipt": {"containers": copy.deepcopy(artifacts["containers"])}, } live = module.verify_current_live_runtime( REPOSITORY, dict(project), controller_state, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) assert sealed["candidateSha256"] assert live["revision"] == TARGET_SHA return live, controller_state def test_candidate_and_live_runtime_are_bound_to_exact_production_observation( adopted_production_runtime, tmp_path ): ( runtime, boundary, project, _model, registry, _read_state, state_bytes, snapshot, ) = adopted_production_runtime module = boundary.module live, controller_state = _candidate_deployment( module, runtime, boundary, project, registry, state_bytes, tmp_path ) assert live["containers"] == { CONTAINER_NAME: { "containerId": "e" * 64, "imageId": CANDIDATE_IMAGE, "service": "quiz-server", } } assert live["imageIDs"] == {CONTAINER_NAME: CANDIDATE_IMAGE} assert ( live["runtimeFingerprint"] == controller_state["live_artifacts"]["registered_legacy"]["runtimeFingerprint"] ) assert snapshot["registered_legacy"]["snapshotSha256"] def test_restart_verification_reconstructs_signed_overlay_from_registry_project( adopted_production_runtime, tmp_path ): ( runtime, boundary, project, _model, registry, _read_state, state_bytes, _snapshot, ) = adopted_production_runtime module = boundary.module _live, controller_state = _candidate_deployment( module, runtime, boundary, project, registry, state_bytes, tmp_path ) fresh_project = dict(project) assert not any(name.startswith("_registered_") for name in fresh_project) overlay_path = module._paths(tmp_path, project["slug"], REQUEST_ID)["overlay_file"] before = len(boundary.compose_config_commands) verified = module.verify_current_live_runtime( REPOSITORY, fresh_project, controller_state, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) assert verified["revision"] == TARGET_SHA observed_commands = boundary.compose_config_commands[before:] assert observed_commands assert any(str(overlay_path) in command for command in observed_commands) def test_candidate_image_must_carry_the_exact_registered_revision( adopted_production_runtime, tmp_path ): ( runtime, boundary, project, _model, _registry, _read_state, _state_bytes, _snapshot, ) = adopted_production_runtime module = boundary.module boundary.images["quiz-monitor:latest"]["Config"]["Labels"][ "org.opencontainers.image.revision" ] = "f" * 40 with pytest.raises( module.RegisteredLegacyAdoptionError, match="exact repository revision provenance", ): module.prepare_candidate_deployment( REPOSITORY, project, REQUEST_ID, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) candidate_file = ( tmp_path / "registered-legacy-adoption" / "quiz-monitor" / f"{REQUEST_ID}.candidate.json" ) assert not candidate_file.exists() def test_candidate_sealing_rejects_runtime_drift_after_build_before_mutation( adopted_production_runtime, tmp_path ): ( runtime, boundary, project, _model, _registry, _read_state, _state_bytes, _snapshot, ) = adopted_production_runtime module = boundary.module boundary.live["Id"] = "f" * 64 boundary.commands.clear() with pytest.raises( module.RegisteredLegacyAdoptionError, match="baseline container runtime changed", ): module.prepare_candidate_deployment( REPOSITORY, project, REQUEST_ID, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) candidate_file = ( tmp_path / "registered-legacy-adoption" / "quiz-monitor" / f"{REQUEST_ID}.candidate.json" ) assert not candidate_file.exists() assert boundary.compose_ups == 0 def test_pre_mutation_cas_rejects_runtime_drift_after_candidate_is_sealed( adopted_production_runtime, tmp_path ): ( runtime, boundary, project, _model, _registry, _read_state, _state_bytes, _snapshot, ) = adopted_production_runtime module = boundary.module bound_project = _prepare_candidate_overlay(module, boundary, project, tmp_path) module.prepare_candidate_deployment( REPOSITORY, bound_project, REQUEST_ID, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) boundary.live["HostConfig"]["SecurityOpt"] = ["seccomp=unconfined"] boundary.commands.clear() with pytest.raises(module.RegisteredLegacyAdoptionError): module.verify_candidate_pre_mutation( REPOSITORY, bound_project, REQUEST_ID, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) assert boundary.compose_ups == 0 assert not any( command[:3] == ["docker", "compose", "--project-name"] and command[-1:] == ["--force-recreate"] for command in boundary.commands ) @pytest.mark.parametrize("drift", ["container_id", "image_id", "environment", "mount"]) def test_signed_live_runtime_drift_is_rejected_before_restore_mutation( adopted_production_runtime, tmp_path, drift ): ( runtime, boundary, project, _model, registry, _read_state, state_bytes, snapshot, ) = adopted_production_runtime module = boundary.module live, _controller_state = _candidate_deployment( module, runtime, boundary, project, registry, state_bytes, tmp_path ) if drift == "container_id": boundary.live["Id"] = "f" * 64 elif drift == "image_id": boundary.live["Image"] = "sha256:" + "9" * 64 boundary.images[boundary.live["Image"]] = { "Id": boundary.live["Image"], "Config": { "Labels": { "org.opencontainers.image.revision": TARGET_SHA, "org.opencontainers.image.source": REPOSITORY, } }, } elif drift == "environment": boundary.live["Config"]["Env"][1] = "QUIZ_TEMPLATE=changed" elif drift == "mount": boundary.live["Mounts"][0]["Source"] = "/other/config" boundary.commands.clear() with pytest.raises(module.RegisteredLegacyAdoptionError): module.restore_deployment_snapshot( REPOSITORY, project, {**snapshot, "candidateSha256": live["candidateSha256"]}, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, expected_current=live, ) assert boundary.compose_ups == 0 assert not any( command[-1:] == ["--force-recreate"] for command in boundary.commands ) @pytest.mark.parametrize("mismatch", ["container_id", "image_id"]) def test_live_receipt_must_match_sealed_candidate_container_identity( adopted_production_runtime, tmp_path, mismatch ): ( runtime, boundary, project, _model, _registry, _read_state, _state_bytes, _snapshot, ) = adopted_production_runtime module = boundary.module bound_project = _prepare_candidate_overlay(module, boundary, project, tmp_path) module.prepare_candidate_deployment( REPOSITORY, bound_project, REQUEST_ID, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) boundary.live = _container( image_id=CANDIDATE_IMAGE, container_id="e" * 64, hostname="candidate-generated-hostname", config_hash="candidate-config-hash", ) boundary.live["Config"]["Image"] = "quiz-monitor:latest" receipt_container_id = boundary.live["Id"] receipt_image_id = CANDIDATE_IMAGE if mismatch == "container_id": receipt_container_id = "f" * 64 else: receipt_image_id = BASELINE_IMAGE deployment_receipt = { "schema": "gitea-deploy-receipt.v1", "repository": REPOSITORY, "revision": TARGET_SHA, "containers": { CONTAINER_NAME: { "container_id": receipt_container_id, "image_id": receipt_image_id, } }, } with pytest.raises( module.RegisteredLegacyAdoptionError, match="sealed candidate image", ): module.record_live_runtime( REPOSITORY, bound_project, REQUEST_ID, TARGET_SHA, deployment_receipt, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) live_file = ( tmp_path / "registered-legacy-adoption" / "quiz-monitor" / f"{REQUEST_ID}.live.json" ) assert not live_file.exists() assert boundary.compose_ups == 0 def test_restore_rechecks_verified_candidate_cas_before_force_recreate( adopted_production_runtime, tmp_path ): ( runtime, boundary, project, _model, _registry, _read_state, state_bytes, snapshot, ) = adopted_production_runtime module = boundary.module live, controller_state = _candidate_deployment( module, runtime, boundary, project, _registry, state_bytes, tmp_path ) # A separate read verifies the signed HMAC receipt and controller's per-container IDs. expected_current = module.verify_current_live_runtime( REPOSITORY, project, controller_state, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) boundary.commands.clear() restored = module.restore_deployment_snapshot( REPOSITORY, project, {**snapshot, "candidateSha256": live["candidateSha256"]}, TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, expected_current=expected_current, ) assert boundary.compose_ups == 1 assert restored["imageIDs"] == {CONTAINER_NAME: BASELINE_IMAGE} assert boundary.live["Image"] == BASELINE_IMAGE assert boundary.live["Config"]["Env"] == [ "OPENRGB_HOST=198.19.1.2", f"QUIZ_TEMPLATE={TEMPLATE_VALUE}", ] assert boundary.live["Mounts"][0]["Source"] == CONFIG_BIND assert ( boundary.live["NetworkSettings"]["Ports"]["8000/tcp"][0]["HostPort"] == "1205" )