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

3908 lines
174 KiB
Python

from __future__ import annotations
import ast
import hashlib
import importlib.util
import io
import json
import os
import stat
import subprocess
import sys
import tarfile
import threading
import types
from contextlib import contextmanager, nullcontext
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location(
"controller_hub_volume", ROOT / "scripts/prepare_controller_hub_release_volume.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
REVISION = "a" * 40
@pytest.mark.skipif(os.name != "posix", reason="requires real POSIX child/lock contention")
def test_native_probe_cleanup_child_can_wait_beyond_default_command_deadline(monkeypatch, tmp_path):
import fcntl
real_run = subprocess.run
observed = []
def scaled_run(arguments, **options):
observed.append(options["timeout"])
# Keep the real deadline ratio, with a real child and kernel flock;
# the original 45-second budget expires before this owner releases.
options["timeout"] = options["timeout"] / 200
return real_run(arguments, **options)
monkeypatch.setattr(module.subprocess, "run", scaled_run)
path = tmp_path / "probe.lock"
with path.open("w") as owner:
fcntl.flock(owner.fileno(), fcntl.LOCK_EX)
release = threading.Timer(0.4, lambda: fcntl.flock(owner.fileno(), fcntl.LOCK_UN))
release.start()
try:
output = module._run([
sys.executable, "-c",
("import fcntl,sys; f=open(sys.argv[1],'a'); "
"fcntl.flock(f.fileno(),fcntl.LOCK_EX); print('released')"),
str(path),
], max_output=4096, diagnostic_action="probe_cleanup")
finally:
release.join(timeout=2)
assert output == b"released\n"
assert observed == [300]
@pytest.mark.parametrize("action", [None, "writer_copy", "compose_stop"])
def test_unrelated_promotion_commands_keep_existing_deadline(monkeypatch, action):
observed = []
def run(*_, **options):
observed.append(options["timeout"])
return subprocess.CompletedProcess([], 0, b"", b"")
monkeypatch.setattr(module.subprocess, "run", run)
module._run(["fixed-command"], diagnostic_action=action)
assert observed == [45]
OLD_REVISION = "c" * 40
IMAGE_ID = "sha256:" + "b" * 64
ORIGINAL_COMPOSE = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: immutable-controller\n"
@pytest.fixture(autouse=True)
def compose_config_root(monkeypatch, tmp_path):
# Production uses the fixed /tmp tmpfs; tests never write outside their tree.
monkeypatch.setattr(module, "_COMPOSE_CONFIG_ROOT", tmp_path)
@pytest.mark.parametrize("operation", ["config", "stop", "up"])
@pytest.mark.parametrize("source_directory", [False, True])
def test_compose_uses_private_ephemeral_cli_config_with_readonly_root(
monkeypatch, tmp_path, operation, source_directory,
):
directories = []
def run(arguments, *, data=None, diagnostic_action=None, **kwargs):
# Compose up attempts to mkdir /root/.docker even for a dry run. The
# controller promoter must keep its root filesystem read-only.
assert arguments[:2] == ["docker", "--config"]
directory = Path(arguments[2])
directories.append(directory)
assert directory.parent == tmp_path
assert directory.name.startswith("itworx-controller-compose-")
assert directory.is_dir() and not directory.is_symlink()
if os.name == "posix":
assert stat.S_IMODE(directory.stat().st_mode) == 0o700
assert directory.stat().st_uid == os.getuid()
assert arguments[3] == "compose"
assert diagnostic_action == f"compose_{operation}"
assert data == b"captured-private-compose"
assert arguments[-1] == operation
assert arguments[arguments.index("--env-file") + 1] == "/dev/null"
assert arguments[arguments.index("-f") + 1] == "-"
assert ("--profile" in arguments) is (source_directory and operation == "config")
if "--profile" in arguments:
assert arguments[arguments.index("--profile") + 1] == "runner"
(directory / "generated-client-state").write_bytes(b"private-client-state")
return b"ok"
monkeypatch.setattr(module, "_run", run)
for _ in range(2):
assert module._compose([operation], b"captured-private-compose", source_directory=source_directory) == b"ok"
assert directories[0] != directories[1]
assert all(not directory.exists() for directory in directories)
def test_compose_private_cli_config_is_cleaned_after_command_failure(monkeypatch):
directories = []
failure = module.VolumeTransitionError(
"controller_volume_command_failed",
command_failure=("compose_up", "exit_1", "mount_read_only"),
)
def run(arguments, **kwargs):
directory = Path(arguments[2])
directories.append(directory)
(directory / "client-state").write_bytes(b"private-state")
raise failure
monkeypatch.setattr(module, "_run", run)
with pytest.raises(module.VolumeTransitionError) as caught:
module._compose(["up"], b"private-compose")
assert caught.value is failure
assert directories and all(not directory.exists() for directory in directories)
@pytest.mark.parametrize("action", ["compose_config", "compose_stop", "compose_up", "image_inspect"])
def test_compose_boundary_diagnostic_never_discloses_stderr(monkeypatch, action):
stderr = b"mkdir /root/.docker: read-only file system fixture-secret-token"
monkeypatch.setattr(module.subprocess, "run", lambda *args, **kwargs: types.SimpleNamespace(
returncode=1, stdout=b"", stderr=stderr,
))
with pytest.raises(module.VolumeTransitionError) as caught:
module._run(["docker", "fixed-operation"], diagnostic_action=action)
diagnostic = module._safe_command_diagnostic(caught.value.command_failure)
assert diagnostic == f"{action}_exit_1_mount_read_only"
assert "fixture-secret-token" not in diagnostic
assert "/root/.docker" not in diagnostic
# Frozen historical layouts; these fixtures must not follow the acceptance registry.
HISTORICAL_PRE_CANDIDATE_RELEASE_FILES = (
"controller_paths.py", "mcp_hub_release_store.py", "deploy.py",
"managed_appdata_paths.py", "prepare_managed_appdata_root.py", "runner_fleet.py",
"runner_capacity_controller.py", "runner_capacity_policy.py",
"runner_fleet_upgrade_v2.py", "status_server.py",
"repopulse_adoption_store.py", "repopulse_registry_restore.py",
"repopulse_adoption_executor.py", "repopulse_adoption_effects.py",
"repopulse_adoption_storage_effects.py", "repopulse_target_attestation.py",
"stage_repopulse_target_attestation.py", "repopulse_adoption_cli.py",
"stage_repopulse_maintenance_environment.py", "run_repopulse_adoption.py",
)
HISTORICAL_CANDIDATE_HELPER_RELEASE_FILES = (
"controller_paths.py", "mcp_hub_release_store.py", "deploy.py",
"managed_appdata_paths.py", "prepare_managed_appdata_root.py", "runner_fleet.py",
"runner_capacity_controller.py", "runner_capacity_policy.py",
"runner_fleet_upgrade_v2.py", "status_server.py", "prepare_mcp_hub_candidate.py",
"repopulse_adoption_store.py", "repopulse_registry_restore.py",
"repopulse_adoption_executor.py", "repopulse_adoption_effects.py",
"repopulse_adoption_storage_effects.py", "repopulse_target_attestation.py",
"stage_repopulse_target_attestation.py", "repopulse_adoption_cli.py",
"stage_repopulse_maintenance_environment.py", "run_repopulse_adoption.py",
)
HISTORICAL_CONFIG_ROOT_RELEASE_FILES = (
"controller_paths.py", "mcp_hub_release_store.py", "hub_permission_preparation.py",
"deploy.py", "managed_appdata_paths.py", "prepare_managed_appdata_root.py", "runner_fleet.py",
"runner_capacity_controller.py", "runner_capacity_policy.py", "runner_fleet_upgrade_v2.py",
"runner_profiles.py", "runner_admission_policy.py", "runner_admission_controller.py",
"runner_resource_envelope.py", "runner_fleet_specialize.py", "runner_statistics_binding.py",
"status_server.py", "repopulse_adoption_store.py", "repopulse_registry_restore.py",
"repopulse_adoption_executor.py", "repopulse_adoption_effects.py",
"repopulse_adoption_storage_effects.py", "repopulse_target_attestation.py",
"stage_repopulse_target_attestation.py", "repopulse_adoption_cli.py",
"stage_repopulse_maintenance_environment.py", "run_repopulse_adoption.py",
)
def volume():
return {
"Name": module.VOLUME,
"Driver": "local",
"Scope": "local",
"Options": None,
"Labels": dict(module.LABELS),
"Mountpoint": module.MOUNTPOINT.as_posix(),
}
@pytest.mark.parametrize(
("key", "value"),
[
("Name", "operator-volume"),
("Driver", "nfs"),
("Scope", "global"),
("Options", {"device": "/mnt/cache/appdata"}),
("Labels", {}),
("Mountpoint", "/mnt/cache/unsafe"),
],
)
def test_unknown_or_redirected_volume_is_never_adopted(key, value):
item = volume()
item[key] = value
with pytest.raises(module.VolumeTransitionError, match="identity_invalid"):
module.validate_volume(item)
def test_only_the_exact_local_volume_is_accepted():
module.validate_volume(volume())
def backup_volume():
return {
"Name": "itworx-forge-master-key-backup",
"Driver": "local",
"Scope": "local",
"Options": None,
"Labels": {
"itworx.managed-by": "deployment-controller",
"itworx.purpose": "forge-master-key-backup",
},
"Mountpoint": "/var/lib/docker/volumes/itworx-forge-master-key-backup/_data",
}
def test_backup_volume_identity_is_exact_and_never_repairs_existing_metadata():
module.validate_backup_volume(backup_volume())
bad = backup_volume()
bad["Labels"]["itworx.purpose"] = "foreign"
with pytest.raises(module.VolumeTransitionError):
module.validate_backup_volume(bad)
def test_promotion_preparer_creates_only_the_fixed_backup_volume_if_absent(monkeypatch, tmp_path):
commands = []
present = False
def run(arguments, **_kwargs):
nonlocal present
commands.append(arguments)
if arguments == ["docker", "info", "--format", "{{json .DockerRootDir}}"]:
return b'"/var/lib/docker"'
if arguments == ["docker", "volume", "ls", "--format", "{{.Name}}"]:
return b"" if not present else b"itworx-forge-master-key-backup\n"
if arguments[:3] == ["docker", "volume", "create"]:
present = True
return b"itworx-forge-master-key-backup\n"
if arguments == ["docker", "volume", "inspect", "itworx-forge-master-key-backup"]:
return json.dumps([backup_volume()]).encode()
raise AssertionError(arguments)
directory_calls = []
monkeypatch.setattr(module, "_run", run)
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
def directory(path, anchor):
directory_calls.append((path, anchor))
return 23
monkeypatch.setattr(module, "_directory", directory)
module.ensure_backup_volume(create=True)
create = [arguments for arguments in commands if arguments[:3] == ["docker", "volume", "create"]]
assert create == [[
"docker", "volume", "create", "--driver", "local",
"--label", "itworx.managed-by=deployment-controller",
"--label", "itworx.purpose=forge-master-key-backup",
"itworx-forge-master-key-backup",
]]
assert directory_calls == [
(module.DOCKER_ROOT / "volumes", module.DOCKER_ROOT),
(module.BACKUP_MOUNTPOINT, module.DOCKER_ROOT),
]
def test_existing_backup_volume_is_validated_without_relabel_or_recreation(monkeypatch, tmp_path):
commands = []
def run(arguments, **_kwargs):
commands.append(arguments)
if arguments == ["docker", "info", "--format", "{{json .DockerRootDir}}"]:
return b'"/var/lib/docker"'
if arguments == ["docker", "volume", "ls", "--format", "{{.Name}}"]:
return b"itworx-forge-master-key-backup\n"
if arguments == ["docker", "volume", "inspect", "itworx-forge-master-key-backup"]:
item = backup_volume()
item["CreatedAt"] = "2000-01-01T00:00:00Z"
return json.dumps([item]).encode()
raise AssertionError(arguments)
monkeypatch.setattr(module, "_run", run)
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
monkeypatch.setattr(
module, "_directory",
lambda _path, _anchor: 23,
)
module.ensure_backup_volume(create=False)
assert not any(arguments[:3] == ["docker", "volume", "create"] for arguments in commands)
def test_missing_backup_volume_is_not_created_outside_promotion(monkeypatch, tmp_path):
commands = []
def run(arguments, **_kwargs):
commands.append(arguments)
if arguments == ["docker", "info", "--format", "{{json .DockerRootDir}}"]:
return b'"/var/lib/docker"'
if arguments == ["docker", "volume", "ls", "--format", "{{.Name}}"]:
return b""
raise AssertionError(arguments)
monkeypatch.setattr(module, "_run", run)
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
monkeypatch.setattr(
module, "_directory",
lambda _path, _anchor: 23,
)
with pytest.raises(module.VolumeTransitionError, match="backup_volume_missing"):
module.ensure_backup_volume(create=False)
assert not any(arguments[:3] == ["docker", "volume", "create"] for arguments in commands)
def _prepare_promotion_doubles(monkeypatch, tmp_path, *, config_valid=True):
events = []
controller_id = "c" * 64
current = {
"Id": controller_id,
"Mounts": [{
"Destination": module.EXECUTABLE_TARGET,
"Source": module.CONTROLLER_DIR.as_posix(),
}],
}
previous_identity = {
"sourceRevision": "b" * 40,
"sourceTree": "c" * 40,
"controllerVersion": "2.1.0",
}
config_tree = ({}, {"": 0o755})
monkeypatch.setattr(module, "_validate_private_stage", lambda _stage: events.append("stage_validated"))
monkeypatch.setattr(module, "ensure_volume", lambda **_kwargs: None)
monkeypatch.setattr(
module, "_controller_image_inspect",
lambda _image_id: (events.append("controller_inspected") or current),
)
monkeypatch.setattr(
module, "_snapshot_attested_controller",
lambda *_args, **_kwargs: ({}, {"": 0o755}, previous_identity),
)
monkeypatch.setattr(module, "_validate_previous_private_tls", lambda _stage: None)
monkeypatch.setattr(module, "_release_tree", lambda *_args, **_kwargs: ({}, {"": 0o755}))
monkeypatch.setattr(module, "_config_tree", lambda *_args, **_kwargs: config_tree)
monkeypatch.setattr(module, "_read_config_leaf", lambda: config_tree)
monkeypatch.setattr(module, "_prepare_merged_tree", lambda *_args, **_kwargs: ({}, {"": 0o755}))
def validate_staged(*_args, **_kwargs):
events.append("config_validated")
if not config_valid:
raise module.VolumeTransitionError("staged_config_invalid")
monkeypatch.setattr(module, "_validate_staged_configuration", validate_staged)
monkeypatch.setattr(
module, "_install_release_leaf",
lambda revision, *_args, **_kwargs: events.append(f"release_installed:{revision}"),
)
monkeypatch.setattr(module, "_read_root_compose", lambda: (ORIGINAL_COMPOSE, 0o644))
monkeypatch.setattr(module, "_mirror_snapshot_tar", lambda: b"mirror")
monkeypatch.setattr(
module, "rendered_model",
lambda *_args, **_kwargs: (events.append("compose_rendered") or b"rendered-compose"),
)
monkeypatch.setattr(
module, "_verify_compose_image",
lambda *_args, **_kwargs: events.append("compose_verified"),
)
monkeypatch.setattr(
module, "_verify_promotion_source_attestation",
lambda *_args, **_kwargs: events.append("source_attested"),
)
monkeypatch.setattr(
module, "ensure_backup_volume",
lambda **_kwargs: events.append("backup_volume_prepared"),
raising=False,
)
monkeypatch.setattr(module, "_release_tar_bytes", lambda *_args, **_kwargs: b"tar")
backup = tmp_path / "promotion-backup"
backup.mkdir(mode=0o700)
backup_fd = 23
monkeypatch.setattr(module, "_create_backup", lambda: (backup, backup_fd))
monkeypatch.setattr(module, "_write_backup", lambda *_args, **_kwargs: None)
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
return events
def test_promotion_prepares_backup_volume_after_source_stage_controller_and_config_checks(
monkeypatch, tmp_path
):
events = _prepare_promotion_doubles(monkeypatch, tmp_path)
module.prepare_promotion(
tmp_path / "stage",
source_revision="d" * 40,
source_tree="e" * 40,
controller_version="2.2.0",
image_id=IMAGE_ID,
)
assert events.index("stage_validated") < events.index("config_validated")
assert events.index("config_validated") < events.index("backup_volume_prepared")
assert events.index("compose_rendered") < events.index("backup_volume_prepared")
assert events.count("compose_verified") == 2
assert events.index("compose_verified") < events.index("backup_volume_prepared")
assert events.count("source_attested") == 4
attestation_after_backup = events.index(
"source_attested", events.index("backup_volume_prepared") + 1
)
assert attestation_after_backup < events.index("release_installed:" + "b" * 40)
def test_promotion_rejects_config_leaf_change_during_staged_validation(monkeypatch, tmp_path):
events = _prepare_promotion_doubles(monkeypatch, tmp_path)
original_config = ({}, {"": 0o755})
changed_config = ({"changed.json": (b"changed", 0o600)}, {"": 0o755})
reads = 0
def read_config():
nonlocal reads
reads += 1
if reads == 1:
return original_config
events.append("config_changed")
return changed_config
monkeypatch.setattr(module, "_read_config_leaf", read_config)
with pytest.raises(module.VolumeTransitionError, match="controller_volume_config_changed"):
module.prepare_promotion(
tmp_path / "stage",
source_revision="d" * 40,
source_tree="e" * 40,
controller_version="2.2.0",
image_id=IMAGE_ID,
)
assert reads == 2
assert events.index("config_validated") < events.index("config_changed")
assert not any(event.startswith("release_installed:") for event in events)
def test_promotion_rejects_legacy_config_source_change_before_config_install(monkeypatch, tmp_path):
events = _prepare_promotion_doubles(monkeypatch, tmp_path)
monkeypatch.setattr(module, "_read_config_leaf", lambda: None)
monkeypatch.setattr(
module, "_config_tree",
lambda entries, _directories, **_kwargs: (
({"changed.json": (b"changed", 0o600)}, {"": 0o755})
if "changed.json" in entries else ({}, {"": 0o755})
),
)
identity = {
"sourceRevision": "b" * 40,
"sourceTree": "c" * 40,
"controllerVersion": "2.1.0",
}
monkeypatch.setattr(
module, "_capture_controller_source",
lambda *_args: ({"changed.json": (b"changed", 0o600)}, {"": 0o755}, identity),
)
with pytest.raises(module.VolumeTransitionError, match="controller_volume_config_changed"):
module.prepare_promotion(
tmp_path / "stage",
source_revision="d" * 40,
source_tree="e" * 40,
controller_version="2.2.0",
image_id=IMAGE_ID,
)
assert not any(event.startswith("release_installed:") for event in events)
def test_promotion_does_not_prepare_backup_volume_when_staged_configuration_fails(
monkeypatch, tmp_path
):
events = _prepare_promotion_doubles(monkeypatch, tmp_path, config_valid=False)
with pytest.raises(module.VolumeTransitionError, match="staged_config_invalid"):
module.prepare_promotion(
tmp_path / "stage",
source_revision="d" * 40,
source_tree="e" * 40,
controller_version="2.2.0",
image_id=IMAGE_ID,
)
assert "backup_volume_prepared" not in events
@pytest.mark.parametrize(
("failed_step", "expected_phase", "error_code"),
[
("old_tls", "old_tls", "controller_volume_private_tls_unsafe"),
("config", "config", "controller_volume_config_missing"),
("release_build", "release_build", "controller_volume_source_archive_incomplete"),
("staged_config", "staged_config", "controller_volume_command_failed"),
("old_release", "old_release", "controller_volume_writer_failed"),
("new_release", "new_release", "controller_volume_writer_failed"),
],
)
def test_prepare_reports_only_fixed_failure_phase_and_code(
monkeypatch, tmp_path, capsys, failed_step, expected_phase, error_code
):
_prepare_promotion_doubles(monkeypatch, tmp_path)
def fail(*_args, **_kwargs):
raise module.VolumeTransitionError(error_code)
if failed_step == "old_tls":
monkeypatch.setattr(module, "_validate_previous_private_tls", fail)
elif failed_step == "config":
monkeypatch.setattr(module, "_read_config_leaf", fail)
elif failed_step == "release_build":
monkeypatch.setattr(module, "_prepare_merged_tree", fail)
elif failed_step == "staged_config":
monkeypatch.setattr(module, "_validate_staged_configuration", fail)
else:
installed = 0
def install(*_args, **_kwargs):
nonlocal installed
installed += 1
if installed == (1 if failed_step == "old_release" else 2):
fail()
monkeypatch.setattr(module, "_install_release_leaf", install)
monkeypatch.setattr(sys, "argv", [
"preparer", "prepare", "--stage-root", str(tmp_path / "stage"),
"--source-revision", "d" * 40, "--source-tree", "e" * 40,
"--controller-version", "2.2.0", "--image-id", IMAGE_ID,
])
with pytest.raises(SystemExit) as failure:
module.main()
assert str(failure.value) == (
"controller protected Hub volume transition failed "
f"phase={expected_phase} code={error_code}"
)
assert capsys.readouterr().out == ""
@pytest.mark.parametrize("error", [
module.VolumeTransitionError("controller_volume_writer_failed\nTOKEN=private"),
module.VolumeTransitionError("controller_volume_unreviewed_code"),
ValueError("TOKEN=private"),
])
def test_prepare_diagnostic_suppresses_untrusted_error_and_phase(monkeypatch, tmp_path, error):
def forged_prepare(*_args, **kwargs):
kwargs["on_phase"]("old_release\nTOKEN=private")
raise error
monkeypatch.setattr(module, "prepare_promotion", forged_prepare)
monkeypatch.setattr(sys, "argv", [
"preparer", "prepare", "--stage-root", str(tmp_path / "stage"),
"--source-revision", "d" * 40, "--source-tree", "e" * 40,
"--controller-version", "2.2.0", "--image-id", IMAGE_ID,
])
with pytest.raises(SystemExit) as failure:
module.main()
assert str(failure.value) == (
"controller protected Hub volume transition failed phase=prepare code=unclassified"
)
@pytest.mark.parametrize(
("stderr", "expected_condition"),
[
(b"container rootfs is marked read-only at /private/path TOKEN=fixture-secret",
"rootfs_read_only"),
(b"read-only file system at /private/path TOKEN=fixture-secret", "mount_read_only"),
(b"permission denied: /private/path TOKEN=fixture-secret", "permission_denied"),
(b"no such file or directory: /private/path TOKEN=fixture-secret",
"destination_missing"),
(b"No such container: private-container-id TOKEN=fixture-secret", "container_missing"),
(b"client version is too old TOKEN=fixture-secret", "api_version_unsupported"),
(b"unrecognized failure TOKEN=fixture-secret", None),
],
)
def test_writer_command_failure_is_categorized_without_retaining_stderr(
monkeypatch, stderr, expected_condition
):
monkeypatch.setattr(module.subprocess, "run", lambda *_args, **_kwargs: types.SimpleNamespace(
returncode=1, stdout=b"", stderr=stderr,
))
with pytest.raises(module.VolumeTransitionError) as failure:
module._run(
["docker", "cp", "-", "container:/private/path"],
data=b"TOKEN=fixture-secret",
diagnostic_action="writer_copy",
)
assert failure.value.args == ("controller_volume_command_failed",)
assert failure.value.command_failure == (
"writer_copy", "exit_1", expected_condition,
)
diagnostic = module._safe_command_diagnostic(failure.value.command_failure)
assert diagnostic is not None
assert "fixture-secret" not in repr(failure.value)
assert "fixture-secret" not in diagnostic
assert "private/path" not in diagnostic
@pytest.mark.parametrize(
("failure_kind", "expected_outcome"),
[("timeout", "timeout"), ("spawn", "spawn_failed"), ("overflow", "output_limit")],
)
def test_writer_command_failure_diagnostics_bound_non_exit_outcomes(
monkeypatch, failure_kind, expected_outcome
):
def run(*_args, **_kwargs):
if failure_kind == "timeout":
raise subprocess.TimeoutExpired(cmd=["docker", "cp"], timeout=45,
stderr=b"TOKEN=fixture-secret")
if failure_kind == "spawn":
raise OSError("TOKEN=fixture-secret")
return types.SimpleNamespace(returncode=0, stdout=b"oversized", stderr=b"")
monkeypatch.setattr(module.subprocess, "run", run)
kwargs = {"max_output": 1} if failure_kind == "overflow" else {}
with pytest.raises(module.VolumeTransitionError) as failure:
module._run(["docker", "cp", "-", "container:/target"],
diagnostic_action="writer_copy", **kwargs)
assert failure.value.command_failure == ("writer_copy", expected_outcome, None)
def test_prepare_reports_only_allowlisted_command_and_stderr_conditions(monkeypatch, tmp_path):
def failed_prepare(*_args, **kwargs):
kwargs["on_phase"]("old_release")
raise module.VolumeTransitionError(
"controller_volume_command_failed",
command_failure=("writer_copy", "exit_1", "rootfs_read_only"),
cleanup_failure=("writer_cleanup", "exit_2", "permission_denied"),
)
monkeypatch.setattr(module, "prepare_promotion", failed_prepare)
monkeypatch.setattr(sys, "argv", [
"preparer", "prepare", "--stage-root", str(tmp_path / "stage"),
"--source-revision", "d" * 40, "--source-tree", "e" * 40,
"--controller-version", "2.2.0", "--image-id", IMAGE_ID,
])
with pytest.raises(SystemExit) as failure:
module.main()
assert str(failure.value) == (
"controller protected Hub volume transition failed phase=old_release "
"code=controller_volume_command_failed "
"diagnostic=writer_copy_exit_1_rootfs_read_only "
"cleanupDiagnostic=writer_cleanup_exit_2_permission_denied"
)
@pytest.mark.parametrize("value", [
("docker cp /private/file", "exit_1", None),
("writer_copy", "exit_1", "TOKEN=fixture-secret"),
("writer_copy", "exit_999", None),
("writer_copy", "exit_1", "unknown_condition"),
["writer_copy", "exit_1", None],
])
def test_command_diagnostic_rejects_unreviewed_fields(value):
assert module._safe_command_diagnostic(value) is None
def test_release_writer_cleanup_failure_preserves_primary_copy_failure(monkeypatch):
partial, expected = _release_with_config()
monkeypatch.setattr(module, "_read_release_leaf", lambda _revision: partial)
monkeypatch.setattr(module, "ensure_volume", lambda **_kwargs: None)
monkeypatch.setattr(module, "_remove_stale_writer", lambda _image_id: None)
monkeypatch.setattr(module, "_inspect_writer", lambda *_args, **_kwargs: {})
calls = []
writer_id = "f" * 64
def run(arguments, *, diagnostic_action=None, **_kwargs):
calls.append(diagnostic_action)
if diagnostic_action == "writer_create":
return writer_id.encode()
if diagnostic_action == "writer_wait":
return b"0"
if diagnostic_action == "writer_copy":
raise module.VolumeTransitionError(
"controller_volume_command_failed",
command_failure=("writer_copy", "exit_1", "rootfs_read_only"),
)
if diagnostic_action == "writer_cleanup":
raise module.VolumeTransitionError(
"controller_volume_command_failed",
command_failure=("writer_cleanup", "exit_2", "permission_denied"),
)
return b""
monkeypatch.setattr(module, "_run", run)
with pytest.raises(module.VolumeTransitionError) as failure:
module._install_release_leaf(REVISION, expected, IMAGE_ID)
assert failure.value.command_failure == (
"writer_copy", "exit_1", "rootfs_read_only",
)
assert failure.value.cleanup_failure == (
"writer_cleanup", "exit_2", "permission_denied",
)
assert calls[-2:] == ["writer_copy", "writer_cleanup"]
def writer_inventory(inherited_labels):
identifier = "c" * 64
spec = module._writer_spec(IMAGE_ID, REVISION, kind="config")
return {
"Id": identifier, "Name": f"/{module.WRITER_NAME}", "Image": IMAGE_ID,
"Config": {
"Image": IMAGE_ID, "User": "0:0", "Entrypoint": ["python3"], "Cmd": spec[-2:],
"Labels": {**inherited_labels, module.WRITER_LABEL: "true",
module.WRITER_TARGET_LABEL: "config", module.WRITER_REVISION_LABEL: REVISION},
},
"HostConfig": {
"NetworkMode": "none", "Binds": [], "Privileged": False, "ReadonlyRootfs": True,
"CpuQuota": module.WRITER_CPU_QUOTA, "CpuPeriod": module.WRITER_CPU_PERIOD,
"Memory": module.WRITER_MEMORY_BYTES, "PidsLimit": module.WRITER_PIDS_LIMIT,
"CapDrop": ["ALL"], "Tmpfs": {"/tmp": "rw,nosuid,nodev,size=16777216,mode=1777"},
"SecurityOpt": ["no-new-privileges:true"],
},
"Mounts": [{"Type": "volume", "Name": module.VOLUME,
"Source": module.MOUNTPOINT.as_posix(), "Destination": module.TARGET, "RW": True}],
}
@pytest.mark.parametrize("damage", [None, "changed_image_label", "extra_label", "reserved_image_label", "falsey_labels"])
def test_writer_labels_are_bound_to_exact_image_inheritance(monkeypatch, damage):
inherited = {"org.opencontainers.image.revision": "d" * 40, "io.buildah.version": "1.40.0"}
item = writer_inventory(inherited)
if damage == "changed_image_label":
item["Config"]["Labels"]["org.opencontainers.image.revision"] = "e" * 40
elif damage == "extra_label":
item["Config"]["Labels"]["unexpected"] = "value"
elif damage == "reserved_image_label":
inherited[module.WRITER_LABEL] = "inherited-marker"
elif damage == "falsey_labels":
inherited = []
def run(arguments, **kwargs):
if arguments == ["docker", "inspect", item["Id"]]:
return json.dumps([item]).encode()
assert arguments == ["docker", "image", "inspect", IMAGE_ID]
return json.dumps([{"Id": IMAGE_ID, "Config": {"Labels": inherited}}]).encode()
monkeypatch.setattr(module, "_run", run)
if damage:
with pytest.raises(module.VolumeTransitionError, match="writer_.*invalid"):
module._inspect_writer(item["Id"], IMAGE_ID, REVISION, kind="config")
else:
assert module._inspect_writer(item["Id"], IMAGE_ID, REVISION, kind="config") == item
@pytest.mark.parametrize("damage", [None, "tmpfs_projection", "extra_bind", "wrong_volume", "read_only"])
def test_writer_accepts_declared_tmpfs_without_inspect_mount_projection(monkeypatch, damage):
item = writer_inventory({})
if damage == "tmpfs_projection":
item["Mounts"].append({"Type": "tmpfs", "Destination": "/tmp"})
elif damage == "extra_bind":
item["Mounts"].append({"Type": "bind", "Destination": "/foreign"})
elif damage == "wrong_volume":
item["Mounts"][0]["Name"] = "foreign-volume"
elif damage == "read_only":
item["Mounts"][0]["RW"] = False
def run(arguments, **kwargs):
if arguments == ["docker", "inspect", item["Id"]]:
return json.dumps([item]).encode()
assert arguments == ["docker", "image", "inspect", IMAGE_ID]
return json.dumps([{"Id": IMAGE_ID, "Config": {"Labels": None}}]).encode()
monkeypatch.setattr(module, "_run", run)
if damage in {"extra_bind", "wrong_volume", "read_only"}:
with pytest.raises(module.VolumeTransitionError, match="writer_mount_invalid"):
module._inspect_writer(item["Id"], IMAGE_ID, REVISION, kind="config")
else:
module._inspect_writer(item["Id"], IMAGE_ID, REVISION, kind="config")
@pytest.mark.parametrize("name", ["foreign-project", "", None, "${COMPOSE_PROJECT_NAME}"])
def test_source_compose_name_cannot_be_overridden_by_fixed_project_flag(monkeypatch, name):
def never_compose(*args, **kwargs):
pytest.fail("Foreign source project reached Compose")
monkeypatch.setattr(module, "_run", never_compose)
source = f"name: {json.dumps(name)}\nservices:\n deploy-control:\n image: immutable-controller\n".encode()
with pytest.raises(module.VolumeTransitionError, match="project_invalid"):
module.rendered_model(source, REVISION, IMAGE_ID)
def test_compose_without_explicit_name_retains_canonical_project(monkeypatch):
def run(arguments, *, data=None, **kwargs):
if arguments[:3] == ["docker", "image", "inspect"]:
return IMAGE_ID.encode()
normalized = model()
normalized["name"] = (
arguments[arguments.index("--project-name") + 1]
if "--project-name" in arguments else "data"
)
return json.dumps(normalized).encode()
monkeypatch.setattr(module, "_run", run)
original = b"services:\n deploy-control:\n image: immutable-controller\n"
rendered = json.loads(module.rendered_model(original, REVISION, IMAGE_ID))
assert rendered["name"] == "gitea-deploy-runners"
def model():
return {
"name": "gitea-deploy-runners",
"services": {
"deploy-control": {
"container_name": module.CONTAINER,
"image": "immutable-controller",
"volumes": [
{
"type": "bind",
"source": module.DOCKER_ROOT.as_posix(),
"target": module.DOCKER_ROOT.as_posix(),
"read_only": True,
},
{
"type": "bind",
"source": module.CONTROLLER_DIR.as_posix(),
"target": module.EXECUTABLE_TARGET,
"read_only": True,
},
],
"environment": {"PRIVATE_SETTING": "unchanged"},
},
"runner": {
"image": "pinned-runner",
"volumes": [
{"type": "bind", "source": "/fixed/data", "target": "/data"}
],
},
},
}
@pytest.fixture
def compose(monkeypatch):
baseline = model()
calls = []
def run(arguments, *, data=None, **_kwargs):
calls.append((arguments, data))
if arguments[:3] == ["docker", "image", "inspect"]:
return IMAGE_ID.encode("ascii")
if arguments[-2:] == ["--format", "json"]:
return data if data and data.startswith(b"{") else json.dumps(baseline).encode()
return b""
monkeypatch.setattr(module, "_run", run)
return baseline, calls
def test_compose_uses_protected_code_and_two_config_views(compose):
baseline, calls = compose
rendered = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
service = rendered["services"]["deploy-control"]
assert service["image"] == IMAGE_ID
assert service["pull_policy"] == "never"
volumes = service["volumes"]
assert {
"type": "bind",
"source": module._expected_revision_path(REVISION).as_posix(),
"target": module.EXECUTABLE_TARGET,
"read_only": True,
"bind": {"propagation": "rslave"},
} in volumes
assert {
"type": "volume",
"source": module.VOLUME,
"target": module.TARGET,
"volume": {"nocopy": True},
} in volumes
assert {
"type": "volume",
"source": "itworx-forge-master-key-backup",
"target": "/forge-master-key-backup",
"volume": {"nocopy": True},
} in volumes
assert {
"type": "bind",
"source": module._expected_config_path().as_posix(),
"target": module.CONFIG_TARGET,
"read_only": True,
"bind": {"propagation": "rslave"},
} in volumes
assert len(volumes) == 5
volumes.remove(next(item for item in volumes if item["target"] == module.EXECUTABLE_TARGET))
volumes.remove(next(item for item in volumes if item["target"] == module.TARGET))
volumes.remove(next(item for item in volumes if item["target"] == "/forge-master-key-backup"))
volumes.remove(next(item for item in volumes if item["target"] == module.CONFIG_TARGET))
assert rendered["volumes"] == {
module.VOLUME: {"name": module.VOLUME, "external": True},
"itworx-forge-master-key-backup": {
"name": "itworx-forge-master-key-backup", "external": True
},
}
assert rendered["services"]["runner"] == baseline["services"]["runner"]
assert service["environment"] == baseline["services"]["deploy-control"]["environment"]
compose_calls = [
arguments for arguments, _ in calls
if arguments[:2] == ["docker", "--config"] and arguments[3] == "compose"
]
assert compose_calls
assert all("--project-directory" in arguments for arguments in compose_calls)
assert module.ROOT.as_posix() in compose_calls[0]
assert all(module.MOUNTPOINT.as_posix() in arguments for arguments in compose_calls[1:])
assert all("--env-file" in arguments and "/dev/null" in arguments for arguments in compose_calls)
assert all("-f" in arguments and "-" in arguments for arguments in compose_calls)
assert calls[0][1] == ORIGINAL_COMPOSE
def test_compose_migrates_legacy_config_alias_to_explicit_private_bind(compose):
baseline, _calls = compose
baseline["services"][module.SERVICE]["volumes"].append(
{
"type": "bind",
"source": module._expected_config_path().as_posix(),
"target": module.CONFIG_TARGET,
"read_only": True,
}
)
rendered = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
aliases = [
mount for mount in rendered["services"][module.SERVICE]["volumes"]
if mount.get("target") == module.CONFIG_TARGET
]
assert aliases == [
{
"type": "bind",
"source": module._expected_config_path().as_posix(),
"target": module.CONFIG_TARGET,
"read_only": True,
"bind": {"propagation": "rslave"},
}
]
def test_compose_accepts_empty_bind_normalization_and_canonicalizes_both_managed_binds(compose):
baseline, _calls = compose
volumes = baseline["services"][module.SERVICE]["volumes"]
next(item for item in volumes if item["target"] == module.EXECUTABLE_TARGET)["bind"] = {}
volumes.append(
{
"type": "bind",
"source": module._expected_config_path().as_posix(),
"target": module.CONFIG_TARGET,
"read_only": True,
"bind": {},
}
)
rendered = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
managed = [
mount for mount in rendered["services"][module.SERVICE]["volumes"]
if mount.get("target") in {module.EXECUTABLE_TARGET, module.CONFIG_TARGET}
]
assert {item["target"]: item["bind"] for item in managed} == {
module.EXECUTABLE_TARGET: {"propagation": "rslave"},
module.CONFIG_TARGET: {"propagation": "rslave"},
}
def test_compose_normalizes_all_observed_live_generated_bind_defaults(compose):
baseline, _calls = compose
volumes = baseline["services"][module.SERVICE]["volumes"]
for mount in volumes:
mount["bind"] = {"create_host_path": True}
rendered = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
graph = next(mount for mount in rendered["services"][module.SERVICE]["volumes"]
if mount["target"] == module.DOCKER_ROOT.as_posix())
assert graph == {
"type": "bind", "source": module.DOCKER_ROOT.as_posix(),
"target": module.DOCKER_ROOT.as_posix(), "read_only": True,
}
module._bound_model(json.dumps(rendered).encode())
@pytest.mark.parametrize("bind", [
{"create_host_path": 1}, {"create_host_path": "true"},
{"create_host_path": False},
{"create_host_path": True, "propagation": "rshared"},
{"create_host_path": True, "unknown": True},
])
def test_compose_rejects_unsafe_generated_docker_root_bind(compose, bind):
baseline, _calls = compose
graph = next(mount for mount in baseline["services"][module.SERVICE]["volumes"]
if mount["target"] == module.DOCKER_ROOT.as_posix())
graph["bind"] = bind
with pytest.raises(module.VolumeTransitionError):
module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
@pytest.mark.parametrize("target", [module.EXECUTABLE_TARGET, module.CONFIG_TARGET])
@pytest.mark.parametrize("private", [None, "rprivate", "rslave"])
def test_compose_normalizes_generated_create_host_path_for_fixed_readonly_mounts(
compose, target, private
):
baseline, _calls = compose
volumes = baseline["services"][module.SERVICE]["volumes"]
bind = {"create_host_path": True}
if private:
bind["propagation"] = private
if target == module.EXECUTABLE_TARGET:
mount = next(item for item in volumes if item["target"] == target)
mount["bind"] = bind
else:
volumes.append({
"type": "bind", "source": module._expected_config_path().as_posix(),
"target": target, "read_only": True, "bind": bind,
})
rendered = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
managed = [mount for mount in rendered["services"][module.SERVICE]["volumes"]
if mount["target"] in {module.EXECUTABLE_TARGET, module.CONFIG_TARGET}]
assert len(managed) == 2
assert all(mount["read_only"] is True for mount in managed)
assert all(mount["bind"] == {"propagation": "rslave"} for mount in managed)
assert rendered["services"]["runner"] == baseline["services"]["runner"]
# Only captured source metadata is normalized. Activation stays canonical.
module._bound_model(json.dumps(rendered).encode())
next(mount for mount in managed if mount["target"] == target)["bind"]["create_host_path"] = True
with pytest.raises(module.VolumeTransitionError):
module._bound_model(json.dumps(rendered).encode())
@pytest.mark.parametrize("target", [module.EXECUTABLE_TARGET, module.CONFIG_TARGET])
@pytest.mark.parametrize("bind", [
{"create_host_path": 1},
{"create_host_path": "true"},
{"create_host_path": True, "propagation": "rshared"},
{"create_host_path": True, "selinux": "z"},
])
def test_compose_rejects_unsafe_or_untyped_generated_bind_metadata(compose, target, bind):
baseline, _calls = compose
volumes = baseline["services"][module.SERVICE]["volumes"]
if target == module.EXECUTABLE_TARGET:
next(mount for mount in volumes if mount["target"] == target)["bind"] = bind
else:
volumes.append({
"type": "bind", "source": module._expected_config_path().as_posix(),
"target": target, "read_only": True, "bind": bind,
})
with pytest.raises(module.VolumeTransitionError):
module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
@pytest.mark.parametrize(
("target", "bind"),
[
(module.EXECUTABLE_TARGET, {"propagation": "rshared"}),
(module.EXECUTABLE_TARGET, {"propagation": "shared"}),
(module.EXECUTABLE_TARGET, {"propagation": "rslave", "create_host_path": False}),
(module.CONFIG_TARGET, {"propagation": "rshared"}),
(module.CONFIG_TARGET, {"propagation": "shared"}),
(module.CONFIG_TARGET, {"propagation": "rslave", "create_host_path": False}),
],
)
def test_compose_rejects_noncanonical_controller_bind_propagation(compose, target, bind):
baseline, _calls = compose
volumes = baseline["services"][module.SERVICE]["volumes"]
if target == module.EXECUTABLE_TARGET:
mount = next(item for item in volumes if item["target"] == target)
mount["bind"] = bind
else:
volumes.append(
{
"type": "bind",
"source": module._expected_config_path().as_posix(),
"target": target,
"read_only": True,
"bind": bind,
}
)
with pytest.raises(module.VolumeTransitionError):
module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
def test_compose_accepts_already_canonical_controller_mounts_without_duplicates(compose):
baseline, _calls = compose
volumes = baseline["services"][module.SERVICE]["volumes"]
next(item for item in volumes if item["target"] == module.EXECUTABLE_TARGET)["bind"] = {
"propagation": "rslave"
}
volumes.extend(
[
{
"type": "volume",
"source": module.VOLUME,
"target": module.TARGET,
"volume": {"nocopy": True},
},
{
"type": "volume",
"source": module.BACKUP_VOLUME,
"target": module.BACKUP_TARGET,
"volume": {"nocopy": True},
},
{
"type": "bind",
"source": module._expected_config_path().as_posix(),
"target": module.CONFIG_TARGET,
"read_only": True,
"bind": {"propagation": "rslave"},
},
]
)
baseline["volumes"] = {
module.VOLUME: {"name": module.VOLUME, "external": True},
module.BACKUP_VOLUME: {"name": module.BACKUP_VOLUME, "external": True},
}
rendered = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
final_mounts = rendered["services"][module.SERVICE]["volumes"]
assert len(final_mounts) == len(volumes)
assert sum(item.get("target") == module.EXECUTABLE_TARGET for item in final_mounts) == 1
assert sum(item.get("target") == module.TARGET for item in final_mounts) == 1
assert sum(item.get("target") == module.BACKUP_TARGET for item in final_mounts) == 1
assert sum(item.get("target") == module.CONFIG_TARGET for item in final_mounts) == 1
def test_activation_and_rollback_models_both_pin_managed_bind_propagation(compose):
_baseline, _calls = compose
old_model = json.loads(module.rendered_model(ORIGINAL_COMPOSE, "b" * 40, IMAGE_ID))
next_model = json.loads(module.rendered_model(ORIGINAL_COMPOSE, "c" * 40, IMAGE_ID))
for rendered in (old_model, next_model):
mounts = rendered["services"][module.SERVICE]["volumes"]
managed = {
mount["target"]: mount
for mount in mounts
if mount.get("target") in {module.EXECUTABLE_TARGET, module.CONFIG_TARGET}
}
assert set(managed) == {module.EXECUTABLE_TARGET, module.CONFIG_TARGET}
assert managed[module.EXECUTABLE_TARGET]["bind"] == {"propagation": "rslave"}
assert managed[module.CONFIG_TARGET]["bind"] == {"propagation": "rslave"}
def _legacy_private_compose(revision):
value = json.loads(module.rendered_model(ORIGINAL_COMPOSE, revision, IMAGE_ID))
for mount in value["services"][module.SERVICE]["volumes"]:
if mount.get("target") in {module.EXECUTABLE_TARGET, module.CONFIG_TARGET}:
mount["bind"] = {"propagation": "rprivate"}
return (json.dumps(value, sort_keys=True, indent=2) + "\n").encode()
def _projected_compose(captured):
value = json.loads(captured)
for mount in value["services"][module.SERVICE]["volumes"]:
if mount.get("target") in {module.EXECUTABLE_TARGET, module.CONFIG_TARGET}:
mount["bind"] = {"propagation": "rslave"}
assert mount["read_only"] is True
return (json.dumps(value, sort_keys=True, indent=2) + "\n").encode()
@pytest.mark.parametrize(
("rollback", "current_state", "write_expected", "error"),
[
(False, "raw_target", True, None),
(False, "projected_target", False, None),
(False, "before", True, None),
(True, "raw_target", True, None),
(True, "raw_after", True, None),
(True, "projected_after", True, None),
(True, "projected_target", False, None),
(True, "before", True, None),
(False, "unrecognized", False, "controller_volume_compose_transition_conflict"),
(True, "unrecognized", False, "controller_volume_compose_rollback_conflict"),
],
)
def test_root_compose_projects_legacy_receipts_and_bounds_retries(
compose, monkeypatch, tmp_path, rollback, current_state, write_expected, error,
):
_baseline, _calls = compose
before = b"captured-source-compose"
after = _legacy_private_compose("c" * 40)
rollback_value = _legacy_private_compose("b" * 40)
values = {"compose-after.json": after, "compose-rollback.json": rollback_value}
raw_target = rollback_value if rollback else after
projected_target = _projected_compose(raw_target)
current_by_state = {
"raw_target": raw_target,
"projected_target": projected_target,
"raw_after": after,
"projected_after": _projected_compose(after),
"before": before,
"unrecognized": b"unrecognized-compose",
}
live_file = {"value": current_by_state[current_state], "mode": 0o640}
receipt = {
"composeBeforeSha256": module._hash(before),
"composeAfterSha256": module._hash(after),
"composeRollbackSha256": module._hash(rollback_value),
}
writes = []
monkeypatch.setattr(module, "_directory", lambda *_args: 23)
monkeypatch.setattr(
module, "_file_info_at", lambda *_args: (live_file["value"], live_file["mode"])
)
monkeypatch.setattr(module, "_read_backup", lambda *_args: b'{"composeMode":416}')
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
def atomic_at(_parent, name, value, expected, *, private, mode):
assert name == "docker-compose.yml"
assert expected == live_file["value"]
assert private is False
assert mode == 0o640
writes.append(value)
live_file["value"] = value
monkeypatch.setattr(module, "_atomic_at", atomic_at)
backup = tmp_path / "backup"
if error:
with pytest.raises(module.VolumeTransitionError, match=error):
module._ensure_root_compose(backup, receipt, values, rollback=rollback)
assert writes == []
assert live_file["value"] == current_by_state[current_state]
return
module._ensure_root_compose(backup, receipt, values, rollback=rollback)
module._ensure_root_compose(backup, receipt, values, rollback=rollback)
assert len(writes) == int(write_expected)
assert live_file["value"] == projected_target
assert module._hash(after) == receipt["composeAfterSha256"]
assert module._hash(rollback_value) == receipt["composeRollbackSha256"]
assert values["compose-after.json"] == after
assert values["compose-rollback.json"] == rollback_value
@pytest.mark.parametrize("rollback", [False, True])
def test_root_compose_projected_retry_still_checks_file_mode(
compose, monkeypatch, tmp_path, rollback,
):
_baseline, _calls = compose
after = module.rendered_model(ORIGINAL_COMPOSE, "c" * 40, IMAGE_ID)
rollback_value = module.rendered_model(ORIGINAL_COMPOSE, "b" * 40, IMAGE_ID)
projected = rollback_value if rollback else after
before = b"captured-source-compose"
receipt = {
"composeBeforeSha256": module._hash(before),
"composeAfterSha256": module._hash(after),
"composeRollbackSha256": module._hash(rollback_value),
}
values = {"compose-after.json": after, "compose-rollback.json": rollback_value}
monkeypatch.setattr(module, "_directory", lambda *_args: 23)
monkeypatch.setattr(module, "_file_info_at", lambda *_args: (projected, 0o600))
monkeypatch.setattr(module, "_read_backup", lambda *_args: b'{"composeMode":416}')
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
monkeypatch.setattr(
module, "_atomic_at",
lambda *_args, **_kwargs: pytest.fail("A projected retry must not rewrite the file"),
)
with pytest.raises(module.VolumeTransitionError, match="compose_mode_invalid"):
module._ensure_root_compose(tmp_path / "backup", receipt, values, rollback=rollback)
def test_compose_allows_only_the_fixed_managed_appdata_volume_mount(compose):
baseline, _calls = compose
mount = {
"type": "volume",
"source": module.MANAGED_APPDATA_VOLUME,
"target": module.MANAGED_APPDATA_MOUNTPOINT.as_posix(),
}
baseline["services"][module.SERVICE]["volumes"].append(mount)
baseline["volumes"] = {
module.MANAGED_APPDATA_VOLUME: {
"name": module.MANAGED_APPDATA_VOLUME,
"external": True,
}
}
rendered = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
assert mount in rendered["services"][module.SERVICE]["volumes"]
assert rendered["volumes"][module.MANAGED_APPDATA_VOLUME] == {
"name": module.MANAGED_APPDATA_VOLUME,
"external": True,
}
def test_compose_rejects_a_managed_appdata_target_alias(compose):
baseline, _calls = compose
baseline["services"][module.SERVICE]["volumes"].append({
"type": "volume",
"source": module.MANAGED_APPDATA_VOLUME,
"target": "/var/lib/docker/volumes/foreign/_data",
})
baseline["volumes"] = {
module.MANAGED_APPDATA_VOLUME: {
"name": module.MANAGED_APPDATA_VOLUME,
"external": True,
}
}
with pytest.raises(module.VolumeTransitionError, match="managed_appdata"):
module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
def test_source_relative_controller_bind_uses_original_compose_directory(monkeypatch):
def run(arguments, *, data=None, **kwargs):
if arguments[:3] == ["docker", "image", "inspect"]:
return IMAGE_ID.encode()
normalized = model()
directory = arguments[arguments.index("--project-directory") + 1]
if arguments[-2:] == ["--format", "json"]:
normalized["services"][module.SERVICE]["volumes"][1]["source"] = directory + "/controller"
return json.dumps(normalized).encode()
monkeypatch.setattr(module, "_run", run)
original = ORIGINAL_COMPOSE + b" volumes:\n - ./controller:/opt/gitea-deploy:ro\n"
rendered = json.loads(module.rendered_model(original, REVISION, IMAGE_ID))
mounts = rendered["services"][module.SERVICE]["volumes"]
assert next(m["source"] for m in mounts if m["target"] == module.EXECUTABLE_TARGET) == module._mount_source_for_revision(REVISION)
@pytest.mark.parametrize(
"damage",
[
"target_collision", "foreign_volume", "graph_rw", "relative_bind", "env_file",
"backup_target_collision", "backup_volume_alias", "backup_target_child_alias",
"backup_definition_collision",
],
)
def test_compose_rejects_unsafe_or_unpinned_activation_inputs(compose, damage):
baseline, _ = compose
if damage == "target_collision":
baseline["services"]["deploy-control"]["volumes"].append(
{"type": "bind", "source": "/unsafe", "target": module.TARGET}
)
elif damage == "foreign_volume":
baseline["services"]["runner"]["volumes"].append(
{"type": "volume", "source": module.VOLUME, "target": "/foreign"}
)
elif damage == "graph_rw":
baseline["services"]["deploy-control"]["volumes"][0]["read_only"] = False
elif damage == "relative_bind":
baseline["services"]["runner"]["volumes"][0]["source"] = "./unsafe"
elif damage == "env_file":
baseline["services"]["deploy-control"]["env_file"] = [".env"]
elif damage == "backup_target_collision":
baseline["services"]["deploy-control"]["volumes"].append(
{"type": "bind", "source": "/foreign", "target": "/forge-master-key-backup"}
)
elif damage == "backup_volume_alias":
baseline["services"]["runner"]["volumes"].append(
{
"type": "volume", "source": "itworx-forge-master-key-backup",
"target": "/foreign-backup",
}
)
elif damage == "backup_target_child_alias":
baseline["services"]["runner"]["volumes"].append(
{"type": "bind", "source": "/foreign", "target": "/forge-master-key-backup/state"}
)
else:
baseline["volumes"] = {
"itworx-forge-master-key-backup": {"name": "foreign", "external": True}
}
with pytest.raises(module.VolumeTransitionError):
module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
def test_pre_promotion_accepts_the_healthy_controller_without_the_new_mount(compose):
baseline, _calls = compose
reference = "registry.example/controller:stable"
baseline["services"][module.SERVICE]["image"] = reference
current = {
"Image": IMAGE_ID,
"Config": {
"Image": reference,
"Labels": {
"com.docker.compose.project": "gitea-deploy-runners",
"com.docker.compose.service": module.SERVICE,
},
},
"State": {"Health": {"Status": "healthy"}},
"Mounts": [
{
"Type": "volume", "Name": module.VOLUME,
"Source": module.MOUNTPOINT.as_posix(), "Destination": module.TARGET,
"RW": True,
}
],
}
original = f"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: {reference}\n".encode()
rendered = json.loads(
module.rendered_model(original, REVISION, IMAGE_ID, current_container=current)
)
assert not any(mount.get("Destination") == "/forge-master-key-backup" for mount in current["Mounts"])
assert any(
mount.get("target") == "/forge-master-key-backup"
and mount.get("source") == "itworx-forge-master-key-backup"
for mount in rendered["services"][module.SERVICE]["volumes"]
)
def test_compose_tag_replacement_cannot_change_controller_startup_image(compose, monkeypatch):
_baseline, calls = compose
original_run = module._run
def replaced_tag(arguments, **kwargs):
if arguments[:3] == ["docker", "image", "inspect"]:
return ("sha256:" + "c" * 64).encode("ascii")
return original_run(arguments, **kwargs)
monkeypatch.setattr(module, "_run", replaced_tag)
with pytest.raises(module.VolumeTransitionError, match="image_reference_changed"):
module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
assert not any(
arguments[-1:] == ["deploy-control"] and "up" in arguments
for arguments, _ in calls
)
@pytest.mark.parametrize("damage", [None, "foreign_reference", "wrong_project", "wrong_service", "unhealthy", "digest_reference", "wrong_image", "normalized_reference", "interpolation"])
def test_drifted_tag_requires_healthy_current_controller_provenance(compose, monkeypatch, damage):
baseline, calls = compose
reference = "registry.example/controller:stable"
if damage == "digest_reference":
reference = "registry.example/controller@sha256:" + "c" * 64
baseline["services"][module.SERVICE]["image"] = reference
current = {
"Image": IMAGE_ID,
"Config": {"Image": reference, "Labels": {
"com.docker.compose.project": "gitea-deploy-runners",
"com.docker.compose.service": module.SERVICE,
}},
"State": {"Health": {"Status": "healthy"}},
}
if damage == "foreign_reference":
current["Config"]["Image"] = "registry.example/foreign:stable"
elif damage == "wrong_project":
current["Config"]["Labels"]["com.docker.compose.project"] = "foreign"
elif damage == "wrong_service":
current["Config"]["Labels"]["com.docker.compose.service"] = "foreign"
elif damage == "unhealthy":
current["State"]["Health"]["Status"] = "unhealthy"
elif damage == "wrong_image":
current["Image"] = "sha256:" + "c" * 64
original_run = module._run
def drifted_tag(arguments, **kwargs):
if arguments[:3] == ["docker", "image", "inspect"]:
return ("sha256:" + "c" * 64).encode("ascii")
return original_run(arguments, **kwargs)
monkeypatch.setattr(module, "_run", drifted_tag)
source_reference = reference
if damage == "normalized_reference":
source_reference = "registry.example/foreign:stable"
elif damage == "interpolation":
source_reference = "${CONTROLLER_IMAGE}"
original = f"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: {source_reference}\n".encode()
with pytest.raises(module.VolumeTransitionError, match="image_reference_(changed|invalid)"):
module.rendered_model(original, REVISION, IMAGE_ID, current_container=current)
assert not any("up" in arguments or "pull" in arguments for arguments, _ in calls)
def _tag_drift_container():
return {
"Id": "d" * 64,
"Image": IMAGE_ID,
"Name": f"/{module.CONTAINER}",
"State": {"Running": True, "Health": {"Status": "healthy"}},
"Config": {
"Image": "docker.gitea.com/runner-images:ubuntu-latest",
"Labels": {
"com.docker.compose.project": "gitea-deploy-runners",
"com.docker.compose.service": "deploy-control",
"com.docker.compose.oneoff": "False",
},
},
"Mounts": [
{
"Type": "bind",
"Source": module.CONTROLLER_DIR.as_posix(),
"Destination": module.EXECUTABLE_TARGET,
"RW": False,
},
{
"Type": "bind",
"Source": module._expected_config_path().as_posix(),
"Destination": module.CONFIG_TARGET,
"RW": False,
},
{
"Type": "volume",
"Name": module.VOLUME,
"Source": module.MOUNTPOINT.as_posix(),
"Destination": module.TARGET,
"RW": True,
},
],
}
_DEFAULT_SOURCE_IMAGE_ID = object()
def _install_tag_drift_docker_fixture(
monkeypatch, *, source_image_id=_DEFAULT_SOURCE_IMAGE_ID, source_reference=None,
expected_image_output=IMAGE_ID, mutate=None
):
source_ref = "docker.gitea.com/runner-images:ubuntu-latest"
if source_reference is not None:
source_ref = source_reference
if source_image_id is _DEFAULT_SOURCE_IMAGE_ID:
source_image_id = "sha256:" + "c" * 64
container = _tag_drift_container()
if mutate is not None:
mutate(container)
compose_model = model()
compose_model["services"][module.SERVICE]["image"] = source_ref
def run(arguments, *, data=None, **_kwargs):
if arguments[:2] == ["docker", "inspect"]:
return json.dumps([container]).encode("utf-8")
if arguments[:3] == ["docker", "image", "inspect"]:
reference = arguments[-1]
if reference == source_ref:
if source_image_id is None:
return b""
return (source_image_id + "\n").encode("ascii")
if reference == IMAGE_ID:
return (expected_image_output + "\n").encode("ascii") if expected_image_output else b""
return b"sha256:" + b"c" * 64 + b"\n"
if arguments[-2:] == ["--format", "json"]:
return json.dumps(compose_model).encode("utf-8")
return b""
monkeypatch.setattr(module, "_run", run)
return container, source_ref, source_image_id
def test_observed_runner_tag_drift_proof_pins_both_rendered_models(monkeypatch):
container, source_ref, source_image_id = _install_tag_drift_docker_fixture(monkeypatch)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
proof = module._attest_observed_runner_tag_drift(original, container, IMAGE_ID)
rollback = json.loads(module.rendered_model(
original, REVISION, IMAGE_ID, source_tag_drift_proof=proof
))
forward = json.loads(module.rendered_model(
original, "c" * 40, IMAGE_ID, source_tag_drift_proof=proof
))
assert proof.source_reference == source_ref
assert proof.source_image_id == source_image_id
for rendered in (rollback, forward):
assert rendered["services"][module.SERVICE]["image"] == IMAGE_ID
assert rendered["services"][module.SERVICE]["pull_policy"] == "never"
@pytest.mark.parametrize(
("mutate", "source_image_id", "expected_error"),
[
(lambda item: item["Config"].__setitem__("Image", "wrong:tag"), None, "source_tag_drift_container_invalid"),
(lambda item: item["Config"]["Labels"].__setitem__("com.docker.compose.project", "other"), None, "source_tag_drift_container_invalid"),
(lambda item: item["Config"]["Labels"].__setitem__("com.docker.compose.service", "runner"), None, "source_tag_drift_container_invalid"),
(lambda item: item["Config"]["Labels"].__setitem__("com.docker.compose.oneoff", "True"), None, "source_tag_drift_container_invalid"),
(lambda item: item["State"].__setitem__("Running", False), None, "container_not_running"),
(lambda item: item.__setitem__("Name", "/renamed-controller"), None, "container_image_changed"),
(lambda item: item["Mounts"].__setitem__(0, {"Type": "bind", "Source": "/unsafe", "Destination": module.EXECUTABLE_TARGET, "RW": False}), None, "executable_mount_invalid"),
(lambda item: item["Mounts"][1].__setitem__("RW", True), None, "config_alias_invalid"),
(None, None, "image_reference_invalid"),
(None, IMAGE_ID, "source_tag_drift_not_observed"),
(None, "not-an-image-id", "image_reference_invalid"),
],
)
def test_observed_runner_tag_drift_rejects_unattested_or_non_drift_cases(
monkeypatch, mutate, source_image_id, expected_error
):
container, _source_ref, _observed_id = _install_tag_drift_docker_fixture(
monkeypatch, source_image_id=source_image_id, mutate=mutate
)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
with pytest.raises(module.VolumeTransitionError, match=expected_error):
module._attest_observed_runner_tag_drift(original, container, IMAGE_ID)
def test_observed_runner_tag_drift_rejects_wrong_source_tag(monkeypatch):
container, _source_ref, _observed_id = _install_tag_drift_docker_fixture(
monkeypatch, source_reference="docker.gitea.com/runner-images@sha256:" + "c" * 64
)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images@sha256:" + b"c" * 64 + b"\n"
with pytest.raises(module.VolumeTransitionError, match="source_tag_drift_reference_invalid"):
module._attest_observed_runner_tag_drift(original, container, IMAGE_ID)
def test_observed_runner_tag_drift_rejects_missing_local_expected_image(monkeypatch):
container, _source_ref, _observed_id = _install_tag_drift_docker_fixture(
monkeypatch, expected_image_output=""
)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
with pytest.raises(module.VolumeTransitionError, match="source_tag_drift_expected_image_missing"):
module._attest_observed_runner_tag_drift(original, container, IMAGE_ID)
def test_rendered_model_rejects_altered_drift_proof(monkeypatch):
container, _source_ref, _observed_id = _install_tag_drift_docker_fixture(monkeypatch)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
proof = module._attest_observed_runner_tag_drift(original, container, IMAGE_ID)
altered = proof._replace(compose_sha256="0" * 64)
with pytest.raises(module.VolumeTransitionError, match="source_tag_drift_proof_invalid"):
module.rendered_model(original, REVISION, IMAGE_ID, source_tag_drift_proof=altered)
@pytest.mark.parametrize(
("mutate", "expected_error"),
[
(lambda item: item.__setitem__("Id", "e" * 64), "container_changed_during_tag_drift_check"),
(lambda item: item.__setitem__("Image", "sha256:" + "e" * 64), "container_image_changed"),
(lambda item: item["Config"].__setitem__("Image", "other:tag"), "source_tag_drift_container_invalid"),
(lambda item: item["Config"]["Labels"].__setitem__("com.docker.compose.project", "other"), "source_tag_drift_container_invalid"),
(lambda item: item["Mounts"][1].__setitem__("RW", True), "config_alias_invalid"),
(lambda item: item["Mounts"].append({"Type": "bind", "Source": "/other", "Destination": "/extra", "RW": False}), "source_tag_drift_proof_invalid"),
(lambda item: item["Config"].__setitem__("User", "unexpected"), "source_tag_drift_proof_invalid"),
],
)
def test_rendered_model_revalidates_live_container_and_mount_proof(
monkeypatch, mutate, expected_error
):
container, _source_ref, _observed_id = _install_tag_drift_docker_fixture(monkeypatch)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
proof = module._attest_observed_runner_tag_drift(original, container, IMAGE_ID)
mutate(container)
with pytest.raises(module.VolumeTransitionError, match=expected_error):
module.rendered_model(original, REVISION, IMAGE_ID, source_tag_drift_proof=proof)
def test_rendered_model_rejects_tag_that_resolves_back_to_expected_image(monkeypatch):
container, source_ref, _observed_id = _install_tag_drift_docker_fixture(monkeypatch)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
proof = module._attest_observed_runner_tag_drift(original, container, IMAGE_ID)
original_run = module._run
def moved_tag(arguments, **kwargs):
if arguments[:3] == ["docker", "image", "inspect"] and arguments[-1] == source_ref:
return (IMAGE_ID + "\n").encode("ascii")
return original_run(arguments, **kwargs)
monkeypatch.setattr(module, "_run", moved_tag)
with pytest.raises(module.VolumeTransitionError, match="source_tag_drift_changed"):
module.rendered_model(original, REVISION, IMAGE_ID, source_tag_drift_proof=proof)
def test_rendered_model_rejects_missing_expected_image_after_proof(monkeypatch):
container, _source_ref, _observed_id = _install_tag_drift_docker_fixture(monkeypatch)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
proof = module._attest_observed_runner_tag_drift(original, container, IMAGE_ID)
original_run = module._run
def missing_expected(arguments, **kwargs):
if arguments[:3] == ["docker", "image", "inspect"] and arguments[-1] == IMAGE_ID:
return b""
return original_run(arguments, **kwargs)
monkeypatch.setattr(module, "_run", missing_expected)
with pytest.raises(module.VolumeTransitionError, match="source_tag_drift_expected_image_missing"):
module.rendered_model(original, REVISION, IMAGE_ID, source_tag_drift_proof=proof)
def test_rendered_model_rejects_reuse_with_changed_captured_compose_bytes(monkeypatch):
container, _source_ref, _observed_id = _install_tag_drift_docker_fixture(monkeypatch)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
proof = module._attest_observed_runner_tag_drift(original, container, IMAGE_ID)
with pytest.raises(module.VolumeTransitionError, match="source_tag_drift_proof_invalid"):
module.rendered_model(original + b"# changed captured source\n", REVISION,
IMAGE_ID, source_tag_drift_proof=proof)
def test_observed_runner_tag_drift_rechecks_same_container_around_image_inspection(monkeypatch):
source_ref = "docker.gitea.com/runner-images:ubuntu-latest"
first = _tag_drift_container()
second = _tag_drift_container()
second["Id"] = "e" * 64
compose_model = model()
compose_model["services"][module.SERVICE]["image"] = source_ref
inspect_count = 0
def run(arguments, *, data=None, **_kwargs):
nonlocal inspect_count
if arguments[:2] == ["docker", "inspect"]:
inspect_count += 1
return json.dumps([first if inspect_count == 1 else second]).encode("utf-8")
if arguments[:3] == ["docker", "image", "inspect"]:
reference = arguments[-1]
return (("sha256:" + "c" * 64) if reference == source_ref else IMAGE_ID).encode("ascii")
if arguments[-2:] == ["--format", "json"]:
return json.dumps(compose_model).encode("utf-8")
return b""
monkeypatch.setattr(module, "_run", run)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
with pytest.raises(module.VolumeTransitionError, match="container_changed_during_tag_drift_check"):
module._attest_observed_runner_tag_drift(original, first, IMAGE_ID)
def test_observed_runner_tag_drift_rejects_mount_change_during_tag_inspection(monkeypatch):
source_ref = "docker.gitea.com/runner-images:ubuntu-latest"
first = _tag_drift_container()
second = _tag_drift_container()
second["Mounts"][1]["RW"] = True
compose_model = model()
compose_model["services"][module.SERVICE]["image"] = source_ref
inspect_count = 0
def run(arguments, *, data=None, **_kwargs):
nonlocal inspect_count
if arguments[:2] == ["docker", "inspect"]:
inspect_count += 1
return json.dumps([first if inspect_count == 1 else second]).encode("utf-8")
if arguments[:3] == ["docker", "image", "inspect"]:
reference = arguments[-1]
return (("sha256:" + "c" * 64) if reference == source_ref else IMAGE_ID).encode("ascii")
if arguments[-2:] == ["--format", "json"]:
return json.dumps(compose_model).encode("utf-8")
return b""
monkeypatch.setattr(module, "_run", run)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
with pytest.raises(module.VolumeTransitionError, match="config_alias_invalid"):
module._attest_observed_runner_tag_drift(original, first, IMAGE_ID)
def test_observed_runner_tag_drift_rejects_config_change_during_tag_inspection(monkeypatch):
source_ref = "docker.gitea.com/runner-images:ubuntu-latest"
first = _tag_drift_container()
second = _tag_drift_container()
second["Config"]["User"] = "unexpected"
compose_model = model()
compose_model["services"][module.SERVICE]["image"] = source_ref
inspect_count = 0
def run(arguments, *, data=None, **_kwargs):
nonlocal inspect_count
if arguments[:2] == ["docker", "inspect"]:
inspect_count += 1
return json.dumps([first if inspect_count == 1 else second]).encode("utf-8")
if arguments[:3] == ["docker", "image", "inspect"]:
reference = arguments[-1]
return (("sha256:" + "c" * 64) if reference == source_ref else IMAGE_ID).encode("ascii")
if arguments[-2:] == ["--format", "json"]:
return json.dumps(compose_model).encode("utf-8")
return b""
monkeypatch.setattr(module, "_run", run)
original = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: docker.gitea.com/runner-images:ubuntu-latest\n"
with pytest.raises(module.VolumeTransitionError, match="container_changed_during_tag_drift_check"):
module._attest_observed_runner_tag_drift(original, first, IMAGE_ID)
def test_cli_explicit_tag_drift_switch_is_accepted_only_for_prepare(monkeypatch, tmp_path):
calls = []
monkeypatch.setattr(sys, "argv", [
"prepare_controller_hub_release_volume.py", "prepare",
"--stage-root", str(tmp_path), "--source-revision", REVISION,
"--source-tree", REVISION, "--controller-version", "2.1.13",
"--image-id", IMAGE_ID, "--allow-observed-runner-tag-drift",
])
monkeypatch.setattr(
module, "prepare_promotion",
lambda *args, **kwargs: calls.append(kwargs) or tmp_path / "backup",
)
module.main()
assert len(calls) == 1
assert {key: value for key, value in calls[0].items() if key != "on_phase"} == {
"source_revision": REVISION,
"source_tree": REVISION,
"controller_version": "2.1.13",
"image_id": IMAGE_ID,
"allow_observed_runner_tag_drift": True,
}
assert callable(calls[0]["on_phase"])
def test_cli_rejects_tag_drift_switch_for_non_prepare_operation(monkeypatch):
monkeypatch.setattr(sys, "argv", [
"prepare_controller_hub_release_volume.py", "activate",
"--backup", "/unused", "--allow-observed-runner-tag-drift",
])
with pytest.raises(SystemExit, match="source_tag_recovery_operation_invalid"):
module.main()
@pytest.mark.parametrize("allow_recovery", [False, True])
def test_promotion_image_proof_failures_precede_persistent_volume_writes(
monkeypatch, tmp_path, allow_recovery
):
original = b"captured source compose with exact original bytes\n"
current = _tag_drift_container()
old_revision = "c" * 40
config_tree = ({"projects.json": (b"{}", 0o600)}, {"": 0o755})
compose_model = model()
compose_model["services"][module.SERVICE]["image"] = "docker.gitea.com/runner-images:ubuntu-latest"
proof = module._ObservedRunnerTagDriftProof(
current["Id"], IMAGE_ID, module.OBSERVED_RUNNER_IMAGE_REF,
"sha256:" + "c" * 64, module._hash(original),
module._tag_drift_config_identity(current), (),
)
stage_validation = []
writes = []
monkeypatch.setattr(module, "_validate_private_stage", lambda *_args: None)
monkeypatch.setattr(module, "ensure_volume", lambda **_kwargs: None)
monkeypatch.setattr(module, "ensure_backup_volume", lambda **_kwargs: None)
monkeypatch.setattr(module, "_controller_image_inspect", lambda *_args, **_kwargs: current)
monkeypatch.setattr(module, "_snapshot_attested_controller", lambda *_args: (
{"controller-release.json": (b"captured release", 0o644)},
{"": 0o755},
{"sourceRevision": old_revision, "sourceTree": "d" * 40},
))
monkeypatch.setattr(module, "_read_root_compose", lambda: (original, 0o600))
monkeypatch.setattr(module, "_attest_observed_runner_tag_drift", lambda *_args: proof)
monkeypatch.setattr(module, "_validate_previous_private_tls", lambda *_args: None)
monkeypatch.setattr(module, "_release_tree", lambda *_args: ({"deploy.py": (b"old", 0o644)}, {"": 0o755}))
monkeypatch.setattr(module, "_config_tree", lambda *_args, **_kwargs: config_tree)
monkeypatch.setattr(module, "_read_config_leaf", lambda: None)
monkeypatch.setattr(module, "_prepare_merged_tree", lambda *_args: ({"deploy.py": (b"new", 0o644)}, {"": 0o755}))
monkeypatch.setattr(module, "_validate_staged_configuration", lambda *_args: stage_validation.append(True))
monkeypatch.setattr(module, "_mirror_snapshot_tar", lambda: b"mirror")
monkeypatch.setattr(module, "_source_compose_model", lambda _original: (
compose_model, compose_model["services"][module.SERVICE]
))
if allow_recovery:
def invalidate_after_stage(*_args):
assert stage_validation
raise module.VolumeTransitionError("controller_volume_source_tag_drift_changed")
monkeypatch.setattr(module, "_verify_tag_drift_proof", invalidate_after_stage)
else:
monkeypatch.setattr(
module, "_verify_source_compose_image",
lambda *_args: (_ for _ in ()).throw(
module.VolumeTransitionError("controller_volume_image_reference_changed")
),
)
monkeypatch.setattr(module, "_verify_compose_image", lambda *_args: None)
monkeypatch.setattr(module, "_install_config_tree", lambda *_args: writes.append("config"))
monkeypatch.setattr(module, "_install_release_leaf", lambda *_args: writes.append("release"))
with pytest.raises(module.VolumeTransitionError):
module.prepare_promotion(
tmp_path, source_revision=REVISION, source_tree="e" * 40,
controller_version="2.1.13", image_id=IMAGE_ID,
allow_observed_runner_tag_drift=allow_recovery,
)
assert stage_validation
assert writes == []
def test_promotion_revalidates_drift_proof_immediately_before_first_volume_write(
monkeypatch, tmp_path
):
original = b"captured source compose with exact original bytes\n"
current = _tag_drift_container()
old_revision = "c" * 40
config_tree = ({"projects.json": (b"{}", 0o600)}, {"": 0o755})
stage_validation = []
proof = module._ObservedRunnerTagDriftProof(
current["Id"], IMAGE_ID, module.OBSERVED_RUNNER_IMAGE_REF,
"sha256:" + "c" * 64, module._hash(original),
module._tag_drift_config_identity(current), (),
)
writes = []
proof_checks = 0
image_checks = 0
def source_model(_original):
compose_model = model()
compose_model["services"][module.SERVICE]["image"] = module.OBSERVED_RUNNER_IMAGE_REF
return compose_model, compose_model["services"][module.SERVICE]
monkeypatch.setattr(module, "_validate_private_stage", lambda *_args: None)
monkeypatch.setattr(module, "ensure_volume", lambda **_kwargs: None)
monkeypatch.setattr(module, "ensure_backup_volume", lambda **_kwargs: None)
monkeypatch.setattr(module, "_controller_image_inspect", lambda *_args, **_kwargs: current)
monkeypatch.setattr(module, "_snapshot_attested_controller", lambda *_args: (
{"controller-release.json": (b"captured release", 0o644)},
{"": 0o755},
{"sourceRevision": old_revision, "sourceTree": "d" * 40},
))
monkeypatch.setattr(module, "_read_root_compose", lambda: (original, 0o600))
monkeypatch.setattr(module, "_attest_observed_runner_tag_drift", lambda *_args: proof)
monkeypatch.setattr(module, "_validate_previous_private_tls", lambda *_args: None)
monkeypatch.setattr(module, "_release_tree", lambda *_args: ({"deploy.py": (b"old", 0o644)}, {"": 0o755}))
monkeypatch.setattr(module, "_config_tree", lambda *_args, **_kwargs: config_tree)
monkeypatch.setattr(module, "_read_config_leaf", lambda: config_tree)
monkeypatch.setattr(module, "_prepare_merged_tree", lambda *_args: ({"deploy.py": (b"new", 0o644)}, {"": 0o755}))
monkeypatch.setattr(module, "_validate_staged_configuration", lambda *_args: stage_validation.append(True))
monkeypatch.setattr(module, "_mirror_snapshot_tar", lambda: b"mirror")
monkeypatch.setattr(module, "_source_compose_model", source_model)
monkeypatch.setattr(module, "_compose", lambda *_args, **_kwargs: b"")
def verify_tag_proof(*_args):
nonlocal proof_checks
proof_checks += 1
assert stage_validation
if proof_checks == 4:
raise module.VolumeTransitionError("controller_volume_source_tag_drift_changed")
def verify_image(*_args):
nonlocal image_checks
image_checks += 1
monkeypatch.setattr(module, "_verify_tag_drift_proof", verify_tag_proof)
monkeypatch.setattr(module, "_verify_compose_image", verify_image)
monkeypatch.setattr(module, "_install_config_tree", lambda *_args: writes.append("config"))
monkeypatch.setattr(module, "_install_release_leaf", lambda *_args: writes.append("release"))
with pytest.raises(module.VolumeTransitionError, match="source_tag_drift_changed"):
module.prepare_promotion(
tmp_path, source_revision=REVISION, source_tree="e" * 40,
controller_version="2.1.13", image_id=IMAGE_ID,
allow_observed_runner_tag_drift=True,
)
assert proof_checks == 4
assert image_checks == 2
assert writes == []
def test_config_snapshot_has_exact_allowlist_and_broker_token():
entries = {
"projects.json": (b"{}", 0o600),
"broker-token": (b"secret", 0o600),
"gitea-validation.json": (b"{}", 0o600),
"gitea-validation-token": (b"secret", 0o600),
"private-tls.json": (b"{}", 0o600),
"appops-enrollment-bootstrap": (b"secret", 0o600),
"webhook-secrets/forge": (b"secret", 0o600),
"appops-deployment-secrets/repopulse": (b"secret", 0o600),
"status_server.py": (b"code", 0o644),
}
directories = {
"": 0o755,
"webhook-secrets": 0o700,
"appops-deployment-secrets": 0o700,
}
config, config_dirs = module._config_tree(entries, directories)
assert set(config) == set(entries) - {"status_server.py"}
assert "broker-token" in config
assert config_dirs == directories
def _forge_secret_entries():
return {
name: (f'fixture:{index}'.encode(), module.FORGE_SECRET_MODE)
for index, name in enumerate(sorted(module.FORGE_SECRET_OWNERS))
}
def _release_with_config(*, forge_secrets=False):
release = {
name: (f'fixed:{name}\n'.encode(), 0o644)
for name in module.RELEASE_FILES
}
manifest = {
'apiVersion': 'itworx.controller-artifact/v1',
'controllerVersion': '2.1.14',
'sourceRepository': 'jens/unraid-appops-gateway',
'sourceRevision': REVISION,
'sourceTree': 'c' * 40,
'artifactSha256': module._artifact_digest(release),
'builtAt': '2026-10-04T00:00:00Z',
}
release['controller-release.json'] = (json.dumps(manifest).encode(), 0o644)
config = {'broker-token': (b'fixture-token', 0o600)}
directories = {'': 0o755}
if forge_secrets:
config.update(_forge_secret_entries())
directories[module.FORGE_SECRET_DIRECTORY] = 0o700
return (release, {'': 0o755}), (release | config, directories)
def _fake_release_writer(monkeypatch, state):
writer_id = 'f' * 64
calls = []
def run(arguments, *, data=None, **_kwargs):
calls.append(arguments)
if arguments[:2] == ['docker', 'create']:
return writer_id.encode()
if arguments[:2] == ['docker', 'wait']:
return b'0'
if arguments[:2] == ['docker', 'cp']:
state['tree'] = module._read_tar_tree(data)
with tarfile.open(fileobj=io.BytesIO(data), mode='r:') as archive:
archive_mode = '-a' in arguments
state['owners'] = {
member.name: ((0, 0) if archive_mode else (member.uid, member.gid))
for member in archive.getmembers() if member.isfile()
}
return b''
monkeypatch.setattr(module, 'ensure_volume', lambda **_kwargs: None)
monkeypatch.setattr(module, '_remove_stale_writer', lambda _image_id: None)
monkeypatch.setattr(module, '_inspect_writer', lambda *_args, **_kwargs: {})
monkeypatch.setattr(module, '_run', run)
return calls
def test_existing_exact_release_subset_is_augmented_and_read_back(monkeypatch):
old, expected = _release_with_config()
state = {'tree': old}
monkeypatch.setattr(module, '_read_release_leaf', lambda _revision: state['tree'])
calls = _fake_release_writer(monkeypatch, state)
module._install_release_leaf(REVISION, expected, IMAGE_ID)
assert state['tree'] == expected
assert len([call for call in calls if call[:2] == ['docker', 'create']]) == 1
assert len([call for call in calls if call[:2] == ['docker', 'cp']]) == 1
def test_divergent_existing_release_refuses_before_writer(monkeypatch):
old, expected = _release_with_config()
damaged = dict(old[0])
damaged['status_server.py'] = (b'tampered', 0o644)
monkeypatch.setattr(module, '_read_release_leaf', lambda _revision: (damaged, old[1]))
calls = _fake_release_writer(monkeypatch, {'tree': None})
with pytest.raises(module.VolumeTransitionError, match='release_collision'):
module._install_release_leaf(REVISION, expected, IMAGE_ID)
assert not any(call[:2] == ['docker', 'create'] for call in calls)
def test_complete_existing_release_uses_no_writer(monkeypatch):
_old, expected = _release_with_config()
monkeypatch.setattr(module, '_read_release_leaf', lambda _revision: expected)
calls = _fake_release_writer(monkeypatch, {'tree': None})
module._install_release_leaf(REVISION, expected, IMAGE_ID)
assert not any(call[:2] == ['docker', 'create'] for call in calls)
def test_release_copy_preserves_fixed_forge_file_owners_on_readback(monkeypatch):
_old, expected = _release_with_config(forge_secrets=True)
state = {'tree': None, 'owners': {}}
def read_leaf(_revision):
if state['tree'] is not None:
for name, owner in module.FORGE_SECRET_OWNERS.items():
if state['owners'].get(name) != owner:
raise module.VolumeTransitionError('controller_volume_tree_file_unsafe')
return state['tree']
monkeypatch.setattr(module, '_read_release_leaf', read_leaf)
calls = _fake_release_writer(monkeypatch, state)
module._install_release_leaf(REVISION, expected, IMAGE_ID)
assert state['tree'] == expected
assert all(state['owners'][name] == owner
for name, owner in module.FORGE_SECRET_OWNERS.items())
assert len([call for call in calls if call[:2] == ['docker', 'cp']]) == 1
assert next(call for call in calls if call[:2] == ['docker', 'cp']) == [
'docker', 'cp', '-',
f"{'f' * 64}:{module.TARGET}/{module.ARTIFACTS}/{REVISION}",
]
def test_complete_forge_secret_bundle_has_exact_export_modes_and_owners():
entries = {
'projects.json': (b'{}', 0o600),
'broker-token': (b'fixture-token', 0o600),
'gitea-validation.json': (b'{}', 0o600),
'gitea-validation-token': (b'fixture-validation-token', 0o600),
} | _forge_secret_entries()
directories = {'': 0o755, module.FORGE_SECRET_DIRECTORY: 0o700}
config, config_dirs = module._config_tree(entries, directories)
archive_bytes = module._release_tar_bytes(REVISION, (config, config_dirs))
restored, restored_dirs = module._read_tar_tree(archive_bytes)
assert restored == config
assert restored_dirs == config_dirs
with tarfile.open(fileobj=io.BytesIO(archive_bytes), mode='r:') as archive:
for name, owner in module.FORGE_SECRET_OWNERS.items():
member = archive.getmember(name)
assert (member.uid, member.gid) == owner
assert member.mode == module.FORGE_SECRET_MODE
assert archive.getmember('broker-token').uid == 0
assert archive.getmember('broker-token').gid == 0
@pytest.mark.parametrize('damage', ['partial', 'unknown', 'wrong_mode', 'nested'])
def test_forge_secret_snapshot_requires_the_complete_exact_private_tree(damage):
entries = {
'projects.json': (b'{}', 0o600),
'broker-token': (b'fixture-token', 0o600),
'gitea-validation.json': (b'{}', 0o600),
'gitea-validation-token': (b'fixture-validation-token', 0o600),
} | _forge_secret_entries()
directories = {'': 0o755, module.FORGE_SECRET_DIRECTORY: 0o700}
if damage == 'partial':
entries.pop(min(module.FORGE_SECRET_OWNERS))
elif damage == 'unknown':
entries[f'{module.FORGE_SECRET_DIRECTORY}/unexpected.secret'] = (b'x', 0o400)
elif damage == 'wrong_mode':
name = min(module.FORGE_SECRET_OWNERS)
entries[name] = (entries[name][0], 0o600)
else:
directories[f'{module.FORGE_SECRET_DIRECTORY}/nested'] = 0o700
with pytest.raises(module.VolumeTransitionError):
module._config_tree(entries, directories)
def test_forge_secret_tar_owner_drift_is_rejected():
output = io.BytesIO()
with tarfile.open(fileobj=output, mode='w', format=tarfile.USTAR_FORMAT) as archive:
member = tarfile.TarInfo(
f'{module.FORGE_SECRET_DIRECTORY}/runtime_database_url.secret'
)
member.size = 1
member.mode = module.FORGE_SECRET_MODE
member.uid = member.gid = 0
archive.addfile(member, io.BytesIO(b'x'))
with pytest.raises(module.VolumeTransitionError, match='entry_unsafe'):
module._read_tar_tree(output.getvalue())
def test_config_writer_uses_tar_ownership_without_copy_archive_mode(monkeypatch):
tree = (
{
'projects.json': (b'{}', 0o600),
'broker-token': (b'fixture-token', 0o600),
'gitea-validation.json': (b'{}', 0o600),
'gitea-validation-token': (b'fixture-validation-token', 0o600),
} | _forge_secret_entries(),
{'': 0o755, module.FORGE_SECRET_DIRECTORY: 0o700},
)
writer_id = 'f' * 64
expected_archive = module._release_tar_bytes('a' * 40, tree)
commands = []
copy_payloads = []
copy_owners = {}
def run(arguments, *, data=None, **_kwargs):
commands.append(arguments)
if arguments == ['docker', 'create']:
return writer_id.encode()
if arguments[:2] == ['docker', 'wait']:
return b'0'
if arguments[:2] == ['docker', 'cp']:
copy_payloads.append(data)
with tarfile.open(fileobj=io.BytesIO(data), mode='r:') as archive:
archive_mode = '-a' in arguments
copy_owners.update({
member.name: ((0, 0) if archive_mode else (member.uid, member.gid))
for member in archive.getmembers() if member.isfile()
})
return b''
monkeypatch.setattr(module, '_read_partial_config_leaf', lambda: None)
monkeypatch.setattr(module, 'ensure_volume', lambda **_kwargs: None)
monkeypatch.setattr(module, '_release_tar_bytes', lambda *_args, **_kwargs: expected_archive)
monkeypatch.setattr(module, '_remove_stale_writer', lambda *_args, **_kwargs: None)
monkeypatch.setattr(module, '_writer_spec', lambda *_args, **_kwargs: ['docker', 'create'])
monkeypatch.setattr(module, '_inspect_writer', lambda *_args, **_kwargs: {})
monkeypatch.setattr(module, '_read_config_leaf', lambda: tree)
monkeypatch.setattr(module, '_run', run)
module._install_config_tree(tree, IMAGE_ID)
assert [command for command in commands if command[:2] == ['docker', 'cp']] == [[
'docker', 'cp', '-', f'{writer_id}:{module.TARGET}/{module.CONFIG_DIR}',
]]
assert copy_payloads == [expected_archive]
assert all(copy_owners[name] == owner
for name, owner in module.FORGE_SECRET_OWNERS.items())
def test_partial_forge_bundle_fails_during_promotion_preparation_before_stop(
monkeypatch, tmp_path
):
valid_config_tree = module._config_tree
events = _prepare_promotion_doubles(monkeypatch, tmp_path)
monkeypatch.setattr(module, '_config_tree', valid_config_tree)
partial = {
'projects.json': (b'{}', 0o600),
'broker-token': (b'fixture-token', 0o600),
'gitea-validation.json': (b'{}', 0o600),
'gitea-validation-token': (b'fixture-validation-token', 0o600),
min(module.FORGE_SECRET_OWNERS): (b'fixture', module.FORGE_SECRET_MODE),
}
directories = {'': 0o755, module.FORGE_SECRET_DIRECTORY: 0o700}
identity = {
'sourceRevision': 'b' * 40,
'sourceTree': 'c' * 40,
'controllerVersion': '2.1.0',
}
monkeypatch.setattr(
module, '_snapshot_attested_controller',
lambda *_args, **_kwargs: (partial, directories, identity),
)
with pytest.raises(module.VolumeTransitionError, match='forge_secret_bundle_incomplete'):
module.prepare_promotion(
tmp_path / 'stage',
source_revision='d' * 40,
source_tree='e' * 40,
controller_version='2.2.0',
image_id=IMAGE_ID,
)
assert 'backup_volume_prepared' not in events
@pytest.mark.skipif(os.name == 'nt' or not hasattr(os, 'geteuid') or os.geteuid() != 0,
reason='fixed Forge ownership roundtrip requires Linux root')
def test_forge_secret_filesystem_capture_and_private_stage_keep_exact_owner(tmp_path):
config_root = tmp_path / 'config'
config_root.mkdir(mode=0o700)
secret_root = config_root / module.FORGE_SECRET_DIRECTORY
secret_root.mkdir(mode=0o700)
for index, (relative, owner) in enumerate(sorted(module.FORGE_SECRET_OWNERS.items())):
path = config_root / relative
path.write_bytes(f'fixture:{index}'.encode())
os.chown(path, *owner)
path.chmod(module.FORGE_SECRET_MODE)
descriptor = os.open(config_root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
try:
entries, directories = module._read_tree_fd(descriptor, allow_forge_secrets=True)
finally:
os.close(descriptor)
config, config_dirs = module._config_tree(entries, directories, allow_partial=True)
payload = module._release_tar_bytes(REVISION, (config, config_dirs))
roundtrip, roundtrip_dirs = module._read_tar_tree(payload)
assert roundtrip == config
assert roundtrip_dirs == config_dirs
staged = tmp_path / 'private-stage'
module._write_private_tree(staged, config, config_dirs)
for relative, owner in module.FORGE_SECRET_OWNERS.items():
info = (staged / relative).stat(follow_symlinks=False)
assert (info.st_uid, info.st_gid) == owner
assert stat.S_IMODE(info.st_mode) == module.FORGE_SECRET_MODE
def test_unknown_config_file_is_rejected():
entries = {
"projects.json": (b"{}", 0o600),
"broker-token": (b"secret", 0o600),
"gitea-validation.json": (b"{}", 0o600),
"gitea-validation-token": (b"secret", 0o600),
"shell.py": (b"unexpected", 0o600),
}
with pytest.raises(module.VolumeTransitionError, match="config_snapshot_unknown"):
module._config_tree(entries, {"": 0o755})
def test_partial_config_tree_is_accepted_only_for_crash_resume():
partial = {"projects.json": (b"{}", 0o600), "broker-token": (b"secret", 0o600)}
assert module._config_tree(partial, {"": 0o755}, allow_partial=True) == (
partial, {"": 0o755}
)
with pytest.raises(module.VolumeTransitionError, match="snapshot_incomplete"):
module._config_tree(partial, {"": 0o755})
def test_config_snapshot_requires_private_mode_for_every_file():
entries = {
"projects.json": (b"{}", 0o644),
"broker-token": (b"secret", 0o600),
"gitea-validation.json": (b"{}", 0o600),
"gitea-validation-token": (b"secret", 0o600),
}
with pytest.raises(module.VolumeTransitionError, match="config_file_unsafe"):
module._config_tree(entries, {"": 0o755})
def test_previous_private_tls_is_validated_from_the_attested_config_snapshot(
tmp_path, monkeypatch
):
previous = tmp_path / "previous-controller"
previous.mkdir(mode=0o700)
(previous / "status_server.py").write_text("# captured validator\n", encoding="utf-8")
(previous / "private-tls.json").write_bytes(b"captured private config")
calls = []
monkeypatch.setattr(module, "_run", lambda arguments, **kwargs: calls.append((arguments, kwargs)) or b"")
module._validate_previous_private_tls(tmp_path)
assert calls == [(
["python3", str(previous / "status_server.py"), "--check-private-tls-config"],
{"max_output": 4096},
)]
(previous / "private-tls.json").unlink()
calls.clear()
module._validate_previous_private_tls(tmp_path)
assert calls == []
def test_staged_config_checks_run_new_artifact_with_copied_config_and_bounded_output(
tmp_path, monkeypatch
):
stage_root = tmp_path / "source"
stage_root.mkdir(mode=0o700)
release_tree = (
{
"status_server.py": (b"new status server", 0o644),
"controller-release.json": (b"new release identity", 0o644),
},
{"": 0o755},
)
config_tree = (
{
"projects.json": (b"{\"repo\":{}}", 0o600),
"broker-token": (b"private broker token", 0o600),
"gitea-validation.json": (b"{}", 0o600),
"gitea-validation-token": (b"private validation token", 0o600),
"private-tls.json": (b"private TLS config", 0o600),
"webhook-secrets/repo": (b"private webhook secret", 0o600),
},
{"": 0o755, "webhook-secrets": 0o700},
)
calls = []
validation_root = stage_root / "validation"
def run(arguments, *, data=None, max_output=module.MAX_BYTES):
calls.append((arguments, max_output))
assert (validation_root / "status_server.py").read_bytes() == b"new status server"
assert (validation_root / "projects.json").read_bytes() == b"{\"repo\":{}}"
assert (validation_root / "broker-token").read_bytes() == b"private broker token"
assert (validation_root / "webhook-secrets" / "repo").read_bytes() == b"private webhook secret"
return b"validated"
monkeypatch.setattr(module, "_write_private_tree", _write_test_tree)
monkeypatch.setattr(module, "_run", run)
validator = getattr(module, "_validate_staged_configuration", None)
assert callable(validator), "staged configuration validation helper is required"
validator(stage_root, release_tree, config_tree)
assert calls == [
(["python3", str(validation_root / "status_server.py"), "--check-artifact"], 4096),
(["python3", str(validation_root / "status_server.py"), "--check-config"], 4096),
(["python3", str(validation_root / "status_server.py"), "--check-validation-config"], 4096),
(["python3", str(validation_root / "status_server.py"), "--check-validation-api"], 4096),
(["python3", str(validation_root / "status_server.py"), "--check-private-tls-config"], 4096),
]
assert not validation_root.exists()
def test_staged_config_check_failure_is_sanitized_and_cleans_private_validation_tree(
tmp_path, monkeypatch
):
stage_root = tmp_path / "source"
stage_root.mkdir(mode=0o700)
release_tree = (
{
"status_server.py": (b"new status server", 0o644),
"controller-release.json": (b"new release identity", 0o644),
},
{"": 0o755},
)
config_tree = (
{
"projects.json": (b"{}", 0o600),
"broker-token": (b"top-secret-broker", 0o600),
"gitea-validation.json": (b"{}", 0o600),
"gitea-validation-token": (b"top-secret-validation", 0o600),
},
{"": 0o755},
)
class FailedProcess:
returncode = 1
stdout = b"token=top-secret-validation"
stderr = b"secret=top-secret-broker"
monkeypatch.setattr(module, "_write_private_tree", _write_test_tree)
monkeypatch.setattr(module.subprocess, "run", lambda *args, **kwargs: FailedProcess())
validator = getattr(module, "_validate_staged_configuration", None)
assert callable(validator), "staged configuration validation helper is required"
with pytest.raises(module.VolumeTransitionError, match="command_failed") as error:
validator(stage_root, release_tree, config_tree)
assert "top-secret" not in str(error.value)
assert not (stage_root / "validation").exists()
def _write_test_tree(path, entries, directories):
path.mkdir(mode=0o700)
for relative, _mode in sorted(directories.items(), key=lambda item: item[0].count("/")):
if relative:
(path / relative).mkdir(parents=True, exist_ok=True)
for relative, (contents, _mode) in entries.items():
destination = path / relative
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(contents)
@pytest.mark.skipif(os.name == "nt", reason="private TLS snapshot guard requires POSIX symlink semantics")
def test_previous_private_tls_rejects_symlinked_snapshot(tmp_path):
previous = tmp_path / "previous-controller"
previous.mkdir(mode=0o700)
(previous / "status_server.py").write_text("# captured validator\n", encoding="utf-8")
target = tmp_path / "tls.json"
target.write_bytes(b"unsafe")
(previous / "private-tls.json").symlink_to(target)
with pytest.raises(module.VolumeTransitionError, match="private_tls_unsafe"):
module._validate_previous_private_tls(tmp_path)
@pytest.mark.parametrize("kind", ["artifacts", "config"])
def test_writer_is_exact_image_only_bounded_and_has_no_host_paths(kind):
command = module._writer_spec(IMAGE_ID, REVISION, kind=kind)
assert command[0:3] == ["docker", "create", "--name"]
assert module.WRITER_NAME in command
assert IMAGE_ID in command
assert "--network" in command and command[command.index("--network") + 1] == "none"
assert command[command.index("--user") + 1] == "0:0"
assert "--read-only" in command and "--cap-drop" in command
assert "--cpu-quota" in command and "--memory" in command and "--pids-limit" in command
assert "--mount" in command
mount = command[command.index("--mount") + 1]
assert mount == f"type=volume,source={module.VOLUME},target={module.TARGET},volume-nocopy"
assert not any(item.startswith(("/mnt/", "/var/lib/")) for item in command)
@pytest.mark.parametrize("action", ["writer_copy", "config_writer_copy"])
def test_copy_diagnostic_classifies_missing_passwd_user_without_retaining_user_value(action):
stderr = (b'Error response from daemon: getent unable to find entry '
b'"fixture-user-name" in passwd database')
assert module._command_stderr_condition(stderr, action, max_bytes=1024) == (
"passwd_user_lookup_failed"
)
diagnostic = module._safe_command_diagnostic(
(action, "exit_1", "passwd_user_lookup_failed")
)
assert diagnostic == f"{action}_exit_1_passwd_user_lookup_failed"
assert "fixture-user-name" not in diagnostic
@pytest.mark.parametrize('stderr,condition', [
(b'cannot create /private/fixture-secret: No space left on device', 'storage_full'),
(b'Cannot connect to the Docker daemon at unix:///private/fixture-secret', 'daemon_unavailable'),
])
def test_storage_command_diagnostics_never_retain_private_paths(stderr, condition):
assert module._command_stderr_condition(stderr, 'compose_up', max_bytes=1024) == condition
diagnostic = module._safe_command_diagnostic(('compose_up', 'exit_1', condition))
assert diagnostic == f'compose_up_exit_1_{condition}'
assert 'fixture-secret' not in diagnostic
@pytest.mark.parametrize('condition', [
'image_metadata_failed', 'inspect_failed', 'identity_invalid', 'reinspect_failed',
'identity_changed', 'remove_failed', 'remove_storage_full', 'remove_read_only',
'remove_permission_denied', 'remove_daemon_unavailable', 'remove_busy',
'remove_container_missing', 'remove_api_version_unsupported', 'absence_failed', 'unverified',
])
def test_probe_child_diagnostics_survive_promotion_without_raw_output(monkeypatch, condition):
stderr = ('resource_cgroup_probe_cleanup_' + condition + '\n').encode()
monkeypatch.setattr(module.subprocess, 'run', lambda *a, **kw: types.SimpleNamespace(
returncode=1, stderr=stderr, stdout=b'Bearer fixture-secret'))
with pytest.raises(module.VolumeTransitionError) as error:
module._run(['python3', 'qualified-validator'], diagnostic_action='probe_cleanup', max_output=4096)
assert module._safe_command_diagnostic(error.value.command_failure) == (
'probe_cleanup_exit_1_probe_' + condition)
assert 'fixture-secret' not in str(error.value)
@pytest.mark.parametrize('stderr', [
b'resource_cgroup_probe_cleanup_remove_failed Bearer fixture-secret',
b'resource_cgroup_probe_cleanup_remove_failed\nBearer fixture-secret',
b'resource_cgroup_probe_cleanup_fixture-secret', b'permission denied fixture-secret',
b'resource_cgroup_probe_cleanup_\xff',
b'resource_cgroup_probe_cleanup_remove_failed' + b'x' * 4096,
])
def test_probe_child_rejects_untrusted_or_multiline_diagnostics(stderr):
assert module._command_stderr_condition(stderr, 'probe_cleanup', max_bytes=4096) is None
@pytest.mark.parametrize('read_only,free_bytes', [(True, 1024**3), (False, 0)])
def test_stop_rejects_failed_storage_before_recording_intent_or_stopping(monkeypatch, read_only, free_bytes):
receipt = {'phase': 'prepared'}
monkeypatch.setattr(module, '_load_transaction', lambda _: (42, receipt, {}))
monkeypatch.setattr(module.os, 'close', lambda _: None)
monkeypatch.setattr(module.os, 'statvfs', lambda _: types.SimpleNamespace(
f_flag=1 if read_only else 0, f_bavail=free_bytes, f_frsize=1,
), raising=False)
def forbidden(*args, **kwargs):
pytest.fail('Storage rejection must happen before any persisted stop intent or container stop')
monkeypatch.setattr(module, '_set_promotion_phase', forbidden)
monkeypatch.setattr(module, '_stop_current_controller', forbidden)
code = 'controller_volume_storage_read_only' if read_only else 'controller_volume_storage_insufficient'
with pytest.raises(module.VolumeTransitionError, match=code):
module.stop_promotion(Path('/unused-backup'))
assert receipt['phase'] == 'prepared'
@pytest.mark.parametrize('native_read_only_graph', [False, True])
def test_stop_allows_healthy_storage_and_preserves_intent_before_stop(monkeypatch, native_read_only_graph):
monkeypatch.setattr(module, '_cleanup_promotion_host_probe', lambda *a: nullcontext((None, lambda: None)))
receipt = {'phase': 'prepared'}
events = []
monkeypatch.setattr(module, '_load_transaction', lambda _: (42, receipt, {}))
monkeypatch.setattr(module.os, 'close', lambda _: None)
def filesystem(path):
events.append(('storage', path))
return types.SimpleNamespace(f_flag=1 if native_read_only_graph and path == module.DOCKER_ROOT else 0, f_bavail=1024**3, f_frsize=1)
monkeypatch.setattr(module.os, 'statvfs', filesystem, raising=False)
def persist(descriptor, value, phase):
events.append(('intent', phase))
return {**value, 'phase': phase}
monkeypatch.setattr(module, '_set_promotion_phase', persist)
monkeypatch.setattr(module, '_stop_current_controller', lambda *a, **kw: events.append(('stop', None)))
module.stop_promotion(Path('/unused-backup'))
assert events == [('storage', module.ROOT), ('storage', module.DOCKER_ROOT), ('intent', 'stopped'), ('stop', None)]
def test_stop_rejects_full_graph_even_with_native_read_only_bind(monkeypatch):
receipt = {'phase': 'prepared'}
monkeypatch.setattr(module, '_load_transaction', lambda _: (42, receipt, {}))
monkeypatch.setattr(module.os, 'close', lambda _: None)
monkeypatch.setattr(module.os, 'statvfs', lambda p: types.SimpleNamespace(
f_flag=1 if p == module.DOCKER_ROOT else 0,
f_bavail=0 if p == module.DOCKER_ROOT else 1024**3, f_frsize=1,
), raising=False)
def forbidden(*args, **kwargs):
pytest.fail('Full graph must not record a stop or interrupt the controller')
monkeypatch.setattr(module, '_set_promotion_phase', forbidden)
monkeypatch.setattr(module, '_stop_current_controller', forbidden)
with pytest.raises(module.VolumeTransitionError, match='controller_volume_storage_insufficient'):
module.stop_promotion(Path('/unused-backup'))
def test_promoted_compose_actions_use_only_stdin_and_fixed_service(compose):
baseline, calls = compose
rendered = module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
module.compose_action(rendered, "stop")
module.compose_action(rendered, "up")
assert calls[-2][0][-2:] == ["stop", module.SERVICE]
up_index = calls[-1][0].index("up")
assert calls[-1][0][up_index:] == [
"up", "-d", "--force-recreate", "--pull", "never", "--no-deps", module.SERVICE
]
assert calls[-2][1] == rendered
assert calls[-1][1] == rendered
assert baseline["name"] == "gitea-deploy-runners"
@pytest.mark.parametrize(
("operation", "phase"),
[("activate", "stopped"), ("rollback", "stopped"), ("rollback", "activated")],
)
def test_promotion_uses_projected_persistent_compose_without_dependencies(
compose, tmp_path, monkeypatch, operation, phase,
):
_baseline, calls = compose
previous = REVISION
target = "c" * 40
previous_compose = _legacy_private_compose(previous)
target_compose = _legacy_private_compose(target)
receipt = {
"phase": phase,
"imageId": IMAGE_ID,
"previousRevision": previous,
"sourceRevision": target,
"composeBeforeSha256": module._hash(b"captured-source-compose"),
"composeAfterSha256": module._hash(target_compose),
"composeRollbackSha256": module._hash(previous_compose),
}
values = {
"controller-before.tar": b"previous tree",
"controller-after.tar": b"target tree",
"mirror-before.tar": b"mirror snapshot",
"compose-rollback.json": previous_compose,
"compose-after.json": target_compose,
}
current = {"Mounts": [{
"Destination": module.EXECUTABLE_TARGET,
"Source": module._expected_revision_path(target).as_posix(),
}]}
live_file = {"value": target_compose, "mode": 0o640}
monkeypatch.setattr(module, "_load_transaction", lambda _backup: (1, receipt, values))
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
monkeypatch.setattr(module, "_controller_image_inspect", lambda *args, **kwargs: current)
monkeypatch.setattr(module, "_read_tar_tree", lambda _value: ({}, {"": 0o755}))
monkeypatch.setattr(module, "_verify_release_leaf", lambda *args, **kwargs: True)
monkeypatch.setattr(module, "_sync_controller_mirror", lambda *args, **kwargs: None)
monkeypatch.setattr(module, "_stop_current_controller", lambda *args, **kwargs: None)
monkeypatch.setattr(module, "_verify_compose_image", lambda *args, **kwargs: None)
monkeypatch.setattr(module, "_verify_active_mount", lambda *args, **kwargs: None)
monkeypatch.setattr(module, "_set_promotion_phase", lambda *args, **kwargs: None)
monkeypatch.setattr(module, "_directory", lambda *_args: 23)
monkeypatch.setattr(
module, "_file_info_at", lambda *_args: (live_file["value"], live_file["mode"])
)
monkeypatch.setattr(module, "_read_backup", lambda *_args: b'{"composeMode":416}')
def atomic_at(_parent, _name, value, expected, **_kwargs):
assert expected == live_file["value"]
live_file["value"] = value
monkeypatch.setattr(module, "_atomic_at", atomic_at)
calls.clear()
if operation == "activate":
module.activate_promotion(tmp_path / "backup")
raw_target = target_compose
else:
module.rollback_promotion(tmp_path / "backup")
raw_target = previous_compose
projected_target = _projected_compose(raw_target)
assert live_file["value"] == projected_target
up_calls = [
(arguments, data) for arguments, data in calls
if arguments[:2] == ["docker", "--config"]
and arguments[3] == "compose" and "up" in arguments
]
assert len(up_calls) == 1
up_index = up_calls[0][0].index("up")
assert up_calls[0][0][up_index:] == [
"up", "-d", "--force-recreate", "--pull", "never", "--no-deps", module.SERVICE
]
assert up_calls[0][1] == projected_target
assert module._hash(target_compose) == receipt["composeAfterSha256"]
assert module._hash(previous_compose) == receipt["composeRollbackSha256"]
assert values["compose-after.json"] == target_compose
assert values["compose-rollback.json"] == previous_compose
def test_installer_has_no_shared_executable_stage_or_compose_path():
source = (ROOT / "scripts/upgrade_deployment_controller.sh").read_text(encoding="utf-8")
assert "mktemp -d -p /tmp \"itworx-controller-upgrade-" in source
assert 'docker compose --project-directory "$ROOT" -f "$COMPOSE_FILE"' not in source
assert 'python3 "${work}/${PREPARER_NAME}"' in source
assert 'mv -- "$CONTROLLER_DIR"' not in source
assert 'cp -a -- "$CONTROLLER_DIR"' not in source
assert "ops/gitea-deploy-broker/mcp_hub_release_store.py" in source
def test_installer_release_order_matches_renderer_contract():
renderer_spec = importlib.util.spec_from_file_location(
"controller_renderer", ROOT / "scripts/prepare_controller_artifact.py"
)
renderer = importlib.util.module_from_spec(renderer_spec)
renderer_spec.loader.exec_module(renderer)
assert tuple(module.RELEASE_FILES) == tuple(renderer._ARTIFACT_FILES)
assert len(renderer._ARTIFACT_FILES) == len(set(renderer._ARTIFACT_FILES))
assert module.RELEASE_FILES[:3] == (
"controller_paths.py", "mcp_hub_release_store.py", "hub_permission_preparation.py"
)
candidate_helper = "prepare_mcp_hub_candidate.py"
assert candidate_helper in module.RELEASE_FILES
assert candidate_helper in renderer._ARTIFACT_FILES
assert module.RELEASE_FILES.count(candidate_helper) == 1
previous_files = module.LEGACY_RELEASE_FILES_PRE_CANDIDATE_HELPER
assert len(previous_files) == 20
previous_with_helper = module.LEGACY_RELEASE_FILES_WITH_CANDIDATE_HELPER
assert len(previous_with_helper) == 21
assert previous_with_helper == tuple(
[*previous_files[:previous_files.index("status_server.py") + 1], candidate_helper,
*previous_files[previous_files.index("status_server.py") + 1:]]
)
assert module.LEGACY_RELEASE_FILES_WITH_CONFIG_ROOT == HISTORICAL_CONFIG_ROOT_RELEASE_FILES
assert module._KNOWN_RELEASE_FILE_SETS == (
module.RELEASE_FILES,
module.LEGACY_RELEASE_FILES_PRE_QUIESCENCE,
module.LEGACY_RELEASE_FILES_PRE_COMPONENT,
module.LEGACY_RELEASE_FILES_WITH_ROLE_HELPER,
module.LEGACY_RELEASE_FILES_WITHOUT_ROLE_HELPER,
module.LEGACY_RELEASE_FILES_BEFORE_FORGE,
previous_files,
previous_with_helper,
HISTORICAL_CONFIG_ROOT_RELEASE_FILES,
module.LEGACY_RELEASE_FILES_2_1_13,
module.LEGACY_RELEASE_FILES_2_1_5,
)
assert all(
candidate_helper not in legacy
for legacy in (
previous_files,
module.LEGACY_RELEASE_FILES_WITH_CONFIG_ROOT,
module.LEGACY_RELEASE_FILES_2_1_13,
module.LEGACY_RELEASE_FILES_2_1_5,
)
)
assert module.RELEASE_FILES[module.RELEASE_FILES.index("forge_first_install.py") + 1] == (
"forge_database_credentials.py"
)
helper_index = module.RELEASE_FILES.index("forge_database_credentials.py") + 1
assert module.RELEASE_FILES[helper_index : helper_index + 2] == (
"forge_backup_paths.py", "forge_secret_bundle.py"
)
assert module.LEGACY_RELEASE_FILES_WITH_ROLE_HELPER == tuple(
name for name in module.RELEASE_FILES
if name not in {"forge_backup_paths.py", "forge_secret_bundle.py", "immutable_service_promotion.py", "registered_legacy_adoption.py", "registered_legacy_transport.py", "registered_build_context.py", "runner_acceptance_quiescence.py"}
)
assert module.LEGACY_RELEASE_FILES_WITHOUT_ROLE_HELPER == tuple(
name for name in module.LEGACY_RELEASE_FILES_WITH_ROLE_HELPER
if name != "forge_database_credentials.py"
)
installer = (ROOT / "scripts/upgrade_deployment_controller.sh").read_text(encoding="utf-8")
module_list = installer.split("module_files=(", 1)[1].split(")", 1)[0].split()
module_helper_index = module_list.index("forge_database_credentials.py") + 1
assert module_list[module_helper_index : module_helper_index + 2] == [
"forge_backup_paths.py", "forge_secret_bundle.py"
]
source_list = installer.split("source_paths=(", 1)[1].split(")", 1)[0].split()
source_helper_index = source_list.index("ops/gitea-deploy-broker/forge_database_credentials.py") + 1
assert source_list[source_helper_index : source_helper_index + 2] == [
"ops/gitea-deploy-broker/forge_backup_paths.py",
"ops/gitea-deploy-broker/forge_secret_bundle.py",
]
def test_pre_candidate_20_file_manifest_remains_accepted_and_current_digest_requires_helper():
files = HISTORICAL_PRE_CANDIDATE_RELEASE_FILES
assert module.LEGACY_RELEASE_FILES_PRE_CANDIDATE_HELPER == files
entries = {name: (f"pre-candidate:{name}".encode(), 0o644) for name in files}
manifest = {
"apiVersion": "itworx.controller-artifact/v1",
"controllerVersion": "2.1.14",
"sourceRepository": "jens/unraid-appops-gateway",
"sourceRevision": "b" * 40,
"sourceTree": "c" * 40,
"artifactSha256": module._artifact_digest(entries, files),
"builtAt": "2026-10-02T00:00:00.000Z",
}
entries["controller-release.json"] = (
json.dumps(manifest, separators=(",", ":")).encode(), 0o644
)
identity = module._release_identity(entries)
assert identity["artifactSha256"] == manifest["artifactSha256"]
assert module._matching_release_files(entries, manifest["artifactSha256"]) == files
current_entries = {name: (f"current:{name}".encode(), 0o644) for name in module.RELEASE_FILES}
assert module._artifact_digest(current_entries) == module._artifact_digest(
current_entries, module.RELEASE_FILES
)
del current_entries["prepare_mcp_hub_candidate.py"]
with pytest.raises(module.VolumeTransitionError, match="release_incomplete"):
module._artifact_digest(current_entries)
def test_previous_21_file_helper_manifest_remains_accepted():
files = HISTORICAL_CANDIDATE_HELPER_RELEASE_FILES
assert module.LEGACY_RELEASE_FILES_WITH_CANDIDATE_HELPER == files
assert len(files) == 21
assert files.count("prepare_mcp_hub_candidate.py") == 1
entries = {name: (f"previous-helper:{name}".encode(), 0o644) for name in files}
manifest = {
"apiVersion": "itworx.controller-artifact/v1",
"controllerVersion": "2.1.14",
"sourceRepository": "jens/unraid-appops-gateway",
"sourceRevision": "d" * 40,
"sourceTree": "e" * 40,
"artifactSha256": module._artifact_digest(entries, files),
"builtAt": "2026-10-02T00:00:00.000Z",
}
entries["controller-release.json"] = (
json.dumps(manifest, separators=(",", ":")).encode(), 0o644
)
identity = module._release_identity(entries)
assert identity["artifactSha256"] == manifest["artifactSha256"]
assert module._matching_release_files(entries, manifest["artifactSha256"]) == files
def test_candidate_preparation_helper_is_staged_archived_and_digest_bound(tmp_path):
if os.name == "nt":
pytest.skip("release staging requires POSIX descriptor-based file checks")
helper_name = "prepare_mcp_hub_candidate.py"
helper_source = ROOT / "ops" / "gitea-deploy-broker" / helper_name
helper_bytes = helper_source.read_bytes()
stage_root = tmp_path / "source"
ops = stage_root / "ops" / "gitea-deploy-broker"
scripts = stage_root / "scripts"
ops.mkdir(parents=True, mode=0o700)
scripts.mkdir(mode=0o700)
for name in module.RELEASE_FILES:
folder = scripts if name == "run_repopulse_adoption.py" else ops
contents = helper_bytes if name == helper_name else f"source:{name}\n".encode()
(folder / name).write_bytes(contents)
staged = module._stage_source_files(stage_root)
assert staged[helper_name] == (helper_bytes, 0o644)
revision = "a" * 40
tree = "b" * 40
entries, directories = module._prepare_merged_tree(
stage_root, ({}, {"": 0o755}), revision, tree, "2.1.14"
)
artifact_digest = module._release_identity(entries)["artifactSha256"]
assert artifact_digest == module._artifact_digest(entries)
archive = module._release_tar_bytes(revision, (entries, directories))
extracted, _ = module._read_tar_tree(archive)
assert extracted[helper_name][0] == helper_bytes
assert module._release_identity(extracted)["artifactSha256"] == artifact_digest
tampered = dict(extracted)
tampered[helper_name] = (helper_bytes + b"# altered after manifest\n", 0o644)
with pytest.raises(module.VolumeTransitionError, match="release_identity_invalid"):
module._release_identity(tampered)
def test_installer_explicit_archive_and_install_contract_includes_candidate_helper():
source = (ROOT / "scripts/upgrade_deployment_controller.sh").read_text(encoding="utf-8")
archive_paths = source.split("source_paths=(", 1)[1].split("\n)", 1)[0]
installed_modules = source.split("module_files=(", 1)[1].split("\n)", 1)[0]
assert "ops/gitea-deploy-broker/prepare_mcp_hub_candidate.py" in archive_paths
assert "prepare_mcp_hub_candidate.py" in installed_modules
powershell = (ROOT / "scripts/upgrade_deployment_controller.ps1").read_text(encoding="utf-8")
archive_command = next(line for line in powershell.splitlines()
if line.lstrip().startswith("& git ") and " archive --format=tar " in line)
assert "ops/gitea-deploy-broker" in archive_command
def test_current_release_digest_binds_the_role_password_helper():
entries = {name: (f"source:{name}".encode(), 0o644) for name in module.RELEASE_FILES}
expected = module._artifact_digest(entries)
assert module._matching_release_files(entries, expected) == module.RELEASE_FILES
entries["forge_database_credentials.py"] = (b"tampered helper", 0o644)
with pytest.raises(module.VolumeTransitionError, match="release_identity_invalid"):
module._matching_release_files(entries, expected)
def test_previous_role_helper_release_digest_remains_recognized_and_bound():
files = module.LEGACY_RELEASE_FILES_WITH_ROLE_HELPER
entries = {name: (f"source:{name}".encode(), 0o644) for name in files}
expected = module._artifact_digest(entries, files)
assert module._matching_release_files(entries, expected) == files
entries["forge_database_credentials.py"] = (b"tampered role helper", 0o644)
with pytest.raises(module.VolumeTransitionError, match="release_identity_invalid"):
module._matching_release_files(entries, expected)
def test_historical_config_root_manifest_order_and_digest_remain_exact():
files = HISTORICAL_CONFIG_ROOT_RELEASE_FILES
assert module.LEGACY_RELEASE_FILES_WITH_CONFIG_ROOT == files
entries = {name: (f"source:{name}".encode(), 0o644) for name in files}
expected = module._artifact_digest(entries, files)
assert module._matching_release_files(entries, expected) == files
entries["status_server.py"] = (b"tampered historical file", 0o644)
with pytest.raises(module.VolumeTransitionError, match="release_identity_invalid"):
module._matching_release_files(entries, expected)
def test_activated_controller_must_have_the_exact_backup_mount(monkeypatch):
mounts = [
{
"Type": "bind", "Source": module._expected_revision_path(REVISION).as_posix(),
"Destination": module.EXECUTABLE_TARGET, "RW": False,
},
{
"Type": "volume", "Name": module.VOLUME,
"Source": module.MOUNTPOINT.as_posix(), "Destination": module.TARGET, "RW": True,
},
{
"Type": "bind", "Source": module._expected_config_path().as_posix(),
"Destination": module.CONFIG_TARGET, "RW": False,
},
{
"Type": "bind", "Source": module.DOCKER_ROOT.as_posix(),
"Destination": module.DOCKER_ROOT.as_posix(), "RW": False,
},
{
"Type": "volume", "Name": "itworx-forge-master-key-backup",
"Source": "/var/lib/docker/volumes/itworx-forge-master-key-backup/_data",
"Destination": "/forge-master-key-backup", "RW": True,
},
]
item = {"Mounts": mounts}
monkeypatch.setattr(module, "_controller_image_inspect", lambda _image_id: item)
monkeypatch.setattr(module, "_read_release_leaf", lambda _revision: ({}, {"": 0o755}))
monkeypatch.setattr(module, "_release_identity", lambda _entries: {"sourceRevision": REVISION})
monkeypatch.setattr(module, "_read_config_leaf", lambda: ({}, {"": 0o755}))
module._verify_active_mount(REVISION, IMAGE_ID)
mounts.pop()
with pytest.raises(module.VolumeTransitionError, match="backup_mount"):
module._verify_active_mount(REVISION, IMAGE_ID)
@pytest.mark.parametrize(
"damage",
["backup_readonly", "backup_name", "backup_source", "backup_alias", "docker_root_rw"],
)
def test_activated_controller_rejects_backup_or_docker_root_mount_drift(
monkeypatch, damage
):
mounts = [
{
"Type": "bind", "Source": module._expected_revision_path(REVISION).as_posix(),
"Destination": module.EXECUTABLE_TARGET, "RW": False,
},
{
"Type": "volume", "Name": module.VOLUME,
"Source": module.MOUNTPOINT.as_posix(), "Destination": module.TARGET, "RW": True,
},
{
"Type": "bind", "Source": module._expected_config_path().as_posix(),
"Destination": module.CONFIG_TARGET, "RW": False,
},
{
"Type": "bind", "Source": module.DOCKER_ROOT.as_posix(),
"Destination": module.DOCKER_ROOT.as_posix(), "RW": False,
},
{
"Type": "volume", "Name": module.BACKUP_VOLUME,
"Source": module.BACKUP_MOUNTPOINT.as_posix(),
"Destination": module.BACKUP_TARGET, "RW": True,
},
]
if damage == "backup_readonly":
mounts[-1]["RW"] = False
elif damage == "backup_name":
mounts[-1]["Name"] = "foreign-volume"
elif damage == "backup_source":
mounts[-1]["Source"] = "/var/lib/docker/volumes/foreign/_data"
elif damage == "backup_alias":
mounts.append({
"Type": "bind", "Source": "/foreign", "Destination": f"{module.BACKUP_TARGET}/child",
"RW": True,
})
else:
mounts[-2]["RW"] = True
monkeypatch.setattr(module, "_controller_image_inspect", lambda _image_id: {"Mounts": mounts})
monkeypatch.setattr(module, "_read_release_leaf", lambda _revision: ({}, {"": 0o755}))
monkeypatch.setattr(module, "_release_identity", lambda _entries: {"sourceRevision": REVISION})
monkeypatch.setattr(module, "_read_config_leaf", lambda: ({}, {"": 0o755}))
with pytest.raises(module.VolumeTransitionError):
module._verify_active_mount(REVISION, IMAGE_ID)
@pytest.mark.skipif(os.name == "nt", reason="dirfd swap test requires POSIX openat")
def test_mirror_updates_stay_on_the_open_directory_after_path_swap(tmp_path, monkeypatch):
original = tmp_path / "controller"
moved = tmp_path / "controller-pinned"
original.mkdir()
before_entries = {
"controller_paths.py": (b"prior paths", 0o644),
"mcp_hub_release_store.py": (b"prior Hub helper", 0o644),
}
for name, (value, _mode) in before_entries.items():
(original / name).write_bytes(value)
prior = {
name: (f"prior:{name}".encode(), 0o644)
for name in module.LEGACY_RELEASE_FILES_2_1_13
}
prior["controller-release.json"] = (b"prior manifest", 0o644)
updated = {
name: (f"updated:{name}".encode(), 0o644)
for name in module.RELEASE_FILES
}
updated["controller-release.json"] = (b"updated manifest", 0o644)
prior_tar = module._release_tar_bytes("b" * 40, (prior, {"": 0o755}))
updated_tar = module._release_tar_bytes("c" * 40, (updated, {"": 0o755}))
mirror_tar = module._release_tar_bytes("a" * 40, (before_entries, {"": 0o755}))
current_path = original
monkeypatch.setattr(
module, "_directory",
lambda _path, _anchor: os.open(current_path, os.O_RDONLY | os.O_DIRECTORY),
)
read_item = module._file_info_at_optional
swapped = False
def swap_after_first_read(descriptor, name):
nonlocal swapped
value = read_item(descriptor, name)
if not swapped:
original.rename(moved)
original.mkdir()
swapped = True
return value
monkeypatch.setattr(module, "_file_info_at_optional", swap_after_first_read)
module._sync_controller_mirror(
prior_tar, updated_tar, mirror_tar, direction="forward"
)
assert swapped and list(original.iterdir()) == []
assert (moved / "controller_paths.py").read_bytes() == b"updated:controller_paths.py"
assert (moved / "mcp_hub_release_store.py").read_bytes() == b"updated:mcp_hub_release_store.py"
current_path = moved
monkeypatch.setattr(module, "_file_info_at_optional", read_item)
module._sync_controller_mirror(
updated_tar, prior_tar, mirror_tar, direction="rollback"
)
assert {path.name: path.read_bytes() for path in moved.iterdir()} == {
name: value for name, (value, _mode) in before_entries.items()
}
@pytest.mark.skipif(os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
reason="private /tmp ownership validation requires root on POSIX")
@pytest.mark.parametrize("tmp_mode", [0o700, 0o1777])
def test_private_stage_accepts_root_owned_tmp_with_only_approved_modes(monkeypatch, tmp_mode):
import shutil
import uuid
work = Path("/tmp") / f"itworx-controller-upgrade-{uuid.uuid4().hex[:12]}.Ab3xY7"
source = work / "source"
source.mkdir(parents=True, mode=0o700)
source.chmod(0o700)
work.chmod(0o700)
try:
tmp_info = os.stat("/tmp", follow_symlinks=False)
original_fstat = os.fstat
def fstat_with_tmp_mode(descriptor):
info = original_fstat(descriptor)
if (info.st_dev, info.st_ino) == (tmp_info.st_dev, tmp_info.st_ino):
return types.SimpleNamespace(
st_uid=info.st_uid, st_gid=info.st_gid,
st_mode=stat.S_IFDIR | tmp_mode, st_dev=info.st_dev,
)
return info
monkeypatch.setattr(os, "fstat", fstat_with_tmp_mode)
module._validate_private_stage(source)
finally:
monkeypatch.undo()
shutil.rmtree(work)
@pytest.mark.skipif(os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
reason="private /tmp ownership validation requires root on POSIX")
@pytest.mark.parametrize(
("target_name", "changes"),
[
("tmp", {"st_mode": stat.S_IFDIR | 0o777}),
("tmp", {"st_uid": 1000}),
("tmp", {"st_gid": 1000}),
("work", {"st_mode": stat.S_IFDIR | 0o755}),
("source", {"st_mode": stat.S_IFDIR | 0o755}),
("work", {"st_uid": 1000}),
("work", {"st_gid": 1000}),
("source", {"st_uid": 1000}),
("source", {"st_gid": 1000}),
("source", {"st_dev": 123456}),
],
)
def test_private_stage_rejects_unsafe_tmp_and_child_metadata(monkeypatch, target_name, changes):
import shutil
import uuid
work = Path("/tmp") / f"itworx-controller-upgrade-{uuid.uuid4().hex[:12]}.Ab3xY7"
source = work / "source"
source.mkdir(parents=True, mode=0o700)
source.chmod(0o700)
work.chmod(0o700)
targets = {"tmp": Path("/tmp"), "work": work, "source": source}
target_info = os.stat(targets[target_name], follow_symlinks=False)
original_fstat = os.fstat
def fstat_with_unsafe_metadata(descriptor):
info = original_fstat(descriptor)
if (info.st_dev, info.st_ino) == (target_info.st_dev, target_info.st_ino):
values = {
"st_uid": info.st_uid, "st_gid": info.st_gid,
"st_mode": info.st_mode, "st_dev": info.st_dev,
}
values.update(changes)
return types.SimpleNamespace(**values)
return info
try:
monkeypatch.setattr(os, "fstat", fstat_with_unsafe_metadata)
with pytest.raises(module.VolumeTransitionError, match="private_stage_invalid"):
module._validate_private_stage(source)
finally:
monkeypatch.undo()
shutil.rmtree(work)
@pytest.mark.skipif(os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
reason="private /tmp ownership validation requires root on POSIX")
def test_private_stage_rejects_acl_and_symlinked_source_directory(monkeypatch):
import shutil
import uuid
work = Path("/tmp") / f"itworx-controller-upgrade-{uuid.uuid4().hex[:12]}.Ab3xY7"
source = work / "source"
source.mkdir(parents=True, mode=0o700)
source.chmod(0o700)
work.chmod(0o700)
source_info = os.stat(source, follow_symlinks=False)
original_listxattr = os.listxattr
def listxattr_with_acl(descriptor):
info = os.fstat(descriptor)
values = original_listxattr(descriptor)
if (info.st_dev, info.st_ino) == (source_info.st_dev, source_info.st_ino):
return [*values, "system.posix_acl_access"]
return values
try:
monkeypatch.setattr(os, "listxattr", listxattr_with_acl)
with pytest.raises(module.VolumeTransitionError, match="acl_unsafe"):
module._validate_private_stage(source)
monkeypatch.setattr(os, "listxattr", original_listxattr)
module._validate_private_stage(source)
source.rename(work / "source-real")
source.symlink_to(work / "source-real", target_is_directory=True)
with pytest.raises(module.VolumeTransitionError, match="private_stage_invalid"):
module._validate_private_stage(source)
finally:
shutil.rmtree(work)
@pytest.mark.parametrize("directive", ["env_file", "label_file"])
def test_source_compose_preflight_rejects_host_file_reads_before_compose(compose, directive):
_baseline, calls = compose
unsafe = (
b"name: gitea-deploy-runners\nservices:\n deploy-control:\n"
+ f" image: x\n {directive}: /tmp/secret\n".encode()
)
with pytest.raises(module.VolumeTransitionError, match="external_source_invalid"):
module.rendered_model(unsafe, REVISION, IMAGE_ID)
assert calls == []
@pytest.mark.parametrize(
"source",
[
b"services:\n app:\n 'env_file': /tmp/secret\n",
b"services: {app: {\"include\": ./untrusted.yml}}\n",
b"services:\n - \"build\": ./untrusted\n",
b"services:\n app:\n \"\\u0065nv_file\": /tmp/secret\n",
],
)
def test_source_compose_preflight_rejects_quoted_or_escaped_file_keys(source):
with pytest.raises(module.VolumeTransitionError):
module._preflight_source_compose(source)
def test_preparer_uses_the_pinned_bundled_yaml_parser():
assert module.yaml.__version__ == "6.0.2"
assert module.yaml.__name__.startswith("_appops_controller_yaml_")
assert module.yaml.__with_libyaml__ is False
assert Path(module.yaml.__file__).resolve().parent == ROOT / "scripts/vendor/yaml"
def test_preparer_does_not_replace_a_host_cached_yaml_module(monkeypatch):
host_yaml = types.ModuleType("yaml")
host_yaml.__file__ = "/host/site-packages/yaml/__init__.py"
monkeypatch.setitem(sys.modules, "yaml", host_yaml)
original_sys_path = sys.path.copy()
module_name = "_controller_volume_import_isolation_test"
spec = importlib.util.spec_from_file_location(
module_name, ROOT / "scripts/prepare_controller_hub_release_volume.py"
)
assert spec is not None and spec.loader is not None
isolated_module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, module_name, isolated_module)
spec.loader.exec_module(isolated_module)
assert sys.modules["yaml"] is host_yaml
assert isolated_module.yaml.__name__.startswith("_appops_controller_yaml_")
assert isolated_module.yaml.safe_load(b"services: {}") == {"services": {}}
assert sys.path == original_sys_path
def test_source_compose_preflight_ignores_comments_strings_and_block_scalars():
source = (
b"name: gitea-deploy-runners\n"
b"# include: ./disabled.yml\n"
b"services:\n app:\n"
b" command: |\n"
b" echo 'env_file: is command text'\n"
b" echo include: is command text\n"
b" labels: {note: \"build: is a label value\"}\n"
)
module._preflight_source_compose(source)
def test_source_compose_preflight_rejects_unresolvable_alias_keys():
source = b"services:\n app:\n *unsafe-key: value\n"
with pytest.raises(module.VolumeTransitionError, match="compose_source_invalid"):
module._preflight_source_compose(source)
def test_source_compose_preflight_bounds_recursive_list_aliases():
source = b"services: {app: {image: fixed}}\nx-loop: &a [*a]\n"
module._preflight_source_compose(source)
@pytest.mark.parametrize(
"source",
[
b"services: {}\nconfigs:\n tls-material:\n file: /etc/ssl/private/key.pem\n",
b"services: {}\nsecrets: {gitea-token: {environment: GITEA_TOKEN}}\n",
],
)
def test_source_compose_preflight_rejects_local_config_and_secret_sources(source):
with pytest.raises(module.VolumeTransitionError, match="external_source_invalid"):
module._preflight_source_compose(source)
def test_source_compose_preflight_allows_external_config_and_service_environment_fields():
source = (
b"configs:\n tls-material:\n external: true\n"
b"services:\n app:\n environment:\n file: no-local-file-read\n"
)
module._preflight_source_compose(source)
def test_capture_program_is_fixed_descriptor_based_and_compiles():
legacy = module._controller_capture_source(config_root_in_code=True)
protected = module._controller_capture_source(config_root_in_code=False)
compile(legacy, "legacy-controller-capture", "exec")
compile(protected, "protected-controller-capture", "exec")
assert "config_root='/opt/gitea-deploy'" in legacy
assert "config_root='/controller-config'" in protected
assert "os.O_NOFOLLOW" in legacy and "os.fstat(fd)" in legacy
assert "open(path,'rb')" not in legacy
@pytest.mark.parametrize("legacy_config", [True, False])
def test_capture_archive_roundtrips_through_a_nonseekable_pipe(legacy_config):
source = module._controller_capture_source(config_root_in_code=legacy_config)
assert '__FORGE_' not in source
assert 'runtime_database_url.secret' in source
assert '/forge-master-key-backup' not in source
compile(source, 'controller-capture', 'exec')
statements = ast.parse(source).body
start = next(i for i, node in enumerate(statements)
if isinstance(node, ast.Assign)
and any(isinstance(target, ast.Name) and target.id == "stream"
for target in node.targets))
end = next(i for i, node in enumerate(statements[start:], start)
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call)
and isinstance(node.value.func, ast.Attribute)
and isinstance(node.value.func.value, ast.Name)
and node.value.func.value.id == "stream" and node.value.func.attr == "close")
archive_program = ast.unparse(ast.Module(body=statements[start:end + 1], type_ignores=[]))
fixture = (
"import io,sys,tarfile\n"
"records={'deploy.py':(b'fixture source',0o644),'secrets/key':(b'fixture secret',0o600)}\n"
"dirs={'secrets':0o700}\n"
"forge_secret_owners={}\n"
)
result = subprocess.run([sys.executable, "-c", fixture + archive_program],
capture_output=True, timeout=10, check=False)
assert result.returncode == 0, result.stderr.decode("utf-8", "replace")
with tarfile.open(fileobj=io.BytesIO(result.stdout), mode="r:") as archive:
assert archive.getnames() == ["secrets", "deploy.py", "secrets/key"]
assert archive.getmember("secrets").mode == 0o700
assert archive.getmember("secrets/key").mode == 0o600
assert archive.extractfile("deploy.py").read() == b"fixture source"
assert archive.extractfile("secrets/key").read() == b"fixture secret"
assert all(member.uid == member.gid == 0 for member in archive.getmembers())
@pytest.mark.parametrize("files", [
module.LEGACY_RELEASE_FILES_WITHOUT_ROLE_HELPER,
module.LEGACY_RELEASE_FILES_2_1_5,
module.LEGACY_RELEASE_FILES_WITH_CONFIG_ROOT,
module.LEGACY_RELEASE_FILES_BEFORE_FORGE,
])
def test_prior_release_capture_must_match_protected_git_tree(monkeypatch, files):
entries = {name: (f"source:{name}".encode(), 0o644) for name in files}
source_revision = "32a6eece0e3e1276946abdc04196c819daf02e2a"
source_tree = "e0d7e80b79504b964d968e51e0d21c288cac8f0d"
manifest = {
"apiVersion": "itworx.controller-artifact/v1",
"controllerVersion": "2.1.5",
"sourceRepository": "jens/unraid-appops-gateway",
"sourceRevision": source_revision,
"sourceTree": source_tree,
"artifactSha256": module._artifact_digest(entries, files),
"builtAt": "2026-10-01T00:00:00.000Z",
}
entries["controller-release.json"] = (
json.dumps(manifest, separators=(",", ":")).encode(), 0o644
)
identity = module._release_identity(entries)
outputs = {"rev-parse": (source_tree + "\n").encode()}
outputs.update({
f"ops/gitea-deploy-broker/{name}": entries[name][0]
for name in files if name != "run_repopulse_adoption.py"
})
outputs["scripts/run_repopulse_adoption.py"] = entries["run_repopulse_adoption.py"][0]
controller_id = "c" * 64
def run(arguments, **_kwargs):
assert arguments[:5] == ["docker", "exec", controller_id, "git",
f"--git-dir={module.SOURCE_GIT_DIR}"]
return outputs[arguments[-1].split(":", 1)[-1]] if arguments[-2] == "show" else outputs["rev-parse"]
monkeypatch.setattr(module, "_run", run)
module._verify_git_source_identity(entries, identity, controller_id=controller_id)
outputs["ops/gitea-deploy-broker/deploy.py"] = b"replaced source"
with pytest.raises(module.VolumeTransitionError, match="source_mismatch"):
module._verify_git_source_identity(entries, identity, controller_id=controller_id)
@pytest.mark.parametrize("controller_id", ["gitea-deploy-control", "", "a" * 63, "a" * 65])
def test_prior_release_source_verification_requires_pinned_container_id(controller_id, monkeypatch):
monkeypatch.setattr(module, "_run", lambda *_args, **_kwargs: pytest.fail("Docker must not run"))
with pytest.raises(module.VolumeTransitionError, match="container_invalid"):
module._verify_git_source_identity({}, {}, controller_id=controller_id)
@pytest.mark.parametrize(
("name", "revision", "version", "captured", "source", "accepted"),
[
("run_repopulse_adoption.py", "034c9f844d2455850ba37b31379b7edaa786583e", "2.1.12", b"one\r\ntwo\r\n", b"one\ntwo\n", True),
("deploy.py", "034c9f844d2455850ba37b31379b7edaa786583e", "2.1.12", b"one\r\ntwo\r\n", b"one\ntwo\n", False),
("run_repopulse_adoption.py", "a" * 40, "2.1.12", b"one\r\ntwo\r\n", b"one\ntwo\n", False),
("run_repopulse_adoption.py", "034c9f844d2455850ba37b31379b7edaa786583e", "2.1.13", b"one\r\ntwo\r\n", b"one\ntwo\n", False),
("run_repopulse_adoption.py", "034c9f844d2455850ba37b31379b7edaa786583e", "2.1.12", b"one\r\ntwo\n", b"one\ntwo\n", False),
("run_repopulse_adoption.py", "034c9f844d2455850ba37b31379b7edaa786583e", "2.1.12", b"one\r\nchanged\r\n", b"one\ntwo\n", False),
("run_repopulse_adoption.py", "034c9f844d2455850ba37b31379b7edaa786583e", "2.1.12", b"one\r\r\n", b"one\r\n", False),
],
)
def test_legacy_crlf_compatibility_is_limited_to_one_attested_handoff(
name, revision, version, captured, source, accepted,
):
identity = {"sourceRevision": revision, "controllerVersion": version}
assert module._matches_legacy_crlf_source(name, captured, source, identity) is accepted
@pytest.mark.parametrize("revision,tree,artifact", [
("e854334923cca574482d22b8424ba3ce46ec6aa1", "cf2a117f9222b952deb9902c646116612f3a7ea0",
"e88739bf091c8bac77ea0db5b97300666a1baa8ba984bbf6ae91e3bf25719e08"),
("aaf7d11821a86be65c91fead97a6a4758c4fbacd", "34a90f9bd06fa4efa26a19dbf5c7c13239d03721",
"ba8de75ba04f25b5298f81d107e6108bc7a92de95531b390ade0bcba9e4678e2"),
])
def test_current_attested_crlf_capture_requires_exact_full_release_identity(revision, tree, artifact):
checked_out_source = (ROOT / "scripts/run_repopulse_adoption.py").read_bytes()
source = checked_out_source.replace(b"\r\n", b"\n")
captured = source.replace(b"\n", b"\r\n")
assert hashlib.sha256(source).hexdigest() == (
"9c66fae21f1eec27215af08f7bb7d24bafc6f10757393b56c42721fa99dccc66"
)
assert hashlib.sha256(captured).hexdigest() == (
"e3036cef0f6e1934297de2d35c0381f93441bbf6f28bd18c94c3f47fc116e4e1"
)
identity = {
"sourceRevision": revision,
"sourceTree": tree,
"controllerVersion": "2.1.14",
"artifactSha256": artifact,
}
assert module._matches_legacy_crlf_source(
"run_repopulse_adoption.py", captured, source, identity
)
@pytest.mark.parametrize(
("name", "field", "value", "change_capture"),
[
("run_repopulse_adoption.py", "sourceRevision", "f" * 40, False),
("run_repopulse_adoption.py", "sourceTree", "f" * 40, False),
("run_repopulse_adoption.py", "controllerVersion", "2.1.15", False),
("run_repopulse_adoption.py", "artifactSha256", "f" * 64, False),
("other.py", "", "", False),
("run_repopulse_adoption.py", "", "", True),
],
)
@pytest.mark.parametrize("revision,tree,artifact", [
("e854334923cca574482d22b8424ba3ce46ec6aa1", "cf2a117f9222b952deb9902c646116612f3a7ea0",
"e88739bf091c8bac77ea0db5b97300666a1baa8ba984bbf6ae91e3bf25719e08"),
("aaf7d11821a86be65c91fead97a6a4758c4fbacd", "34a90f9bd06fa4efa26a19dbf5c7c13239d03721",
"ba8de75ba04f25b5298f81d107e6108bc7a92de95531b390ade0bcba9e4678e2"),
])
def test_current_attested_crlf_compatibility_rejects_near_misses(
name, field, value, change_capture, revision, tree, artifact,
):
checked_out_source = (ROOT / "scripts/run_repopulse_adoption.py").read_bytes()
source = checked_out_source.replace(b"\r\n", b"\n")
captured = source.replace(b"\n", b"\r\n")
identity = {
"sourceRevision": revision,
"sourceTree": tree,
"controllerVersion": "2.1.14",
"artifactSha256": artifact,
}
if field:
identity[field] = value
if change_capture:
captured = captured.replace(b"\r\n", b"\n", 1)
assert not module._matches_legacy_crlf_source(name, captured, source, identity)
def test_current_e854_crlf_exception_integrates_with_git_source_verification(monkeypatch):
revision = "e854334923cca574482d22b8424ba3ce46ec6aa1"
tree = "cf2a117f9222b952deb9902c646116612f3a7ea0"
artifact_sha = "e88739bf091c8bac77ea0db5b97300666a1baa8ba984bbf6ae91e3bf25719e08"
files = module.LEGACY_RELEASE_FILES_BEFORE_FORGE
assert len(files) == 30
checked_out_script = (ROOT / "scripts/run_repopulse_adoption.py").read_bytes()
runner_source = checked_out_script.replace(b"\r\n", b"\n")
runner_capture = runner_source.replace(b"\n", b"\r\n")
assert hashlib.sha256(runner_source).hexdigest() == (
"9c66fae21f1eec27215af08f7bb7d24bafc6f10757393b56c42721fa99dccc66"
)
assert hashlib.sha256(runner_capture).hexdigest() == (
"e3036cef0f6e1934297de2d35c0381f93441bbf6f28bd18c94c3f47fc116e4e1"
)
source_by_name = {}
entries = {}
for name in files:
path = (ROOT / "scripts/run_repopulse_adoption.py" if name == "run_repopulse_adoption.py"
else ROOT / "ops/gitea-deploy-broker" / name)
source = runner_source if name == "run_repopulse_adoption.py" else path.read_bytes()
source = source.replace(b"\r\n", b"\n")
source_by_name[name] = source
entries[name] = (
runner_capture if name == "run_repopulse_adoption.py" else source,
0o644,
)
identity_data = {
"apiVersion": "itworx.controller-artifact/v1",
"controllerVersion": "2.1.14",
"sourceRepository": "jens/unraid-appops-gateway",
"sourceRevision": revision,
"sourceTree": tree,
"artifactSha256": artifact_sha,
"builtAt": "2026-09-30T00:00:00Z",
}
entries["controller-release.json"] = (
json.dumps(identity_data, separators=(",", ":")).encode(), 0o644
)
identity = {key: str(value) for key, value in identity_data.items()}
git_outputs = {path: source for name, source in source_by_name.items() for path in [
"scripts/run_repopulse_adoption.py"
if name == "run_repopulse_adoption.py"
else f"ops/gitea-deploy-broker/{name}"
]}
controller_id = "c" * 64
def select_attested_files(captured, expected):
assert expected == artifact_sha
assert all(name in captured for name in files)
return files
def run(arguments, **_kwargs):
assert arguments[:5] == [
"docker", "exec", controller_id, "git", f"--git-dir={module.SOURCE_GIT_DIR}"
]
if arguments[-2] == "rev-parse":
return f"{tree}\n".encode()
return git_outputs[arguments[-1].split(":", 1)[-1]]
monkeypatch.setattr(module, "_matching_release_files", select_attested_files)
monkeypatch.setattr(module, "_run", run)
module._verify_git_source_identity(entries, identity, controller_id=controller_id)
other_file = next(name for name in files if name != "run_repopulse_adoption.py")
other_path = f"ops/gitea-deploy-broker/{other_file}"
git_outputs[other_path] = b"tampered protected Git source"
with pytest.raises(module.VolumeTransitionError, match="source_mismatch"):
module._verify_git_source_identity(entries, identity, controller_id=controller_id)
def _current_controller_with_graph_mount():
return {
"Id": "c" * 64, "Image": IMAGE_ID, "Name": "/gitea-deploy-control",
"State": {"Running": True},
"Mounts": [
{"Type": "bind", "Source": module._expected_revision_path(REVISION).as_posix(),
"Destination": module.EXECUTABLE_TARGET, "RW": False, "Propagation": "rslave"},
{"Type": "bind", "Source": "/var/lib/docker", "Destination": "/var/lib/docker",
"RW": False, "Propagation": "rslave"},
],
}
def test_current_controller_accepts_only_fixed_readonly_docker_root_ancestor(monkeypatch):
item = _current_controller_with_graph_mount()
monkeypatch.setattr(module, "_run", lambda *_args, **_kwargs: json.dumps([item]).encode())
assert module._controller_image_inspect(IMAGE_ID) == item
@pytest.mark.parametrize("damage", ["writable", "redirected_target", "child_source", "parent_source", "volume_type", "named_volume_alias", "duplicate_graph", "shared_propagation", "private_propagation"])
def test_current_controller_rejects_other_docker_root_appdata_aliases(monkeypatch, damage):
item = _current_controller_with_graph_mount()
graph = item["Mounts"][1]
if damage == "writable":
graph["RW"] = True
elif damage == "redirected_target":
graph["Destination"] = "/graph-alias"
elif damage == "child_source":
graph["Source"] = "/var/lib/docker/volumes"
elif damage == "parent_source":
graph["Source"] = "/var/lib"
elif damage == "volume_type":
graph["Type"] = "volume"
elif damage == "named_volume_alias":
graph["Name"] = module.MANAGED_APPDATA_VOLUME
elif damage == "duplicate_graph":
item["Mounts"].append(dict(graph))
elif damage == "shared_propagation":
graph["Propagation"] = "rshared"
else:
graph["Propagation"] = "rprivate"
monkeypatch.setattr(module, "_run", lambda *_args, **_kwargs: json.dumps([item]).encode())
with pytest.raises(module.VolumeTransitionError, match="managed_appdata_mount_alias"):
module._controller_image_inspect(IMAGE_ID)
@pytest.mark.parametrize("populated", [False, True])
def test_config_snapshot_preserves_fixed_appops_legacy_intent_directory(populated):
entries = {name: (b"{}", 0o600) for name in ("projects.json", "broker-token", "gitea-validation.json", "gitea-validation-token")}
if populated:
entries["appops-legacy-upgrade/unraid-appops-gateway.json"] = (b'{"schema":"itworx.appops-legacy-upgrade-intent/v1"}', 0o600)
directories = {"": 0o755, "appops-legacy-upgrade": 0o700}
assert module._config_tree(entries, directories) == (entries, directories)
assert module._read_tar_tree(module._release_tar_bytes(REVISION, (entries, directories))) == (entries, directories)
@pytest.mark.parametrize("damage", ["unknown_file", "nested_directory", "public_directory", "public_file"])
def test_config_snapshot_keeps_appops_legacy_state_contract_closed(damage):
entries = {name: (b"{}", 0o600) for name in ("projects.json", "broker-token", "gitea-validation.json", "gitea-validation-token")}
directories = {"": 0o755, "appops-legacy-upgrade": 0o700}
if damage == "unknown_file":
entries["appops-legacy-upgrade/shell.py"] = (b"unexpected", 0o600)
elif damage == "nested_directory":
directories["appops-legacy-upgrade/nested"] = 0o700
elif damage == "public_directory":
directories["appops-legacy-upgrade"] = 0o755
else:
entries["appops-legacy-upgrade/unraid-appops-gateway.json"] = (b"{}", 0o644)
with pytest.raises(module.VolumeTransitionError):
module._config_tree(entries, directories)
@pytest.mark.parametrize("action", ["up", "stop"])
def test_legacy_receipt_projection_preserves_evidence_and_readonly_mounts(compose, action):
_baseline, calls = compose
value = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
for mount in value["services"][module.SERVICE]["volumes"]:
if mount.get("target") in {module.EXECUTABLE_TARGET, module.CONFIG_TARGET}:
mount["bind"] = {"propagation": "rprivate"}
captured = json.dumps(value).encode()
original_hash = module._hash(captured)
with pytest.raises(module.VolumeTransitionError):
module._bound_model(captured)
module.compose_action(captured, action)
projected = json.loads(calls[-1][1])
for mount in projected["services"][module.SERVICE]["volumes"]:
if mount.get("target") in {module.EXECUTABLE_TARGET, module.CONFIG_TARGET}:
assert mount["read_only"] is True
assert mount["bind"] == {"propagation": "rslave"}
mount["bind"] = {"propagation": "rprivate"}
assert projected == value
assert module._hash(captured) == original_hash
@pytest.mark.parametrize("damage", ["writable", "shared", "extra_option", "foreign_source"])
def test_legacy_receipt_projection_rejects_unsafe_mount_before_docker(compose, damage):
_baseline, calls = compose
value = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
mount = next(x for x in value["services"][module.SERVICE]["volumes"]
if x.get("target") == module.EXECUTABLE_TARGET)
mount["bind"] = {"propagation": "rprivate"}
if damage == "writable":
mount["read_only"] = False
elif damage == "shared":
mount["bind"]["propagation"] = "rshared"
elif damage == "extra_option":
mount["bind"]["create_host_path"] = True
else:
mount["source"] = "/tmp/untrusted-controller"
calls.clear()
with pytest.raises(module.VolumeTransitionError):
module.compose_action(json.dumps(value).encode(), "up")
assert calls == []
@pytest.mark.parametrize("phase", ["prepared", "stopped", "activated", "verified"])
@pytest.mark.parametrize("failure", ["read_only", "full"])
def test_forward_stop_rechecks_storage_after_intent_or_on_resume(monkeypatch, phase, failure):
monkeypatch.setattr(module, "_cleanup_promotion_host_probe", lambda *a: nullcontext((None, lambda: None)))
receipt = {"phase": phase, "imageId": IMAGE_ID,
"previousRevision": OLD_REVISION, "sourceRevision": REVISION}
persisted = phase != "prepared"
events = []
monkeypatch.setattr(module, "_load_transaction", lambda _: (42, receipt, {
"compose-rollback.json": b"old", "compose-after.json": b"new"}))
monkeypatch.setattr(module.os, "close", lambda _: None)
def persist(_fd, value, new_phase):
nonlocal persisted
persisted = True
return {**value, "phase": new_phase}
monkeypatch.setattr(module, "_set_promotion_phase", persist)
def filesystem(_path):
return types.SimpleNamespace(
f_flag=1 if persisted and failure == "read_only" else 0,
f_bavail=0 if persisted and failure == "full" else 1024**3,
f_frsize=1)
monkeypatch.setattr(module.os, "statvfs", filesystem, raising=False)
monkeypatch.setattr(module, "_controller_image_inspect", lambda *_a, **_kw: {
"State": {"Running": True}, "Mounts": [{"Destination": module.EXECUTABLE_TARGET,
"Source": module._expected_revision_path(OLD_REVISION).as_posix()}]})
monkeypatch.setattr(module, "compose_action", lambda *args: events.append(args))
code = "controller_volume_storage_read_only" if failure == "read_only" else "controller_volume_storage_insufficient"
with pytest.raises(module.VolumeTransitionError, match=code):
module.stop_promotion(Path("/unused-backup"))
assert events == []
def test_probe_cleanup_failure_precedes_stop_intent_and_controller_stop(monkeypatch):
receipt = {"phase": "prepared"}
monkeypatch.setattr(module, "_load_transaction", lambda _: (42, receipt, {}))
monkeypatch.setattr(module.os, "close", lambda _: None)
monkeypatch.setattr(module, "_require_writable_promotion_storage", lambda: None)
def reject(*args):
raise module.VolumeTransitionError("controller_volume_command_failed")
monkeypatch.setattr(module, "_cleanup_promotion_host_probe", reject)
monkeypatch.setattr(module, "_set_promotion_phase", lambda *a: pytest.fail("No stop intent"))
monkeypatch.setattr(module, "_stop_current_controller", lambda *a, **kw: pytest.fail("No stop"))
with pytest.raises(module.VolumeTransitionError, match="controller_volume_command_failed"):
module.stop_promotion(Path("/unused-backup"))
assert receipt["phase"] == "prepared"
def test_promotion_cleanup_uses_incoming_code_but_only_attested_previous_source(monkeypatch, tmp_path):
receipt = {"phase": "prepared", "imageId": IMAGE_ID, "previousRevision": OLD_REVISION,
"sourceRevision": REVISION, "sourceTree": "d" * 40}
old_source = module._expected_revision_path(OLD_REVISION).as_posix()
events = []
def inspect(image, **kw):
assert image == IMAGE_ID
assert kw in ({"require_running": True}, {"expected_source": old_source, "require_running": True})
events.append("attest")
return {"Id": "c" * 64, "Mounts": [{"Destination": module.EXECUTABLE_TARGET,
"Source": old_source}]}
monkeypatch.setattr(module, "_controller_image_inspect", inspect)
@contextmanager
def lock(*, native_cleanup):
assert native_cleanup is True
events.append("lock")
yield lambda: None
events.append("unlock")
def cleanup(**kw):
assert kw["image_id"] == IMAGE_ID and kw["code_source"] == old_source
kw["assert_lock"]()
events.append("cleanup")
validator = types.SimpleNamespace(_host_probe_lifecycle_lock=lock, _cleanup_stopped_host_probe_locked=cleanup)
monkeypatch.setattr(module, "_promotion_probe_validator", lambda *args: nullcontext(validator))
monkeypatch.setattr(module, "_run", lambda *args, **kwargs: pytest.fail("No nested child lock"))
with module._cleanup_promotion_host_probe(receipt, {"controller-after.tar": b"qualified"}) as (identifier, assert_lock):
assert identifier == "c" * 64
assert_lock()
events.append("stop-window")
assert events == ["attest", "lock", "cleanup", "attest", "stop-window", "unlock"]
@pytest.mark.parametrize("present", [False, True])
def test_legacy_promotion_allows_only_no_probe_without_adopting_legacy_source(monkeypatch, present):
receipt = {"imageId": IMAGE_ID, "previousRevision": OLD_REVISION}
monkeypatch.setattr(module, "_controller_image_inspect", lambda *a, **kw: {
"Id": "c" * 64, "Mounts": [{"Destination": module.EXECUTABLE_TARGET,
"Source": module.CONTROLLER_DIR.as_posix()}]})
calls = []
def run(args, **kw):
calls.append(args)
return b"d" * 64 if present else b""
monkeypatch.setattr(module, "_run", run)
validator = types.SimpleNamespace(_host_probe_lifecycle_lock=lambda **kw: nullcontext(lambda: None))
monkeypatch.setattr(module, "_promotion_probe_validator", lambda *args: nullcontext(validator))
if present:
with pytest.raises(module.VolumeTransitionError, match="controller_volume_probe_cleanup_identity_invalid"):
with module._cleanup_promotion_host_probe(receipt, {}):
pytest.fail("Unknown legacy probe admitted")
else:
with module._cleanup_promotion_host_probe(receipt, {}):
pass
assert calls == [["docker", "ps", "-a", "--no-trunc", "--filter",
"name=^itworx-runner-cgroup-probe$", "--format", "{{.ID}}"]]
def test_resumed_forward_stop_of_stopped_controller_needs_no_storage(monkeypatch):
receipt = {"phase": "stopped", "imageId": IMAGE_ID,
"previousRevision": OLD_REVISION, "sourceRevision": REVISION}
monkeypatch.setattr(module, "_load_transaction", lambda _: (42, receipt, {}))
monkeypatch.setattr(module.os, "close", lambda _: None)
monkeypatch.setattr(module, "_controller_image_inspect", lambda *_a, **_kw: {
"Id": "c" * 64, "State": {"Running": False}, "Mounts": [{"Destination": module.EXECUTABLE_TARGET,
"Source": module._expected_revision_path(OLD_REVISION).as_posix()}]})
monkeypatch.setattr(module, "_require_writable_promotion_storage",
lambda: pytest.fail("Already stopped recovery must remain possible"))
monkeypatch.setattr(module, "compose_action", lambda *_: pytest.fail("No stop needed"))
module.stop_promotion(Path("/unused-backup"))
def test_qualified_probe_modules_use_private_imports_and_leave_bare_modules_untouched(monkeypatch):
names = ('mcp_hub_release_store', 'runner_profiles', 'runner_resource_envelope')
entries = {f'{name}.py': ((ROOT / 'ops/gitea-deploy-broker' / f'{name}.py').read_bytes(), 0o644) for name in names}
monkeypatch.setattr(module, '_read_tar_tree', lambda _: (entries, {'': 0o755}))
monkeypatch.setattr(module, '_release_identity', lambda _: {'sourceRevision': REVISION, 'sourceTree': 'd' * 40})
sentinels = {name: types.ModuleType(f'foreign_{name}') for name in names}
for name, value in sentinels.items():
monkeypatch.setitem(sys.modules, name, value)
before = set(sys.modules)
before_path = list(sys.path)
receipt = {'sourceRevision': REVISION, 'sourceTree': 'd' * 40}
with module._promotion_probe_validator(receipt, {'controller-after.tar': b'qualified'}) as validator:
assert validator._HOST_PROBE_NAME == 'itworx-runner-cgroup-probe'
assert validator.PinnedDirectory.__module__.startswith('_itworx_promotion_probe_')
assert len(validator.RUNNER_PROFILES) == 6
assert all(sys.modules[name] is value for name, value in sentinels.items())
assert sys.path == before_path
assert not {name for name in set(sys.modules) - before if name.startswith('_itworx_promotion_probe_')}
assert all(sys.modules[name] is value for name, value in sentinels.items())
@pytest.fixture
def probe_stop_window(monkeypatch):
receipt = {'phase': 'prepared', 'imageId': IMAGE_ID, 'previousRevision': OLD_REVISION,
'sourceRevision': REVISION, 'sourceTree': 'd' * 40}
state = {'running': True, 'held': False, 'inspections': 0, 'events': [], 'real_close': os.close}
old_source = module._expected_revision_path(OLD_REVISION).as_posix()
monkeypatch.setattr(module, '_load_transaction', lambda _: (42, receipt, {
'controller-after.tar': b'qualified', 'compose-rollback.json': b'old', 'compose-after.json': b'new'}))
monkeypatch.setattr(module.os, 'close', lambda _: None)
monkeypatch.setattr(module, '_require_writable_promotion_storage', lambda: None)
def inspect(*args, **kwargs):
state['inspections'] += 1
identifier = 'c' * 64
if state.get('changed_on') == state['inspections']:
identifier = 'f' * 64
return {'Id': identifier, 'State': {'Running': state['running']},
'Mounts': [{'Destination': module.EXECUTABLE_TARGET, 'Source': state.get('source', old_source)}]}
monkeypatch.setattr(module, '_controller_image_inspect', inspect)
def assert_lock():
assert state['held']
@contextmanager
def lock(*, native_cleanup):
assert native_cleanup is True and not state['held']
state['held'] = True
state['events'].append('lock')
try:
yield assert_lock
finally:
state['held'] = False
state['events'].append('unlock')
def cleanup(**kwargs):
kwargs['assert_lock']()
state['events'].append('cleanup')
if state.get('cleanup_failure'):
error = RuntimeError('private-secret-canary')
error.condition = state['cleanup_failure']
raise error
validator = types.SimpleNamespace(_host_probe_lifecycle_lock=lock, _cleanup_stopped_host_probe_locked=cleanup)
monkeypatch.setattr(module, '_promotion_probe_validator', lambda *args: nullcontext(validator))
def intent(_fd, value, phase):
assert_lock()
state['events'].append('intent')
value['phase'] = phase
return value
monkeypatch.setattr(module, '_set_promotion_phase', intent)
def compose(*args):
assert_lock()
state['events'].append('compose-stop')
if not state.get('still_running'):
state['running'] = False
monkeypatch.setattr(module, 'compose_action', compose)
return receipt, state, validator
@pytest.mark.parametrize('phase', ['prepared', 'stopped', 'activated', 'verified'])
def test_probe_lifecycle_owner_covers_cleanup_intent_and_exact_confirmed_controller_stop(probe_stop_window, phase):
receipt, state, _validator = probe_stop_window
receipt['phase'] = phase
if phase in {'activated', 'verified'}:
state['source'] = module._expected_revision_path(REVISION).as_posix()
module.stop_promotion(Path('/unused-backup'))
assert state['events'] == ['lock', 'cleanup', *(['intent'] if phase == 'prepared' else []), 'compose-stop', 'unlock']
assert state['inspections'] == 4 and state['running'] is False and state['held'] is False
@pytest.mark.parametrize('failure', ['identity_invalid', 'identity_changed', 'remove_busy', 'foreign-unapproved-condition'])
def test_untrusted_or_active_probe_prevents_intent_and_stop_with_finite_redaction(probe_stop_window, failure):
receipt, state, _validator = probe_stop_window
state['cleanup_failure'] = failure
with pytest.raises(module.VolumeTransitionError) as error:
module.stop_promotion(Path('/unused-backup'))
assert receipt['phase'] == 'prepared' and state['running'] is True
assert state['events'] == ['lock', 'cleanup', 'unlock']
assert 'private-secret-canary' not in str(error.value)
expected = 'probe_' + failure
assert error.value.command_failure == ('probe_cleanup', 'exit_1', expected if expected in module._COMMAND_DIAGNOSTIC_CONDITIONS else None)
@pytest.mark.parametrize('failure', ['changed_before_stop', 'changed_after_stop', 'still_running'])
def test_stop_never_claims_success_without_same_controller_and_stopped_state(probe_stop_window, failure):
receipt, state, _validator = probe_stop_window
if failure == 'still_running':
state['still_running'] = True
else:
state['changed_on'] = 3 if failure == 'changed_before_stop' else 4
with pytest.raises(module.VolumeTransitionError):
module.stop_promotion(Path('/unused-backup'))
assert receipt['phase'] == 'stopped' and state['held'] is False
if failure == 'changed_before_stop':
assert 'compose-stop' not in state['events']
@pytest.mark.skipif(os.name != 'posix', reason='native independent process lifecycle exclusion')
def test_late_independent_probe_is_excluded_through_persist_and_confirmed_stop(probe_stop_window, monkeypatch, tmp_path):
import fcntl
receipt, state, validator = probe_stop_window
lock_path = tmp_path / 'probe.lock'
lock_path.write_bytes(b'')
close = state['real_close']
# The fixture suppresses closing its fake transaction descriptor only.
monkeypatch.setattr(module.os, 'close', close)
monkeypatch.setattr(module, '_load_transaction', lambda _: (os.open(lock_path, os.O_RDONLY), receipt, {
'controller-after.tar': b'qualified', 'compose-rollback.json': b'old', 'compose-after.json': b'new'}))
child = ('import fcntl,os,sys; fd=os.open(sys.argv[1],os.O_RDWR);\n'
'try: fcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)\n'
'except BlockingIOError: sys.exit(0)\n'
'sys.exit(1)')
def excluded():
result = subprocess.run([sys.executable, '-c', child, str(lock_path)], capture_output=True, timeout=5, check=False)
assert result.returncode == 0 and not result.stdout and not result.stderr
@contextmanager
def native_lock(*, native_cleanup):
assert native_cleanup is True
with lock_path.open('r+b') as stream:
fcntl.flock(stream.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
state['held'] = True
try:
yield excluded
finally:
state['held'] = False
fcntl.flock(stream.fileno(), fcntl.LOCK_UN)
validator._host_probe_lifecycle_lock = native_lock
cleanup = validator._cleanup_stopped_host_probe_locked
def checked_cleanup(**kwargs):
excluded()
cleanup(**kwargs)
validator._cleanup_stopped_host_probe_locked = checked_cleanup
stop = module._stop_current_controller
def checked_stop(*args, **kwargs):
excluded()
stop(*args, **kwargs)
excluded()
monkeypatch.setattr(module, '_stop_current_controller', checked_stop)
module.stop_promotion(Path('/unused-backup'))
assert state['running'] is False
result = subprocess.run([sys.executable, '-c', child, str(lock_path)], capture_output=True, timeout=5, check=False)
assert result.returncode == 1
def test_rollback_stop_remains_available_without_forward_storage_gate(monkeypatch):
receipt = {"phase": "stopped", "imageId": IMAGE_ID,
"previousRevision": OLD_REVISION, "sourceRevision": REVISION}
events = []
monkeypatch.setattr(module, "_load_transaction", lambda _: (42, receipt, {
"compose-rollback.json": b"old", "compose-after.json": b"new"}))
monkeypatch.setattr(module.os, "close", lambda _: None)
monkeypatch.setattr(module, "_controller_image_inspect", lambda *_a, **_kw: {
"State": {"Running": True}, "Mounts": [{"Destination": module.EXECUTABLE_TARGET,
"Source": module._expected_revision_path(REVISION).as_posix()}]})
monkeypatch.setattr(module, "_require_writable_promotion_storage",
lambda: pytest.fail("Rollback must not inherit forward capacity gate"))
monkeypatch.setattr(module, "compose_action", lambda *args: events.append(args))
module.rollback_promotion(Path("/unused-backup"), stop_only=True)
assert events == [(b"new", "stop")]