from __future__ import annotations import ast import importlib.util import io import json import os import subprocess import sys import tarfile import types 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 IMAGE_ID = "sha256:" + "b" * 64 ORIGINAL_COMPOSE = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: immutable-controller\n" 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 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, } in volumes assert { "type": "volume", "source": module.VOLUME, "target": module.TARGET, "volume": {"nocopy": True}, } in volumes assert { "type": "bind", "source": module._expected_config_path().as_posix(), "target": module.CONFIG_TARGET, "read_only": True, } in volumes assert len(volumes) == 4 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"] == module.CONFIG_TARGET)) assert rendered["volumes"] == { module.VOLUME: {"name": module.VOLUME, "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", "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_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"]) 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" else: baseline["services"]["deploy-control"]["env_file"] = [".env"] with pytest.raises(module.VolumeTransitionError): module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID) 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() if damage: with pytest.raises(module.VolumeTransitionError, match="image_reference_(changed|invalid)"): module.rendered_model(original, REVISION, IMAGE_ID, current_container=current) else: rendered = json.loads(module.rendered_model(original, REVISION, IMAGE_ID, current_container=current)) assert rendered["services"][module.SERVICE]["image"] == IMAGE_ID assert rendered["services"][module.SERVICE]["pull_policy"] == "never" assert not any("up" in arguments or "pull" in arguments for arguments, _ in calls) 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 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 "--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) 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" def test_rollback_uses_no_dependencies_when_recreating_prior_controller(compose, tmp_path, monkeypatch): _baseline, calls = compose previous = REVISION target = "c" * 40 previous_compose = module.rendered_model(ORIGINAL_COMPOSE, previous, IMAGE_ID) target_compose = module.rendered_model(ORIGINAL_COMPOSE, target, IMAGE_ID) receipt = { "phase": "activated", "imageId": IMAGE_ID, "previousRevision": previous, "sourceRevision": target, } values = { "controller-before.tar": module._release_tar_bytes( previous, ({"controller-release.json": (b"previous release", 0o644)}, {"": 0o755}), ), "controller-after.tar": b"unused by isolated rollback test", "mirror-before.tar": b"unused by isolated rollback test", "compose-rollback.json": previous_compose, "compose-after.json": target_compose, } current = { "Mounts": [{ "Destination": module.EXECUTABLE_TARGET, "Source": module._expected_revision_path(previous).as_posix(), }] } 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, "_verify_release_leaf", lambda *args, **kwargs: True) monkeypatch.setattr(module, "_stop_current_controller", lambda *args, **kwargs: None) monkeypatch.setattr(module, "_sync_controller_mirror", lambda *args, **kwargs: None) monkeypatch.setattr(module, "_ensure_root_compose", 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) module.rollback_promotion(tmp_path / "backup") up_calls = [ arguments for arguments, _data in calls if arguments[:2] == ["docker", "compose"] and "up" in arguments ] assert len(up_calls) == 1 up_index = up_calls[0].index("up") assert up_calls[0][up_index:] == [ "up", "-d", "--force-recreate", "--pull", "never", "--no-deps", module.SERVICE ] 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 module.RELEASE_FILES[:3] == ( "controller_paths.py", "mcp_hub_release_store.py", "hub_permission_preparation.py" ) @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") def test_private_stage_rejects_symlinked_source_directory(): 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: 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) 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" ) 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()) def test_prior_release_capture_must_match_protected_git_tree(monkeypatch): files = module.LEGACY_RELEASE_FILES_2_1_5 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