1185 lines
37 KiB
Python
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"
|
|
)
|