Files
Unraid-AppOps-Gateway-Public/backend/tests/test_registered_legacy_runtime.py
T

1185 lines
37 KiB
Python

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"
)