Publish curated Unraid-AppOps-Gateway source from ba66029223865607deddc234e283c11127324eb8

This commit is contained in:
Public Source Publisher committed 2026-10-07 01:56:22 +00:00
1 parent 820244dbb3
commit 8dd4b40252
52 files changed
+11814 -288

No files matched your search

+1 -1
View File
@@ -42,7 +42,7 @@ def _set_npm_config():
settings.apis["NPM_URL"] = "https://npm.example.test"
settings.apis["NPM_IDENTITY"] = "admin@example.test"
settings.apis["NPM_SECRET"] = "NPM_SECRET_VALUE"
settings.apis["NPM_VERIFY_TLS"] = False
settings.apis["NPM_VERIFY_TLS"] = True
settings.apis["NPM_TIMEOUT_SECONDS"] = "10"
@@ -1,12 +1,15 @@
from __future__ import annotations
import ast
import io
import json
import os
import re
import shutil
import subprocess
import sys
import tarfile
from dataclasses import replace
from pathlib import Path
from typing import Any
@@ -24,6 +27,10 @@ CONTROLLER_IMAGE_ID = "sha256:c474d435a4f55ac5205570da0caa47e103f89a6c2ef96ea458
CONTROLLER_IMAGE_REF = "docker.gitea.com/runner-images:ubuntu-latest"
CONTROLLER_REVISION = "034c9f844d2455850ba37b31379b7edaa786583e"
CONTROLLER_TREE = "6b8e92942e6e5eca8a9aa9da0a1509b2019d7b7b"
MODERN_CONTROLLER_CONTAINER_ID = "9093c8e29d9064f9948918e5884f9adc5db6b740991e2934902f93c6ea813e01"
MODERN_CONTROLLER_REVISION = "e854334923cca574482d22b8424ba3ce46ec6aa1"
MODERN_CONTROLLER_TREE = "cf2a117f9222b952deb9902c646116612f3a7ea0"
MODERN_CONTROLLER_ARTIFACT_SHA256 = "e88739bf091c8bac77ea0db5b97300666a1baa8ba984bbf6ae91e3bf25719e08"
REVISION = "a" * 40
TREE = "b" * 40
CALLER_ID = "c" * 64
@@ -247,15 +254,39 @@ def _modern_controller_mounts(source_revision: str) -> list[dict[str, Any]]:
]
def _modern_controller_inspect(source_revision: str) -> dict[str, Any]:
def _modern_controller_source_mounts(source_revision: str) -> list[dict[str, Any]]:
mounts = [dict(mount) for mount in _modern_controller_mounts(source_revision)]
mounts = [mount for mount in mounts if mount["Destination"] != "/forge-master-key-backup"]
for mount in mounts:
if mount["Destination"] in {"/opt/gitea-deploy", "/controller-config"}:
mount["Propagation"] = "rslave"
return mounts
def _modern_controller_inspect(
source_revision: str,
*,
identifier: str = MODERN_CONTROLLER_CONTAINER_ID,
) -> dict[str, Any]:
controller = _controller_inspect()
controller.update({"Id": "e" * 64, "Image": CONTROLLER_IMAGE_ID})
controller.update({"Id": identifier, "Image": CONTROLLER_IMAGE_ID})
controller["State"].update({"Dead": False, "Paused": False, "Restarting": False})
controller["Config"]["Image"] = CONTROLLER_IMAGE_ID
controller["Config"]["Labels"] = _modern_controller_labels()
controller["Mounts"] = _modern_controller_mounts(source_revision)
return controller
def _modern_controller_source_inspect(
source_revision: str,
*,
identifier: str = MODERN_CONTROLLER_CONTAINER_ID,
) -> dict[str, Any]:
controller = _modern_controller_inspect(source_revision, identifier=identifier)
controller["Mounts"] = _modern_controller_source_mounts(source_revision)
return controller
def _appops_inspect() -> dict[str, Any]:
labels = _appops_labels()
return {
@@ -480,7 +511,9 @@ class FakeDocker:
def start_container(self, identifier: str) -> None:
self.started.append(identifier)
source_revision = self.created[-1]["Labels"]["itworx.source.revision"]
self.containers["gitea-deploy-control"] = _modern_controller_inspect(source_revision)
self.containers["gitea-deploy-control"] = _modern_controller_inspect(
source_revision, identifier="e" * 64
)
def remove_retired_failed_container(self, identifier: str) -> None:
assert identifier == RETIRED_HELPER_ID
@@ -505,6 +538,8 @@ class FakeProtectedGit:
self.main_checks = 0
self.fail_current_main_at: int | None = None
self.retired_tree_valid = True
self.controller_release_checks: list[bootstrap.ControllerBaseline] = []
self.invalid_controller_release: bootstrap.ControllerBaseline | None = None
def verify_current_main(self, revision: str, tree: str) -> None:
self.main_checks += 1
@@ -517,6 +552,11 @@ class FakeProtectedGit:
if (revision, tree) != (LEGACY_REVISION, LEGACY_TREE):
raise bootstrap.BootstrapError("controller_promoter_legacy_provenance_invalid")
def verify_controller_release(self, baseline: bootstrap.ControllerBaseline) -> None:
self.controller_release_checks.append(baseline)
if baseline not in bootstrap.CONTROLLER_BASELINES or baseline == self.invalid_controller_release:
raise bootstrap.BootstrapError("controller_promoter_controller_provenance_invalid")
def verify_retired_modern_revision(self, revision: str, tree: str) -> None:
if (revision, tree) != (
bootstrap.RETIRED_FAILED_MODERN_REVISION,
@@ -570,6 +610,15 @@ def _health(
}
def _modern_baseline_health() -> dict[str, Any]:
return _health(
revision=MODERN_CONTROLLER_REVISION,
tree=MODERN_CONTROLLER_TREE,
version="2.1.14",
artifact_sha256=MODERN_CONTROLLER_ARTIFACT_SHA256,
)
def _run_bootstrap(
docker: FakeDocker,
git: FakeProtectedGit,
@@ -610,6 +659,176 @@ def _run_bootstrap(
)
def test_protected_git_reproduces_pinned_release_with_exact_handoff_crlf_conversion(
monkeypatch: pytest.MonkeyPatch,
):
git_executable = shutil.which("git")
if git_executable is None:
pytest.skip("git is required to validate the pinned source archives")
probe = subprocess.run(
[git_executable, "cat-file", "-e", f"{CONTROLLER_REVISION}^{{commit}}"],
cwd=ROOT,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=15,
check=False,
)
if probe.returncode != 0:
pytest.skip("the checkout does not expose the retained historical commits to Git")
def local_git_output(arguments: list[str], *, limit: int = bootstrap.MAX_GIT_BLOB_BYTES) -> bytes:
result = subprocess.run(
[git_executable, "-c", f"safe.directory={ROOT}", *arguments],
cwd=ROOT,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
timeout=15,
check=False,
)
if result.returncode != 0:
raise bootstrap.BootstrapError("controller_promoter_protected_git_invalid")
if len(result.stdout) > limit:
raise bootstrap.BootstrapError("controller_promoter_protected_git_invalid")
return result.stdout
monkeypatch.setattr(bootstrap, "_git_output", local_git_output)
repository = bootstrap.ProtectedGitRepository()
for baseline in (
bootstrap.LEGACY_CONTROLLER_BASELINE,
bootstrap.MODERN_CONTROLLER_BASELINE,
):
repository.verify_controller_release(baseline)
with pytest.raises(
bootstrap.BootstrapError,
match="controller_promoter_controller_provenance_invalid",
):
repository.verify_controller_release(
replace(baseline, tree="f" * 40)
)
archive_paths = [
"scripts/run_repopulse_adoption.py"
if name == "run_repopulse_adoption.py"
else f"ops/gitea-deploy-broker/{name}"
for name in baseline.artifact_files
]
archive = local_git_output(
["archive", "--format=tar", baseline.revision, "--", *archive_paths],
limit=bootstrap.MAX_CONTROLLER_ARTIFACT_ARCHIVE_BYTES,
)
assert bootstrap._controller_artifact_archive_digest(
archive, baseline
) == baseline.artifact_sha256
with pytest.raises(
bootstrap.BootstrapError,
match="controller_promoter_controller_provenance_invalid",
):
bootstrap._controller_artifact_source_bytes(
replace(baseline, revision="f" * 40),
bootstrap.CONTROLLER_CRLF_HANDOFF,
b"source",
)
corrupted_archive = _rewrite_controller_archive(
archive,
mutate_path="ops/gitea-deploy-broker/deploy.py",
mutate=lambda content: content + b"# changed\n",
)
assert bootstrap._controller_artifact_archive_digest(
corrupted_archive, baseline
) != baseline.artifact_sha256
renamed_archive = _rewrite_controller_archive(
archive,
rename_path="scripts/run_repopulse_adoption.py",
new_path="scripts/unreviewed.py",
)
with pytest.raises(
bootstrap.BootstrapError,
match="controller_promoter_controller_provenance_invalid",
):
bootstrap._controller_artifact_archive_digest(renamed_archive, baseline)
handoff_path = "scripts/run_repopulse_adoption.py"
corrupted_handoff = _rewrite_controller_archive(
archive,
mutate_path=handoff_path,
mutate=lambda content: content + b"# changed\n",
)
with pytest.raises(
bootstrap.BootstrapError,
match="controller_promoter_controller_provenance_invalid",
):
bootstrap._controller_artifact_archive_digest(corrupted_handoff, baseline)
crlf_source_archive = _rewrite_controller_archive(
archive,
mutate_path=handoff_path,
mutate=lambda content: content.replace(b"\n", b"\r\r\n"),
)
with pytest.raises(
bootstrap.BootstrapError,
match="controller_promoter_controller_provenance_invalid",
):
bootstrap._controller_artifact_archive_digest(crlf_source_archive, baseline)
def _rewrite_controller_archive(
archive: bytes,
*,
rename_path: str | None = None,
new_path: str | None = None,
mutate_path: str | None = None,
mutate: Any | None = None,
) -> bytes:
output = io.BytesIO()
with tarfile.open(fileobj=io.BytesIO(archive), mode="r:") as original:
with tarfile.open(fileobj=output, mode="w:") as rewritten:
for member in original.getmembers():
if member.isdir():
rewritten.addfile(member)
continue
body = original.extractfile(member)
assert body is not None
content = body.read()
if member.name == rename_path:
assert new_path is not None
member.name = new_path
if member.name == mutate_path:
assert mutate is not None
content = mutate(content)
member.size = len(content)
rewritten.addfile(member, io.BytesIO(content))
return output.getvalue()
def test_controller_source_provenance_failure_precedes_health_and_side_effects():
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
encoding="utf-8"
)
docker = FakeDocker(_legacy_helper_spec())
git = FakeProtectedGit(helper_source)
git.invalid_controller_release = bootstrap.MODERN_CONTROLLER_BASELINE
with pytest.raises(
bootstrap.BootstrapError,
match="controller_promoter_controller_provenance_invalid",
):
_run_bootstrap(
docker,
git,
health_sequence=[_modern_baseline_health()],
)
assert git.controller_release_checks == [bootstrap.MODERN_CONTROLLER_BASELINE]
assert docker.removed == []
assert docker.created == []
assert docker.started == []
def _retired_failed_child(helper_source: str) -> dict[str, Any]:
spec = bootstrap.render_modern_helper_spec(
helper_source,
@@ -678,6 +897,87 @@ def test_initial_controller_runtime_accepts_exact_current_compose_container():
)
def test_modern_source_baseline_accepts_its_fixed_source_mount_profile():
docker = FakeDocker(_legacy_helper_spec())
docker.containers["gitea-deploy-control"] = _modern_controller_source_inspect(
MODERN_CONTROLLER_REVISION
)
base_image_labels = bootstrap._inspect_controller_base_image(docker)
assert (
bootstrap._verify_controller_runtime(
docker,
health_payload=_modern_baseline_health(),
base_image_labels=base_image_labels,
)
== base_image_labels
)
def test_modern_source_mount_profile_is_distinct_from_postpromotion_topology():
source_mounts = _modern_controller_source_mounts(MODERN_CONTROLLER_REVISION)
desired_mounts = _modern_controller_mounts(MODERN_CONTROLLER_REVISION)
assert len(source_mounts) == 10
assert len(desired_mounts) == 11
assert bootstrap._modern_controller_source_mounts_match(
source_mounts, MODERN_CONTROLLER_REVISION
)
assert not bootstrap._modern_controller_source_mounts_match(
desired_mounts, MODERN_CONTROLLER_REVISION
)
assert not bootstrap._modern_controller_source_mounts_match(
source_mounts, "9" * 40
)
assert bootstrap._modern_controller_mounts_match(
desired_mounts, MODERN_CONTROLLER_REVISION
)
docker = FakeDocker(_legacy_helper_spec())
docker.containers["gitea-deploy-control"] = _modern_controller_source_inspect(
MODERN_CONTROLLER_REVISION
)
assert not bootstrap._verified_live_controller(
docker, source_revision=MODERN_CONTROLLER_REVISION
)
@pytest.mark.parametrize(
"mutation",
[
"missing_mount",
"extra_backup_mount",
"artifact_propagation",
"config_propagation",
"integer_rw",
"duplicate_mount",
],
)
def test_modern_source_mount_profile_rejects_topology_drift(mutation: str):
mounts = _modern_controller_source_mounts(MODERN_CONTROLLER_REVISION)
if mutation == "missing_mount":
mounts.pop()
elif mutation == "extra_backup_mount":
backup = next(
mount
for mount in _modern_controller_mounts(MODERN_CONTROLLER_REVISION)
if mount["Destination"] == "/forge-master-key-backup"
)
mounts.append(backup)
elif mutation == "artifact_propagation":
mounts[3]["Propagation"] = "rprivate"
elif mutation == "config_propagation":
mounts[9]["Propagation"] = "rprivate"
elif mutation == "integer_rw":
mounts[1]["RW"] = 0
elif mutation == "duplicate_mount":
mounts.append(dict(mounts[0]))
assert not bootstrap._modern_controller_source_mounts_match(
mounts, MODERN_CONTROLLER_REVISION
)
def test_immutable_legacy_helper_source_fixture_passes_bootstrap_identity_guard():
legacy_spec = _legacy_helper_spec()
docker = FakeDocker(legacy_spec)
@@ -1002,7 +1302,13 @@ def test_exact_failed_historical_child_is_removed_before_current_helper_launch()
result = _run_bootstrap(
docker,
git,
health_sequence=[legacy_health, legacy_health, legacy_health, _health()],
health_sequence=[
legacy_health,
legacy_health,
legacy_health,
legacy_health,
_health(),
],
)
assert result["status"] == "promoted"
@@ -1015,6 +1321,174 @@ def test_exact_failed_historical_child_is_removed_before_current_helper_launch()
) == 1
def test_current_214_baseline_is_verified_and_bound_to_new_helper():
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
encoding="utf-8"
)
docker = FakeDocker(_legacy_helper_spec())
docker.containers["gitea-deploy-control"] = _modern_controller_source_inspect(
MODERN_CONTROLLER_REVISION
)
git = FakeProtectedGit(helper_source)
result = _run_bootstrap(
docker,
git,
health_sequence=[_modern_baseline_health(), _health(), _health()],
)
assert result["status"] == "promoted"
assert git.controller_release_checks == [bootstrap.MODERN_CONTROLLER_BASELINE]
assert docker.created[0]["Labels"] == {
"itworx.controller.promoter": "true",
"itworx.source.repository": "jens/unraid-appops-gateway",
"itworx.source.revision": REVISION,
"itworx.controller.version": "2.1.14",
"itworx.controller.bootstrap.target.id": MODERN_CONTROLLER_CONTAINER_ID,
"itworx.controller.bootstrap.target.image": CONTROLLER_IMAGE_ID,
"itworx.controller.bootstrap.target.revision": MODERN_CONTROLLER_REVISION,
"itworx.controller.bootstrap.target.tree": MODERN_CONTROLLER_TREE,
"itworx.controller.bootstrap.target.version": "2.1.14",
}
def test_retired_failed_child_is_removed_only_after_current_214_profile_rechecks():
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
encoding="utf-8"
)
failed_child = _retired_failed_child(helper_source)
docker = FakeDocker(_legacy_helper_spec(), existing_child=failed_child)
docker.containers["gitea-deploy-control"] = _modern_controller_source_inspect(
MODERN_CONTROLLER_REVISION
)
git = FakeProtectedGit(helper_source)
result = _run_bootstrap(
docker,
git,
health_sequence=[
_modern_baseline_health(),
_modern_baseline_health(),
_modern_baseline_health(),
_modern_baseline_health(),
_health(),
_health(),
],
)
assert result["status"] == "promoted"
assert docker.removed == [RETIRED_HELPER_ID]
assert docker.created[0]["Labels"]["itworx.controller.bootstrap.target.id"] == (
MODERN_CONTROLLER_CONTAINER_ID
)
@pytest.mark.parametrize(
"mutation",
[
"container_id",
"image_id",
"source_revision",
"source_tree",
"artifact_digest",
"version",
"compose_label",
"extra_label",
"missing_label",
"extra_mount",
"missing_mount",
"writable_mount",
"integer_rw_mount",
"missing_rw_mount",
"malformed_mount_source",
"malformed_mount_name",
"mount_alias",
"dead_controller",
"paused_controller",
"restarting_controller",
"missing_state_flag",
"health_revision",
"health_tree",
"health_artifact",
"health_version",
],
)
def test_retired_cleanup_refuses_any_current_214_identity_or_topology_drift(mutation: str):
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
encoding="utf-8"
)
failed_child = _retired_failed_child(helper_source)
docker = FakeDocker(_legacy_helper_spec(), existing_child=failed_child)
controller = _modern_controller_source_inspect(MODERN_CONTROLLER_REVISION)
health = _modern_baseline_health()
if mutation == "container_id":
controller["Id"] = "9" * 64
elif mutation == "image_id":
controller["Image"] = "sha256:" + "9" * 64
elif mutation == "source_revision":
controller["Mounts"][3]["Source"] = (
"/var/lib/docker/volumes/itworx-controller-hub-releases/_data/"
+ "9" * 40
)
elif mutation == "source_tree":
health["artifact"]["sourceTree"] = "9" * 40
elif mutation == "artifact_digest":
health["artifact"]["artifactSha256"] = "9" * 64
elif mutation == "version":
controller["Config"]["Labels"]["org.opencontainers.image.version"] = "forged"
elif mutation == "compose_label":
controller["Config"]["Labels"]["com.docker.compose.service"] = "other"
elif mutation == "extra_label":
controller["Config"]["Labels"]["unexpected"] = "value"
elif mutation == "missing_label":
del controller["Config"]["Labels"]["tech.itworx.role"]
elif mutation == "extra_mount":
controller["Mounts"].append(dict(controller["Mounts"][0]))
controller["Mounts"][-1]["Destination"] = "/unexpected"
elif mutation == "missing_mount":
controller["Mounts"].pop()
elif mutation == "writable_mount":
controller["Mounts"][1]["RW"] = True
elif mutation == "integer_rw_mount":
controller["Mounts"][1]["RW"] = 0
elif mutation == "missing_rw_mount":
del controller["Mounts"][1]["RW"]
elif mutation == "malformed_mount_source":
controller["Mounts"][1]["Source"] = 123
elif mutation == "malformed_mount_name":
controller["Mounts"][8]["Name"] = None
elif mutation == "mount_alias":
controller["Mounts"][9]["Source"] = "/tmp/controller-config"
elif mutation == "dead_controller":
controller["State"]["Dead"] = True
elif mutation == "paused_controller":
controller["State"]["Paused"] = True
elif mutation == "restarting_controller":
controller["State"]["Restarting"] = True
elif mutation == "missing_state_flag":
del controller["State"]["Dead"]
elif mutation == "health_revision":
health["artifact"]["sourceRevision"] = "9" * 40
elif mutation == "health_tree":
health["artifact"]["sourceTree"] = "9" * 40
elif mutation == "health_artifact":
health["artifact"]["artifactSha256"] = "9" * 64
elif mutation == "health_version":
health["artifact"]["controllerVersion"] = "2.1.13"
docker.containers["gitea-deploy-control"] = controller
with pytest.raises(bootstrap.BootstrapError):
_run_bootstrap(
docker,
FakeProtectedGit(helper_source),
health_sequence=[health, health, health],
)
assert docker.removed == []
assert docker.created == []
assert docker.started == []
@pytest.mark.parametrize(
"mutation",
[
@@ -0,0 +1,314 @@
from __future__ import annotations
import copy
import subprocess
import pytest
from scripts import bootstrap_controller_artifact_promoter as bootstrap
from backend.tests import test_controller_artifact_bootstrap as original
FAILED_REVISION = "1" * 40
FAILED_TREE = "2" * 40
FAILED_ID = "3" * 64
class Git(original.FakeProtectedGit):
def verify_failed_modern_ancestor(self, revision, current_revision):
if (
revision != FAILED_REVISION
or current_revision != self.revision
or not self.retired_tree_valid
):
raise bootstrap.BootstrapError(
"controller_promoter_failed_helper_not_ancestor"
)
return FAILED_TREE
def read_blob(self, revision, path):
if revision == FAILED_REVISION:
self.blobs_read.append((revision, path))
assert path == bootstrap.MODERN_HELPER_SOURCE_PATH
return self.helper_source.encode()
return super().read_blob(revision, path)
class Docker(original.FakeDocker):
def inspect_container(self, reference):
if reference == FAILED_ID:
return self.existing_child
return super().inspect_container(reference)
def remove_retired_failed_container(self, identifier):
assert identifier == FAILED_ID
self.removed.append(identifier)
self.existing_child = None
def inputs():
source = (original.ROOT / bootstrap.MODERN_HELPER_SOURCE_PATH).read_text()
child = original._retired_failed_child(source)
baseline = bootstrap.MODERN_CONTROLLER_BASELINE
spec = bootstrap.render_modern_helper_spec(
source,
revision=FAILED_REVISION,
image_id=baseline.image_id,
current_controller_id=baseline.container_id,
current_controller_revision=baseline.revision,
current_controller_tree=baseline.tree,
current_controller_version=baseline.version,
)
child["Id"] = FAILED_ID
child["Config"].update(
{
"Hostname": original.HOST_HOSTNAME,
"Cmd": spec["Cmd"],
"Labels": {**original._controller_labels(), **spec["Labels"]},
"Env": ["PATH=/usr/bin"],
"WorkingDir": "/tmp",
"Entrypoint": None,
"Domainname": "",
"AttachStdin": False,
"AttachStdout": False,
"AttachStderr": False,
"Tty": False,
"OpenStdin": False,
"StdinOnce": False,
"OnBuild": None,
}
)
child["State"].update({"Paused": False, "Restarting": False, "Dead": False})
child["HostConfig"].update(copy.deepcopy(bootstrap.FAILED_HELPER_HOST_DEFAULTS))
child["HostConfig"]["RestartPolicy"] = {"Name": "no", "MaximumRetryCount": 0}
docker = Docker(original._legacy_helper_spec(), existing_child=child)
docker.containers[bootstrap.CONTROLLER_NAME] = (
original._modern_controller_source_inspect(baseline.revision)
)
docker.images[baseline.image_id]["Config"].update(
{
"Env": ["PATH=/usr/bin"],
"WorkingDir": "/tmp",
"Entrypoint": None,
"Shell": ["/bin/bash", "-c"],
}
)
return docker, Git(source), child
def run(docker, git):
return original._run_bootstrap(
docker,
git,
health_sequence=[
original._modern_baseline_health(),
original._modern_baseline_health(),
original._modern_baseline_health(),
original._modern_baseline_health(),
original._health(),
original._health(),
],
)
def test_noncurrent_protected_ancestor_failure_releases_only_its_verified_name():
docker, git, child = inputs()
result = run(docker, git)
assert result["status"] == "promoted"
assert docker.removed == [FAILED_ID]
assert (
git.blobs_read.count((FAILED_REVISION, bootstrap.MODERN_HELPER_SOURCE_PATH))
== 1
)
assert (
docker.created[0]["Labels"]["itworx.controller.bootstrap.target.id"]
== bootstrap.MODERN_CONTROLLER_ID
)
@pytest.mark.parametrize(
"change",
[
"running",
"succeeded",
"bool_exit",
"paused",
"dead",
"restarting",
"source",
"target",
"command",
"entrypoint",
"environment",
"mount",
"host_capability",
"controller",
"appops",
"ancestor",
],
)
def test_noncurrent_failed_child_recovery_refuses_unattested_identity_or_baseline(
change,
):
docker, git, child = inputs()
if change == "running":
child["State"].update(Running=True, Status="running")
elif change == "succeeded":
child["State"]["ExitCode"] = 0
elif change == "bool_exit":
child["State"]["ExitCode"] = True
elif change in {"paused", "dead", "restarting"}:
child["State"][change.capitalize()] = True
elif change == "source":
child["Config"]["Labels"]["itworx.source.repository"] = "other/repository"
elif change == "target":
child["Config"]["Labels"]["itworx.controller.bootstrap.target.id"] = "9" * 64
elif change == "command":
child["Config"]["Cmd"][2] += "\necho changed"
elif change == "entrypoint":
child["Config"]["Entrypoint"] = ["/bin/sh"]
elif change == "environment":
child["Config"]["Env"].append("CUSTOM_VARIABLE=extra")
elif change == "mount":
child["Mounts"][2]["RW"] = True
elif change == "host_capability":
child["HostConfig"]["CapAdd"] = ["SYS_ADMIN"]
elif change == "controller":
docker.containers[bootstrap.CONTROLLER_NAME]["Id"] = "9" * 64
elif change == "appops":
docker.containers[original.APPOPS_CONTAINER_ID]["Image"] = "sha256:" + "9" * 64
else:
git.retired_tree_valid = False
with pytest.raises(bootstrap.BootstrapError):
run(docker, git)
assert docker.removed == docker.created == docker.started == []
def test_noncurrent_child_identity_drift_on_final_named_reread_refuses_delete():
docker, git, child = inputs()
docker.named_inspect_override = copy.deepcopy(child)
docker.named_inspect_override["Id"] = "9" * 64
docker.named_inspect_override_at = 3
with pytest.raises(bootstrap.BootstrapError):
run(docker, git)
assert not docker.removed
def test_dynamic_failed_child_delete_retains_nonforce_and_no_volume_policy(monkeypatch):
client = bootstrap.DockerSocketClient()
calls = []
monkeypatch.setattr(
client,
"_request",
lambda method, path, **kw: (calls.append((method, path)) or (204, b"")),
)
client.remove_retired_failed_container(FAILED_ID)
assert calls == [("DELETE", f"/containers/{FAILED_ID}?force=false&v=false")]
@pytest.mark.parametrize(
"change",
["cpu", "dns", "masked_paths", "logging", "boolean_default", "unknown_option"],
)
def test_failed_child_requires_complete_known_host_defaults(change):
docker, git, child = inputs()
if change == "cpu":
child["HostConfig"]["CpuShares"] = 100
elif change == "dns":
child["HostConfig"]["Dns"] = ["192.0.2.1"]
elif change == "masked_paths":
child["HostConfig"]["MaskedPaths"] = []
elif change == "logging":
child["HostConfig"]["LogConfig"]["Type"] = "none"
elif change == "boolean_default":
child["HostConfig"]["CpuShares"] = False
else:
child["HostConfig"]["UnreviewedOption"] = None
with pytest.raises(bootstrap.BootstrapError):
run(docker, git)
assert not docker.removed
def test_raw_docker_api_nil_mount_omission_matches_cli_null():
docker, git, child = inputs()
child["HostConfig"].pop("Mounts")
assert run(docker, git)["status"] == "promoted"
def test_appops_drift_on_final_baseline_reread_refuses_cleanup():
docker, git, child = inputs()
docker.appops_inspect_override = copy.deepcopy(
docker.containers[original.APPOPS_CONTAINER_ID]
)
docker.appops_inspect_override["State"]["Running"] = False
docker.appops_inspect_override_at = 3
with pytest.raises(bootstrap.BootstrapError):
run(docker, git)
assert not docker.removed
def test_protected_main_drift_immediately_before_delete_refuses_cleanup():
docker, git, child = inputs()
git.fail_current_main_at = 5
with pytest.raises(bootstrap.BootstrapError):
run(docker, git)
assert not docker.removed
@pytest.mark.parametrize(
"identifier", ["", "short", "A" * 64, "3" * 63, "../containers/other"]
)
def test_delete_adapter_rejects_noncanonical_full_container_identity(
monkeypatch, identifier
):
client = bootstrap.DockerSocketClient()
monkeypatch.setattr(
client,
"_request",
lambda *_args, **_kw: pytest.fail("unexpected Docker mutation"),
)
with pytest.raises(bootstrap.BootstrapError):
client.remove_retired_failed_container(identifier)
@pytest.mark.parametrize("scenario", ["ancestor", "unrelated", "replace_ref"])
def test_git_ancestor_proof_reads_original_protected_objects(
tmp_path, monkeypatch, scenario
):
def git(*args):
return (
subprocess.run(
["git", "-C", str(tmp_path), *args],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
.stdout.decode()
.strip()
)
git("init", "-q", "-b", "main")
git("config", "user.name", "Test")
git("config", "user.email", "test@example.invalid")
path = tmp_path / "tracked"
path.write_text("protected ancestor")
git("add", ".")
git("commit", "-qm", "ancestor")
old = git("rev-parse", "HEAD")
tree = git("rev-parse", "HEAD^{tree}")
path.write_text("new protected main")
git("commit", "-qam", "main")
current = git("rev-parse", "HEAD")
monkeypatch.setattr(bootstrap, "PROTECTED_REPO", str(tmp_path / ".git"))
proof = bootstrap.ProtectedGitRepository()
if scenario == "replace_ref":
git("replace", old, current)
assert git("rev-parse", f"{old}^{{tree}}") != tree
if scenario == "unrelated":
git("checkout", "-qb", "side", old)
path.write_text("unrelated child")
git("commit", "-qam", "side")
old = git("rev-parse", "HEAD")
with pytest.raises(bootstrap.BootstrapError):
proof.verify_failed_modern_ancestor(old, current)
else:
assert proof.verify_failed_modern_ancestor(old, current) == tree
@@ -0,0 +1,354 @@
from __future__ import annotations
import json
import shutil
import subprocess
from types import SimpleNamespace
import httpx
import pytest
from sqlmodel import Session, SQLModel, create_engine
DIAGNOSTIC = (
"APPOPS_CONTROLLER_PROMOTION_DIAGNOSTIC "
"code=helper_failed phase=source_archive exitCode=19"
)
SAFE_DIAGNOSTICS = {"code": "helper_failed", "phase": "source_archive", "exitCode": 19}
SECRET = "must-never-escape-helper-log"
def _docker_frame(stream_type, payload):
return bytes([stream_type, 0, 0, 0]) + len(payload).to_bytes(4, "big") + payload
def _default_docker_logs():
marker = DIAGNOSTIC.encode()
split_at = len(marker) // 2
return b"".join(
(
_docker_frame(1, b"helper started\n"),
_docker_frame(2, f"token={SECRET}\n".encode() + marker[:split_at]),
_docker_frame(2, marker[split_at:] + b"\n"),
)
)
class _Response:
def __init__(self, payload=None):
self.payload = payload or {}
def json(self):
return self.payload
class _Portainer:
def __init__(self, *, exit_code=19, logs=None, running=False):
self.exit_code = exit_code
self.logs = logs if logs is not None else _default_docker_logs()
self.running = running
self.requests = []
self.request_kwargs = []
def _config(self):
return SimpleNamespace(endpoint_id="1")
def list_containers(self):
return {"containers": []}
def _request(self, method, path, **kwargs):
self.requests.append((method, path))
self.request_kwargs.append(kwargs)
if method == "POST" and path.endswith("/containers/create"):
return _Response({"Id": "helper-123"})
if method == "GET" and path.endswith("/helper-123/json"):
return _Response(
{"State": {"Running": self.running, "ExitCode": self.exit_code}}
)
if method == "GET" and path.endswith("/helper-123/logs"):
return httpx.Response(status_code=200, content=self.logs)
return _Response()
def get_container_logs(self, container_name, tail=100):
return {"status": "ok", "container": container_name, "logs": self.logs}
def _session(tmp_path):
engine = create_engine(f"sqlite:///{tmp_path / 'controller-diagnostics.db'}")
SQLModel.metadata.create_all(engine)
return Session(engine)
def test_helper_spec_emits_only_fixed_failure_markers():
from backend.app.services.controller_artifact_helper_spec import (
helper_container_spec,
)
command = helper_container_spec(revision="a" * 40, image_id="sha256:" + "b" * 64)["Cmd"][2]
assert "APPOPS_CONTROLLER_PROMOTION_DIAGNOSTIC" in command
assert "phase=source_commit" in command
assert "phase=source_archive" in command
assert "phase=upgrade" in command
assert "phase=cleanup" in command
assert "exitCode=" in command
def test_generated_helper_command_parses_without_evaluating_command_substitutions():
from backend.app.services.controller_artifact_helper_spec import (
helper_container_spec,
)
bash = shutil.which("bash")
if bash is None:
pytest.skip("bash is required to parse the generated helper command")
command = helper_container_spec(
revision="a" * 40,
image_id="sha256:" + "b" * 64,
)["Cmd"][2]
result = subprocess.run(
[bash, "-n"],
input=command,
capture_output=True,
text=True,
shell=False,
)
assert result.returncode == 0, result.stderr
def test_failed_workspace_validation_does_not_run_cleanup_or_remove_unvalidated_path():
from backend.app.services.controller_artifact_helper_spec import (
helper_container_spec,
)
bash = shutil.which("bash")
if bash is None:
pytest.skip("bash is required to exercise the generated helper command")
command = helper_container_spec(
revision="a" * 40,
image_id="sha256:" + "b" * 64,
)["Cmd"][2]
shell_stubs = """\
stat() {
case "$*" in
*"%a:%u:%g"*) printf '1777:0:0\\n' ;;
*) printf '999:700\\n' ;;
esac
}
mktemp() { printf '/tmp\\n'; }
rm() { printf 'RM_WOULD_RUN\\n'; }
"""
result = subprocess.run(
[bash, "-s"],
input=shell_stubs + command,
capture_output=True,
text=True,
shell=False,
)
assert result.returncode != 0
assert "phase=workspace_validate" in result.stderr
assert "RM_WOULD_RUN" not in result.stdout + result.stderr
def test_diagnostic_parser_accepts_fixed_marker_and_returns_no_log_text():
from backend.app.services import controller_artifact_promoter as promoter
parser = getattr(promoter, "parse_helper_diagnostics", None)
assert callable(parser), "the helper diagnostic parser is not implemented"
assert parser(f"safe status line\n{DIAGNOSTIC}", 19) == SAFE_DIAGNOSTICS
def test_docker_log_demux_reads_stdout_stderr_and_markers_across_frames():
from backend.app.services import controller_artifact_promoter as promoter
demux = getattr(promoter, "_decode_docker_log_stream", None)
assert callable(demux), "the bounded Docker stdcopy log demux is not implemented"
marker = DIAGNOSTIC.encode()
split_at = len(marker) // 2
framed = b"".join(
(
_docker_frame(1, b"stdout line\n"),
_docker_frame(2, b"stderr prefix\n" + marker[:split_at]),
_docker_frame(2, marker[split_at:] + b"\n"),
)
)
assert demux(framed) == f"stdout line\nstderr prefix\n{DIAGNOSTIC}\n"
@pytest.mark.parametrize(
"framed",
[
b"",
_docker_frame(3, b"unknown stream"),
bytes([2, 1, 0, 0]) + (1).to_bytes(4, "big") + b"x",
bytes([2, 0, 0, 0]),
_docker_frame(2, b"truncated")[:-1],
_docker_frame(2, b"\xff"),
bytes([2, 0, 0, 0]) + (4001).to_bytes(4, "big"),
b"".join(_docker_frame(1, b"x") for _ in range(201)),
],
)
def test_docker_log_demux_rejects_invalid_truncated_or_oversized_streams(framed):
from backend.app.services import controller_artifact_promoter as promoter
demux = getattr(promoter, "_decode_docker_log_stream", None)
assert callable(demux), "the bounded Docker stdcopy log demux is not implemented"
assert demux(framed) is None
@pytest.mark.parametrize(
("log_text", "exit_code"),
[
("APPOPS_CONTROLLER_PROMOTION_DIAGNOSTIC code=helper_failed phase=unlisted exitCode=19", 19),
("APPOPS_CONTROLLER_PROMOTION_DIAGNOSTIC code=helper_failed phase=source_archive exitCode=19 extra=secret", 19),
("APPOPS_CONTROLLER_PROMOTION_DIAGNOSTIC code=helper_failed phase=source_archive exitCode=256", 19),
(DIAGNOSTIC, 256),
(DIAGNOSTIC + "\n" + DIAGNOSTIC, 19),
("x" * 4001 + "\n" + DIAGNOSTIC, 19),
(DIAGNOSTIC, True),
],
)
def test_diagnostic_parser_rejects_malformed_oversized_or_adversarial_input(log_text, exit_code):
from backend.app.services import controller_artifact_promoter as promoter
parser = getattr(promoter, "parse_helper_diagnostics", None)
assert callable(parser), "the helper diagnostic parser is not implemented"
expected_exit = exit_code if type(exit_code) is int and 0 <= exit_code <= 255 else None
assert parser(log_text, exit_code) == {
"code": "helper_failed",
"phase": "unknown",
"exitCode": expected_exit,
}
def test_helper_failure_diagnostics_reach_job_audit_and_operation_result_without_logs(tmp_path, monkeypatch):
from backend.app.db.models import AuditEvent, JobRun
from backend.app.services import action_execution
from backend.app.services.controller_artifact_promoter import (
ControllerArtifactPromoter,
)
portainer = _Portainer()
promoted = ControllerArtifactPromoter(portainer=portainer)._run_helper(
revision="a" * 40,
image_id="sha256:" + "b" * 64,
)
assert promoted["status"] == "failed"
assert promoted["diagnostics"] == SAFE_DIAGNOSTICS
assert "logTail" not in promoted
assert any(
method == "GET" and path.endswith("/helper-123/logs")
for method, path in portainer.requests
)
log_request_index = next(
index
for index, (method, path) in enumerate(portainer.requests)
if method == "GET" and path.endswith("/helper-123/logs")
)
assert portainer.request_kwargs[log_request_index]["params"] == {
"stdout": 1,
"stderr": 1,
"tail": 200,
}
assert SECRET not in json.dumps(promoted)
monkeypatch.setattr(ControllerArtifactPromoter, "run", lambda self: promoted)
with _session(tmp_path) as session:
result = action_execution._execute_controller_artifact_promote(
session,
source="operator",
risk="high",
reason="diagnose the failed promotion",
original_policy_decision="approval_required",
)
job = session.get(JobRun, result["jobId"])
audit = session.get(AuditEvent, result["auditId"])
assert result["status"] == "failed"
assert result["diagnostics"] == SAFE_DIAGNOSTICS
assert job is not None and "phase=source_archive" in job.result_summary
assert audit is not None
assert audit.after_state["diagnostics"] == SAFE_DIAGNOSTICS
assert audit.redacted_metadata["diagnostics"] == SAFE_DIAGNOSTICS
durable = json.dumps(
{
"operation": result,
"jobSummary": job.result_summary,
"auditAfter": audit.after_state,
"auditMetadata": audit.redacted_metadata,
}
)
assert SECRET not in durable
assert "logTail" not in durable
@pytest.mark.parametrize(
"logs",
[
b"",
_docker_frame(
2,
b"APPOPS_CONTROLLER_PROMOTION_DIAGNOSTIC code=helper_failed phase=unknown exitCode=19",
),
_docker_frame(
2,
b"APPOPS_CONTROLLER_PROMOTION_DIAGNOSTIC code=helper_failed phase=source_archive exitCode=19 extra=value",
),
_docker_frame(2, b"x" * 4001),
_docker_frame(2, b"truncated")[:-1],
],
)
def test_missing_or_unparseable_helper_phase_is_explicitly_unknown(logs):
from backend.app.services.controller_artifact_promoter import (
ControllerArtifactPromoter,
)
result = ControllerArtifactPromoter(portainer=_Portainer(exit_code=19, logs=logs))._run_helper(
revision="a" * 40,
image_id="sha256:" + "b" * 64,
)
assert result["status"] == "failed"
assert result["diagnostics"] == {
"code": "helper_failed",
"phase": "unknown",
"exitCode": 19,
}
assert "logTail" not in result
assert SECRET not in json.dumps(result)
def test_successful_helper_remains_promoted_and_never_returns_logs():
from backend.app.services.controller_artifact_promoter import (
ControllerArtifactPromoter,
)
portainer = _Portainer(exit_code=0)
portainer.get_container_logs = lambda *args, **kwargs: pytest.fail("success must not fetch helper logs")
result = ControllerArtifactPromoter(portainer=portainer)._run_helper(
revision="a" * 40,
image_id="sha256:" + "b" * 64,
)
assert result == {"status": "promoted", "revision": "a" * 40}
assert not any(path.endswith("/logs") for _, path in portainer.requests)
def test_helper_timeout_remains_outcome_unknown_without_cleanup_or_log_fetch(monkeypatch):
from backend.app.services import controller_artifact_promoter as promoter
portainer = _Portainer(running=True)
portainer.get_container_logs = lambda *args, **kwargs: pytest.fail(
"a running helper has no terminal diagnostic to read"
)
clock_values = iter((100.0, 341.0))
monkeypatch.setattr(promoter.time, "monotonic", lambda: next(clock_values))
result = promoter.ControllerArtifactPromoter(portainer=portainer)._run_helper(
revision="a" * 40,
image_id="sha256:" + "b" * 64,
)
assert result == {
"status": "outcome_unknown",
"revision": "a" * 40,
"reason": "controller_promoter_timeout",
"helperId": "helper-123",
}
assert not any(method == "DELETE" for method, _ in portainer.requests)
assert not any(path.endswith("/logs") for _, path in portainer.requests)
@@ -24,6 +24,29 @@ 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"
# 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",
@@ -279,6 +302,220 @@ def test_promotion_does_not_prepare_backup_volume_when_staged_configuration_fail
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")
@@ -542,6 +779,90 @@ def test_compose_accepts_empty_bind_normalization_and_canonicalizes_both_managed
}
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": "rslave"},
{"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", [False, True])
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"] = "rprivate"
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": "rprivate"} 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": "rslave"},
{"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"),
[
@@ -869,6 +1190,117 @@ def _forge_secret_entries():
}
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),
@@ -931,7 +1363,7 @@ def test_forge_secret_tar_owner_drift_is_rejected():
module._read_tar_tree(output.getvalue())
def test_config_writer_uses_archive_mode_to_preserve_forge_leaf_owners(monkeypatch):
def test_config_writer_uses_tar_ownership_without_copy_archive_mode(monkeypatch):
tree = (
{
'projects.json': (b'{}', 0o600),
@@ -942,8 +1374,10 @@ def test_config_writer_uses_archive_mode_to_preserve_forge_leaf_owners(monkeypat
{'': 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)
@@ -953,11 +1387,17 @@ def test_config_writer_uses_archive_mode_to_preserve_forge_leaf_owners(monkeypat
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: b'archive')
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: {})
@@ -967,9 +1407,11 @@ def test_config_writer_uses_archive_mode_to_preserve_forge_leaf_owners(monkeypat
module._install_config_tree(tree, IMAGE_ID)
assert [command for command in commands if command[:2] == ['docker', 'cp']] == [[
'docker', 'cp', '-a', '-', f'{writer_id}:{module.TARGET}/{module.CONFIG_DIR}',
'docker', 'cp', '-', f'{writer_id}:{module.TARGET}/{module.CONFIG_DIR}',
]]
assert copy_payloads == [b'archive']
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(
@@ -1215,6 +1657,7 @@ def test_writer_is_exact_image_only_bounded_and_has_no_host_paths(kind):
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
@@ -1223,6 +1666,21 @@ def test_writer_is_exact_image_only_bounded_and_has_no_host_paths(kind):
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
def test_promoted_compose_actions_use_only_stdin_and_fixed_service(compose):
baseline, calls = compose
rendered = module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
@@ -1307,9 +1765,44 @@ def test_installer_release_order_matches_renderer_contract():
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_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"
)
@@ -1319,7 +1812,7 @@ def test_installer_release_order_matches_renderer_contract():
)
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"}
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"}
)
assert module.LEGACY_RELEASE_FILES_WITHOUT_ROLE_HELPER == tuple(
name for name in module.LEGACY_RELEASE_FILES_WITH_ROLE_HELPER
@@ -1339,6 +1832,106 @@ def test_installer_release_order_matches_renderer_contract():
]
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")
assert "ops/gitea-deploy-broker" in powershell.split("& git archive", 1)[1].splitlines()[0]
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)
@@ -1970,3 +2563,75 @@ def test_current_e854_crlf_exception_integrates_with_git_source_verification(mon
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)
@@ -0,0 +1,140 @@
"""Runtime readiness is independent of storage for enrolling a new target."""
from __future__ import annotations
import http.client
import io
import json
import threading
from email.message import Message
import pytest
from test_deploy_broker import BROKER_ROOT, _load_module
@pytest.fixture
def controller(monkeypatch, tmp_path):
module = _load_module("appops_runtime_capabilities_test", BROKER_ROOT / "status_server.py")
repository_root = tmp_path / "repos"
repository_root.mkdir()
secret_path = tmp_path / "bootstrap"
secret = b"runtime-capabilities-test-" + b"x" * 48
secret_path.write_bytes(secret)
secret_path.chmod(0o600)
monkeypatch.setattr(module, "APPOPS_ENROLLMENT_BOOTSTRAP_SECRET_FILE", secret_path)
monkeypatch.setattr(module, "APPOPS_NONCE_ROOT", tmp_path / "nonces")
monkeypatch.setattr(module, "REPOSITORY_ROOT", repository_root)
monkeypatch.setattr(module, "_controller_release", lambda: {"revision": "a" * 40})
monkeypatch.setattr(module, "_projects_registry_is_safe", lambda: True)
monkeypatch.setattr(module, "_load_projects", lambda: {"owner/existing": {}})
monkeypatch.setattr(module, "_refresh_appdata_root_selection", lambda: None)
monkeypatch.setattr(module, "trusted_appdata_roots_readiness", lambda: [
{"root": "/srv/example/appdata", "status": "unavailable", "reason": "unsafe_permissions"},
])
monkeypatch.setattr(module, "managed_storage_readiness", lambda: {"status": "needs_initialization"})
server = module.BoundedThreadingHTTPServer(("127.0.0.1", 0), module.Handler)
serving = threading.Thread(target=server.serve_forever, daemon=True)
serving.start()
counter = 0
def request(*, authenticated=True, replay_headers=None):
nonlocal counter
counter += 1
body = b"{}"
path = "/v1/appops/enrollments/capabilities"
timestamp = str(int(module.time.time()))
nonce = f"{counter:032x}"
headers = replay_headers or {
"Content-Type": "application/json",
"X-ITWorx-Deployment-Timestamp": timestamp,
"X-ITWorx-Deployment-Nonce": nonce,
"X-ITWorx-Deployment-Signature": module._request_signature(
secret, method="POST", path=path, timestamp=timestamp, nonce=nonce, raw_body=body,
) if authenticated else "0" * 64,
}
client = http.client.HTTPConnection(*server.server_address, timeout=2)
client.request("POST", path, body=body, headers=headers)
response = client.getresponse()
payload = json.loads(response.read())
client.close()
assert secret.decode() not in json.dumps(payload)
return response.status, payload, headers
yield module, request
server.shutdown()
server.server_close()
serving.join(timeout=2)
def test_registered_runtime_ready_without_new_enrollment_storage(controller):
_, request = controller
status, payload, headers = request()
assert status == 200
assert payload["status"] == "ready"
assert payload["registry"] == {"status": "ready", "registeredProjectCount": 1}
assert payload["enrollmentReadiness"] == {
"status": "unavailable", "reason": "trusted_appdata_roots_unavailable",
}
for name in ("deploymentApply", "deploymentVerify", "rollback", "exactTargetPolicy", "hmacV2", "replayProtection"):
assert payload["features"][name] is True
for name in ("enrollment", "targetRegistration", "runtimeSecretSealing"):
assert payload["features"][name] is False
assert request(replay_headers=headers)[0] == 403
assert request(authenticated=False)[0] == 403
@pytest.mark.parametrize("unavailable", ["registry", "registry_content", "artifact", "repository"])
def test_runtime_prerequisites_remain_fail_closed(controller, monkeypatch, tmp_path, unavailable):
module, request = controller
if unavailable == "registry":
monkeypatch.setattr(module, "_projects_registry_is_safe", lambda: False)
elif unavailable == "registry_content":
def invalid_registry():
raise ValueError("invalid registry")
monkeypatch.setattr(module, "_load_projects", invalid_registry)
elif unavailable == "artifact":
monkeypatch.setattr(module, "_controller_release", lambda: None)
else:
monkeypatch.setattr(module, "REPOSITORY_ROOT", tmp_path / "missing")
status, payload, _ = request()
assert status == 503
assert payload["reason"] == "controller_runtime_prerequisite_unavailable"
for name in ("deploymentApply", "deploymentVerify", "rollback", "exactTargetPolicy", "enrollment", "targetRegistration"):
assert payload["features"][name] is False
def test_enrollment_recovers_when_safe_root_becomes_ready(controller, monkeypatch):
module, request = controller
assert request()[1]["features"]["enrollment"] is False
monkeypatch.setattr(module, "trusted_appdata_roots_readiness", lambda: [{"status": "ready"}])
status, payload, _ = request()
assert status == 200
assert payload["enrollmentReadiness"] == {"status": "ready", "reason": ""}
for name in ("enrollment", "targetRegistration", "runtimeSecretSealing"):
assert payload["features"][name] is True
@pytest.mark.parametrize("current,reason", [
({"status": "blocked", "checks": [{"name": "required_push_validation", "passed": False}]},
"controller_preflight_failed_required_push_validation"),
({"status": "preflight_ok", "currentRevision": "b" * 40, "checks": []},
"current_revision_changed"),
])
def test_self_upgrade_reports_failed_gate_before_missing_current_revision(controller, monkeypatch, current, reason):
module, _ = controller
monkeypatch.setattr(module, "_self_upgrade_preflight", lambda _revision: dict(current))
monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_args, **_kwargs: True)
intent = {"repository": module.SELF_UPGRADE_REPOSITORY, "revision": "c" * 40,
"reason": "Validated infrastructure repair", "expectedCurrentRevision": "a" * 40}
body = json.dumps(intent).encode()
handler = module.Handler.__new__(module.Handler)
handler.path = module.SELF_UPGRADE_PREFLIGHT_PATH
handler.headers = Message()
handler.headers["Content-Length"] = str(len(body))
handler.headers["Content-Type"] = "application/json"
handler.rfile = io.BytesIO(body)
handler._trusted_client = lambda: True
responses = []
handler._send_json = lambda status, payload: responses.append((status, json.loads(payload)))
handler._handle_self_upgrade()
assert responses[0][0] == 409
assert responses[0][1]["reason"] == reason
+211 -13
View File
@@ -764,6 +764,41 @@ def test_appops_legacy_intent_accepts_only_the_reviewed_dockerfile_blob_pair(
deploy_module._appops_validate_intent_payload(unknown_update)
def test_new_database_pair_keeps_protected_full_and_runtime_intent_proofs_strict(
deploy_module,
):
_repository, _metadata, _source, _main, _backup, intent = (
_appops_rollback_intent_fixture(deploy_module, "c" * 40, "a" * 64)
)
def rebind_migration_proof(value):
proof = value["migrationProof"]
evidence = value["rollbackSnapshot"]["appops_legacy"]
evidence["sourceProofSha256"] = deploy_module._canonical_json_sha256(proof["source"])
evidence["protectedMainSha256"] = deploy_module._canonical_json_sha256(proof["protectedMain"])
evidence["migrationProofSha256"] = deploy_module._canonical_json_sha256(proof)
intent["migrationProof"]["source"]["databaseTree"] = (
"21d29c1799118fdc43946c2b6fd5585ccc2c4db1"
)
rebind_migration_proof(intent)
deploy_module._appops_validate_intent_payload(intent)
weak_protected = copy.deepcopy(intent)
weak_protected["migrationProof"]["protectedMain"]["validationContext"] = (
"Managed validation / partial (push)"
)
rebind_migration_proof(weak_protected)
with pytest.raises(deploy_module.DeployError, match="protected-main proof is invalid"):
deploy_module._appops_validate_intent_payload(weak_protected)
incomplete_runtime = copy.deepcopy(intent)
incomplete_runtime["migrationProof"]["runtime"].pop("healthSha256")
rebind_migration_proof(incomplete_runtime)
with pytest.raises(deploy_module.DeployError, match="legacy runtime proof is invalid"):
deploy_module._appops_validate_intent_payload(incomplete_runtime)
def test_appops_cli_rollback_retries_after_state_write_crash_without_synthetic_receipt(
deploy_module, monkeypatch, tmp_path, capsys
):
@@ -1537,6 +1572,47 @@ def test_appops_state_write_rejects_compare_and_swap_drift(deploy_module, monkey
assert path.read_bytes() == original
def test_registered_legacy_state_cas_drift_preserves_external_state_and_records_recovery(
deploy_module, monkeypatch, tmp_path
):
repository = "jens/quiz-monitor"
request_id = "6f9619ff-8b86-4011-b42d-00c04fc964ff"
revision = "a" * 40
state_root = tmp_path / "state"
state_root.mkdir()
monkeypatch.setattr(deploy_module, "STATE_ROOT", state_root)
monkeypatch.setattr(deploy_module, "_registered_legacy_receipt_key", lambda: b"k" * 32)
project = {
"slug": "quiz-monitor", "branch": "main", "compose_project": "quiz-monitor",
"compose_files": ["docker-compose.yml"],
"containers": ["quiz-monitor-quiz-server-1"],
"source_dir": "/srv/example/appdata/quiz-monitor/source",
"_registered_legacy_request_id": request_id,
"_registered_legacy_authorization": {
"requestId": request_id, "revision": revision,
"expectedStateSha256": "0" * 64,
},
}
path = state_root / "quiz-monitor.json"
external_state = b'{"schema_version":3,"live_revision":"independent-update"}\n'
path.write_bytes(external_state)
with pytest.raises(deploy_module.DeployError, match="compare-and-swap"):
deploy_module.write_state(
repository, project, revision, "failed", "deployment failed",
expected_state_sha256="0" * 64,
)
assert path.read_bytes() == external_state
helper = deploy_module._load_registered_legacy_adoption()
outcome = helper.status(
{"repository": repository, "requestId": request_id},
registry={repository: project}, receipt_key=b"k" * 32, state_root=state_root,
)
assert outcome["status"] == "recovery-required"
assert outcome["reasonCode"] == "controller_state_cas_failed"
def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
"""Build the reviewed DB and Dockerfile transitions without old Git history."""
repository = tmp_path / "reviewed-source.git"
@@ -1601,6 +1677,7 @@ def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
forge_seed_rows = (
b' Policy(id="policy-forge-oidc-provision", action="forge.oidc.provision", target="itworx-forge", voice="blocked", manual="allowed", risk="critical", reason="The fixed Forge OIDC client may be provisioned only by an authenticated manual operator route; voice and generic Authentik mutations remain blocked."),\n',
b' Policy(id="policy-forge-secret-prepare", action="forge.secrets.prepare", target="itworx-forge", voice="blocked", manual="blocked", risk="critical", reason="Preparing the fixed Forge bootstrap secret bundle is default-denied and requires an explicit exact-target manual operator policy; voice remains blocked."),\n',
b' Policy(id="policy-forge-route-stage", action="forge.route.stage", target="itworx-forge", voice="blocked", manual="blocked", risk="critical", reason="Staging the fixed disabled Forge proxy host is default-denied; voice remains blocked."),\n',
)
for row in forge_seed_rows:
assert current_init.count(row) == 1
@@ -1629,7 +1706,15 @@ def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
old_tree = git("rev-parse", f"{old_revision}^{{tree}}")
old_database_tree = git("rev-parse", f"{old_revision}:backend/app/db")
(db / "init_db.py").write_bytes(current_init)
candidate_init = current_init
if candidate_mutation == "previous_reviewed_seed_tree":
route_seed = forge_seed_rows[2]
assert candidate_init.count(route_seed) == 1
candidate_init = candidate_init.replace(route_seed, b"", 1)
elif candidate_mutation == "legacy_database_unchanged":
candidate_init = old_init
(db / "init_db.py").write_bytes(candidate_init)
if candidate_mutation == "extra_database_file":
(db / "unreviewed.py").write_text("UNREVIEWED = True\n", encoding="utf-8")
elif candidate_mutation == "models_content":
@@ -1637,21 +1722,47 @@ def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
elif candidate_mutation == "session_content":
(db / "session.py").write_bytes(normalize(current_db / "session.py") + b"\n# unreviewed\n")
elif candidate_mutation == "init_db_content":
(db / "init_db.py").write_bytes(current_init + b"\n# unreviewed database behavior\n")
(db / "init_db.py").write_bytes(candidate_init + b"\n# unreviewed database behavior\n")
elif candidate_mutation == "forge_policy_changed":
changed_init = current_init.replace(
changed_init = candidate_init.replace(
b'action="forge.oidc.provision"',
b'action="forge.oidc.provision-unreviewed"',
1,
)
assert changed_init != current_init
(db / "init_db.py").write_bytes(changed_init)
elif candidate_mutation in {
"route_stage_manual_allowed", "route_stage_voice_allowed", "route_stage_risk_lowered",
}:
route_seed = forge_seed_rows[2]
replacements = {
"route_stage_manual_allowed": (b'manual="blocked"', b'manual="allowed"'),
"route_stage_voice_allowed": (b'voice="blocked"', b'voice="allowed"'),
"route_stage_risk_lowered": (b'risk="critical"', b'risk="high"'),
}
old_field, new_field = replacements[candidate_mutation]
assert route_seed.count(old_field) == 1
candidate_init = candidate_init.replace(route_seed, route_seed.replace(old_field, new_field, 1), 1)
(db / "init_db.py").write_bytes(candidate_init)
elif candidate_mutation == "unknown_policy_row":
route_seed = forge_seed_rows[2]
unknown_policy = (
b' Policy(id="policy-forge-route-stage-unreviewed", '
b'action="forge.route.stage.unreviewed", target="itworx-forge", '
b'voice="blocked", manual="blocked", risk="critical", '
b'reason="Unreviewed policy row."),\n'
)
assert candidate_init.count(route_seed) == 1
candidate_init = candidate_init.replace(route_seed, route_seed + unknown_policy, 1)
(db / "init_db.py").write_bytes(candidate_init)
elif candidate_mutation == "database_file_mode":
(db / "session.py").chmod(0o755)
elif candidate_mutation == "entrypoint_content":
entrypoint.write_bytes(entrypoint.read_bytes() + b"# unreviewed entrypoint\n")
elif candidate_mutation not in {
None, "dockerfile_one_byte", "dockerfile_legacy_unchanged",
None, "previous_reviewed_seed_tree", "legacy_database_unchanged",
"route_stage_manual_allowed", "route_stage_voice_allowed", "route_stage_risk_lowered",
"unknown_policy_row", "dockerfile_one_byte", "dockerfile_legacy_unchanged",
}:
raise AssertionError("unknown fixture mutation")
@@ -1673,6 +1784,11 @@ def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
(working / "Dockerfile").write_bytes(candidate_dockerfile)
git("add", "--all")
if candidate_mutation == "database_file_mode":
# Windows does not preserve chmod's executable bit on the worktree;
# stage the real Git mode mutation so this remains a meaningful tree
# identity negative fixture on every platform.
git("update-index", "--chmod=+x", "--", "backend/app/db/session.py")
git("commit", "-m", "candidate source")
candidate_revision = git("rev-parse", "HEAD")
git("push", "origin", "HEAD:refs/heads/main")
@@ -1729,7 +1845,7 @@ def test_appops_legacy_source_proof_accepts_the_exact_main_184_dockerfile_update
assert proof == {
"legacyRevision": history["oldRevision"],
"legacyTree": history["oldTree"],
"databaseTree": "c87c9aff5a0f0e55b9209e2f085cf1663876520a",
"databaseTree": "21d29c1799118fdc43946c2b6fd5585ccc2c4db1",
"entrypointBlob": "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8",
"candidateRevision": history["candidateRevision"],
"composeBlob": "243aee2c6924775bed7706373a5777f4e619e053",
@@ -1755,6 +1871,26 @@ def test_appops_legacy_source_proof_keeps_the_old_dockerfile_candidate_valid(
assert proof["dockerfileBlob"] == "73e083dcfef25402665ace5a0bf07a083cf37432"
def test_appops_legacy_source_proof_keeps_the_previous_reviewed_seed_tree_valid(
deploy_module, monkeypatch, tmp_path
):
history = _build_reviewed_appops_seed_history(
tmp_path, candidate_mutation="previous_reviewed_seed_tree"
)
assert history["oldDatabaseTree"] == "d7f8a6188f6c6e0c69bce0e547df31d36ed84a28"
assert history["candidateDatabaseTree"] == "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
proof = deploy_module.verify_appops_legacy_source(
history["repository"],
_reviewed_appops_source_project(deploy_module),
history["candidateRevision"],
)
assert proof["databaseTree"] == "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
def test_appops_legacy_source_proof_rejects_a_one_byte_dockerfile_change(
deploy_module, monkeypatch, tmp_path
):
@@ -1772,12 +1908,59 @@ def test_appops_legacy_source_proof_rejects_a_one_byte_dockerfile_change(
)
def test_appops_schema_compatibility_accepts_the_exact_reviewed_seed_tree_pair(
deploy_module, monkeypatch, tmp_path
def test_appops_legacy_source_bindings_remain_fixed_for_the_new_reviewed_tree(
deploy_module, monkeypatch, tmp_path,
):
history = _build_reviewed_appops_seed_history(tmp_path)
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
mutations = (
("_repository_name", "jens/other-repository"),
("branch", "release"),
("gitea_repository_id", 10),
("source_dir", "/tmp/unreviewed-source"),
("compose_project", "unreviewed-project"),
("compose_files", ["other-compose.yml"]),
("containers", ["OtherContainer"]),
("verify_oci_revision", False),
)
for field, value in mutations:
project = _reviewed_appops_source_project(deploy_module)
project[field] = value
with pytest.raises(deploy_module.DeployError, match="fixed legacy source or resolved Compose inputs"):
deploy_module.verify_appops_legacy_source(
history["repository"], project, history["candidateRevision"],
)
for field, value in (
("APPOPS_DATA_PATH", "/tmp/other-data"),
("APPOPS_PUBLISH_HOST", "127.0.0.1"),
("APPOPS_PROXY_NETWORK", "unreviewed-network"),
):
project = _reviewed_appops_source_project(deploy_module)
project["environment"][field] = value
with pytest.raises(deploy_module.DeployError, match="fixed legacy source or resolved Compose inputs"):
deploy_module.verify_appops_legacy_source(
history["repository"], project, history["candidateRevision"],
)
@pytest.mark.parametrize(
("candidate_mutation", "expected_database_tree"),
[
(None, "21d29c1799118fdc43946c2b6fd5585ccc2c4db1"),
("previous_reviewed_seed_tree", "c87c9aff5a0f0e55b9209e2f085cf1663876520a"),
("legacy_database_unchanged", "d7f8a6188f6c6e0c69bce0e547df31d36ed84a28"),
],
)
def test_appops_schema_compatibility_accepts_only_the_two_reviewed_pairs_or_same_tree(
deploy_module, monkeypatch, tmp_path, candidate_mutation, expected_database_tree,
):
history = _build_reviewed_appops_seed_history(
tmp_path, candidate_mutation=candidate_mutation,
)
assert history["oldDatabaseTree"] == "d7f8a6188f6c6e0c69bce0e547df31d36ed84a28"
assert history["candidateDatabaseTree"] == "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
assert history["candidateDatabaseTree"] == expected_database_tree
assert history["candidateEntrypoint"] == "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8"
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
@@ -1789,7 +1972,7 @@ def test_appops_schema_compatibility_accepts_the_exact_reviewed_seed_tree_pair(
assert proof == {
"legacyRevision": history["oldRevision"],
"legacyTree": history["oldTree"],
"databaseTree": "c87c9aff5a0f0e55b9209e2f085cf1663876520a",
"databaseTree": expected_database_tree,
"entrypointBlob": "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8",
"candidateRevision": history["candidateRevision"],
}
@@ -1803,6 +1986,10 @@ def test_appops_schema_compatibility_accepts_the_exact_reviewed_seed_tree_pair(
"session_content",
"init_db_content",
"forge_policy_changed",
"unknown_policy_row",
"route_stage_manual_allowed",
"route_stage_voice_allowed",
"route_stage_risk_lowered",
"database_file_mode",
"entrypoint_content",
],
@@ -1815,10 +2002,13 @@ def test_appops_seed_tree_exception_rejects_every_unreviewed_tree_or_entrypoint_
)
assert history["oldDatabaseTree"] == "d7f8a6188f6c6e0c69bce0e547df31d36ed84a28"
if mutation == "entrypoint_content":
assert history["candidateDatabaseTree"] == "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
assert history["candidateDatabaseTree"] == "21d29c1799118fdc43946c2b6fd5585ccc2c4db1"
assert history["candidateEntrypoint"] != "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8"
else:
assert history["candidateDatabaseTree"] != "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
assert history["candidateDatabaseTree"] not in {
"c87c9aff5a0f0e55b9209e2f085cf1663876520a",
"21d29c1799118fdc43946c2b6fd5585ccc2c4db1",
}
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
@@ -3126,8 +3316,9 @@ def test_deployment_environment_clears_unverified_build_date(deploy_module, tmp_
assert environment["APPOPS_BUILD_DATE"] == ""
def test_deployment_environment_allows_project_build_date_override(deploy_module, tmp_path):
def test_deployment_environment_allows_unverified_project_build_date_override(deploy_module, tmp_path):
project = _project(tmp_path)
project["verify_oci_revision"] = False
project["environment"] = {"APPOPS_BUILD_DATE": "operator-configured-value"}
repository = "jens/unraid-appops-gateway"
revision = "a" * 40
@@ -7573,6 +7764,8 @@ def test_controller_release_identity_is_content_bound(status_module, tmp_path, m
renderer = importlib.util.module_from_spec(renderer_spec)
renderer_spec.loader.exec_module(renderer)
assert renderer._ARTIFACT_FILES == status_module._CONTROLLER_ARTIFACT_FILES
candidate_helper = "prepare_mcp_hub_candidate.py"
assert candidate_helper in status_module._CONTROLLER_ARTIFACT_FILES
forge_first_install_index = status_module._CONTROLLER_ARTIFACT_FILES.index(
"forge_first_install.py"
)
@@ -7588,6 +7781,9 @@ def test_controller_release_identity_is_content_bound(status_module, tmp_path, m
else status_module.ROOT / filename
)
(artifact_root / filename).write_bytes(source.read_bytes())
assert (artifact_root / candidate_helper).read_bytes() == (
status_module.ROOT / candidate_helper
).read_bytes()
monkeypatch.setattr(status_module, "ROOT", artifact_root)
monkeypatch.setattr(status_module, "CONTROLLER_RELEASE_PATH", artifact_root / "controller-release.json")
digest = status_module._controller_artifact_digest()
@@ -7608,7 +7804,9 @@ def test_controller_release_identity_is_content_bound(status_module, tmp_path, m
monkeypatch.setattr(status_module.sys, "argv", ["status_server.py", "--check-artifact"])
status_module.main()
assert "validated controller artifact 2.0.0" in capsys.readouterr().out
(artifact_root / "deploy.py").write_text("tampered", encoding="utf-8")
(artifact_root / candidate_helper).write_text("tampered", encoding="utf-8")
assert status_module._controller_release() is None
(artifact_root / candidate_helper).unlink()
assert status_module._controller_release() is None
+350 -3
View File
@@ -81,7 +81,21 @@ class _Broker:
def enrollment_capabilities(self, *, secret):
assert secret == "b" * 64
return {"status": "ready", "trustedAppdataRoot": self.appdata_root}
return {
"status": "ready",
"trustedAppdataRoot": self.appdata_root,
"features": {
"deploymentApply": True,
"deploymentVerify": True,
"rollback": True,
"exactTargetPolicy": True,
"hmacV2": True,
"replayProtection": True,
"enrollment": True,
"targetRegistration": True,
"runtimeSecretSealing": True,
},
}
def enrollment_preflight(self, enrollment, *, secret):
assert secret == "b" * 64
@@ -209,6 +223,30 @@ def _plane(portainer=None, broker=None, npm=None, authentik=None):
)
class _CompensatingBroker(_Broker):
def unenroll_target(self, _target, *, secret):
assert secret == "b" * 64
return {"status": "rolled_back"}
class _CompensatingNpm(_Npm):
def remove_managed_proxy_host(self, *, host_id, ownership_id):
return {"success": True, "status": "removed"}
class _CompensatingAuthentik(_Authentik):
def remove_deployment_oidc_application(self, **_kwargs):
return {"success": True, "status": "removed"}
def _compensating_plane(broker=None):
return _plane(
broker=broker or _CompensatingBroker(),
npm=_CompensatingNpm(),
authentik=_CompensatingAuthentik(),
)
def _configure(session):
set_secret(session, "ENROLLMENT_BOOTSTRAP", "b" * 64)
EnrollmentPlane.set_trust_config(session, _trust())
@@ -405,8 +443,9 @@ def test_capabilities_block_when_controller_root_differs_from_operator_trust(tmp
result = _plane(broker=AlternateRootBroker()).capabilities(session)
assert result["targetEnrollment"]["status"] == "unavailable"
assert result["controller"]["status"] == "unavailable"
assert result["controller"]["reason"] == "trusted_appdata_root_mismatch"
assert result["controller"]["status"] == "ready"
assert result["controller"]["reason"] == ""
assert result["enrollmentReadiness"]["reason"] == "trusted_appdata_root_unavailable"
def test_capabilities_treat_roots_as_independent_availability_not_global_selection(tmp_path):
@@ -424,6 +463,17 @@ def test_capabilities_treat_roots_as_independent_availability_not_global_selecti
{"root": managed_root, "status": "ready", "reason": ""},
],
"managedStorage": {"status": "ready", "initializationAvailable": False},
"features": {
"deploymentApply": True,
"deploymentVerify": True,
"rollback": True,
"exactTargetPolicy": True,
"hmacV2": True,
"replayProtection": True,
"enrollment": True,
"targetRegistration": True,
"runtimeSecretSealing": True,
},
}
with _session(tmp_path) as session:
@@ -454,6 +504,123 @@ def test_capabilities_treat_roots_as_independent_availability_not_global_selecti
]
def test_runtime_capabilities_remain_available_when_selected_enrollment_root_is_unavailable(tmp_path):
legacy_root = "/srv/example/appdata"
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
class CoreReadyBroker(_Broker):
def enrollment_capabilities(self, *, secret):
assert secret == "b" * 64
return {
"status": "ready",
"reason": "",
"availableTrustedAppdataRoots": [
{"root": legacy_root, "status": "unavailable", "reason": "legacy_appdata_root_unavailable"},
{"root": managed_root, "status": "unavailable", "reason": "managed_appdata_volume_missing"},
],
"managedStorage": {
"status": "unavailable",
"reason": "managed_appdata_volume_missing",
"initializationAvailable": False,
},
"features": {
"deploymentApply": True,
"deploymentVerify": True,
"rollback": True,
"exactTargetPolicy": True,
"hmacV2": True,
"replayProtection": True,
"enrollment": False,
"targetRegistration": False,
"runtimeSecretSealing": False,
},
"enrollmentReadiness": {
"status": "unavailable",
"reason": "legacy_appdata_root_unavailable",
},
"registry": {"status": "ready", "registeredProjectCount": 48},
}
with _session(tmp_path) as session:
_configure(session)
result = _plane(broker=CoreReadyBroker()).capabilities(session)
assert result["controller"] == {"status": "ready", "reason": ""}
assert result["targetEnrollment"]["status"] == "unavailable"
assert result["runtimeDeployment"]["status"] == "ready"
assert result["rollback"]["status"] == "ready"
assert result["verification"]["status"] == "ready"
assert result["enrollmentReadiness"] == {
"status": "unavailable",
"reason": "legacy_appdata_root_unavailable",
}
assert result["availableTrustedAppdataRoots"] == [
{"root": legacy_root, "status": "unavailable", "reason": "legacy_appdata_root_unavailable"},
{"root": managed_root, "status": "unavailable", "reason": "managed_appdata_volume_missing"},
]
def test_runtime_capabilities_fail_closed_when_controller_feature_attestation_is_missing(tmp_path):
class MissingFeaturesBroker(_Broker):
def enrollment_capabilities(self, *, secret):
return {"status": "ready", "trustedAppdataRoot": self.appdata_root}
with _session(tmp_path) as session:
_configure(session)
result = _plane(broker=MissingFeaturesBroker()).capabilities(session)
assert result["controller"]["status"] == "ready"
assert result["runtimeDeployment"]["status"] == "unavailable"
assert result["rollback"]["status"] == "unavailable"
assert result["verification"]["status"] == "unavailable"
assert result["targetEnrollment"]["status"] == "unavailable"
def test_managed_storage_capability_preserves_only_bounded_controller_reason(tmp_path):
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
class ManagedStorageUnavailableBroker(_Broker):
def enrollment_capabilities(self, *, secret):
return {
"status": "ready",
"availableTrustedAppdataRoots": [
{"root": "/srv/example/appdata", "status": "ready"},
{"root": managed_root, "status": "unavailable", "reason": "managed_appdata_volume_missing"},
],
"managedStorage": {
"status": "unavailable",
"reason": "managed_appdata_volume_missing",
"initializationAvailable": False,
},
"features": {
"deploymentApply": True,
"deploymentVerify": True,
"rollback": True,
"exactTargetPolicy": True,
"hmacV2": True,
"replayProtection": True,
"enrollment": False,
"targetRegistration": False,
"runtimeSecretSealing": False,
},
}
with _session(tmp_path) as session:
_configure(session)
EnrollmentPlane.set_trust_config(
session,
_trust().model_copy(update={
"trusted_appdata_root": managed_root,
"managed_appdata_backup_acknowledged": True,
}),
)
result = _plane(broker=ManagedStorageUnavailableBroker()).capabilities(session)
assert result["managedStorage"]["status"] == "unavailable"
assert result["managedStorage"]["reason"] == "managed_appdata_volume_missing"
assert result["enrollmentReadiness"]["reason"] == "managed_appdata_volume_missing"
def test_capabilities_fail_closed_and_hide_unrecognized_controller_roots(tmp_path):
class MalformedRootsBroker(_Broker):
def enrollment_capabilities(self, *, secret):
@@ -543,6 +710,186 @@ def test_enrollment_creates_write_only_credentials_exact_policies_and_is_idempot
assert len(session.exec(select(EnrollmentJob)).all()) == 1
def test_enrollment_policy_id_collision_blocks_before_mutation_and_leaves_no_partial_policy(tmp_path):
class RecordingBroker(_CompensatingBroker):
enroll_calls = 0
def enroll_target(self, *args, **kwargs):
self.enroll_calls += 1
return super().enroll_target(*args, **kwargs)
with _session(tmp_path) as session:
_configure(session)
collision = Policy(
id="enrollment-wsbc-portal-deployment-rollback",
action="deployment.rollback",
target="wsbc-portal",
voice="blocked",
manual="allowed",
risk="high",
reason="pre-existing exact-target policy",
provenance_json={"owner": "pre-existing"},
)
session.add(collision)
session.commit()
broker = RecordingBroker()
result = _compensating_plane(broker).enroll(
session, _plan(), idempotency_key="enroll-policy-collision-0001"
)
# A pre-existing rollback policy must never leave behind the apply half
# of the enrollment-generated pair, even after failure compensation.
assert session.get(Policy, "enrollment-wsbc-portal-deployment-apply") is None
assert session.get(Policy, collision.id).provenance_json == {"owner": "pre-existing"}
assert result["enrollment"]["status"] == "preflight_failed"
assert result["job"]["status"] == "preflight_failed"
assert "exact_policy_id_collision" in result["job"]["preflight"]["blockers"]
assert broker.enroll_calls == 0
assert session.exec(select(Policy).where(Policy.target == "wsbc-portal")).all() == [collision]
def test_late_policy_id_collision_rolls_back_the_whole_pair(tmp_path, monkeypatch):
class FailedControllerCompensationBroker(_CompensatingBroker):
def unenroll_target(self, _target, *, secret):
assert secret == "b" * 64
return {"status": "rollback_failed"}
collision = Policy(
id="enrollment-wsbc-portal-deployment-rollback",
action="deployment.rollback",
target="wsbc-portal",
voice="blocked",
manual="allowed",
risk="high",
reason="concurrent exact-target policy",
provenance_json={"owner": "concurrent"},
)
checkpoint = EnrollmentPlane._checkpoint
def add_collision_after_preflight(session, enrollment, job, *, stage, evidence):
checkpoint(session, enrollment, job, stage=stage, evidence=evidence)
if stage == "installing_policy":
session.add(collision)
session.commit()
monkeypatch.setattr(
EnrollmentPlane, "_checkpoint", staticmethod(add_collision_after_preflight)
)
with _session(tmp_path) as session:
_configure(session)
result = _compensating_plane(FailedControllerCompensationBroker()).enroll(
session, _plan(), idempotency_key="fixture-" + "x" * 32
)
assert result["job"]["preflight"]["status"] == "ready"
assert result["enrollment"]["status"] == "compensation_required"
assert result["job"]["status"] == "compensation_required"
assert result["job"]["evidence"]["compensation"] == {
"completed": False,
"outcomes": {
"oidc": "removed",
"proxy": "removed",
"controller": "rollback_failed",
},
}
assert session.get(Policy, "enrollment-wsbc-portal-deployment-apply") is None
assert session.get(Policy, collision.id).provenance_json == {"owner": "concurrent"}
assert session.exec(select(Policy).where(Policy.target == "wsbc-portal")).all() == [collision]
def test_final_ready_commit_failure_rolls_back_the_policy_pair(tmp_path):
engine = create_engine(f"sqlite:///{tmp_path / 'enrollment.db'}")
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
_configure(session)
plane = _plane()
commit = session.commit
def fail_after_policy_pair_is_staged():
ready = session.exec(
select(Enrollment).where(Enrollment.status == "ready")
).first()
policies = session.exec(
select(Policy).where(Policy.target == "wsbc-portal")
).all()
if ready is not None and {policy.action for policy in policies} == {
"deployment.apply", "deployment.rollback",
}:
raise RuntimeError("simulated final enrollment commit failure")
commit()
session.commit = fail_after_policy_pair_is_staged
with pytest.raises(RuntimeError, match="final enrollment commit failure"):
plane.enroll(session, _plan(), idempotency_key="enroll-final-commit-failure-0001")
# A request-scoped MCP session is closed when the operation raises; prove
# the pending pair was not committed by reading through a fresh session.
with Session(engine) as verification:
enrollment = verification.exec(select(Enrollment)).one()
job = verification.exec(select(EnrollmentJob)).one()
assert enrollment.status == "provisioning"
assert job.stage == "installing_policy"
assert verification.exec(
select(Policy).where(Policy.target == "wsbc-portal")
).all() == []
def test_concurrent_policy_id_collision_during_flush_rolls_back_pending_pair(tmp_path):
collision = Policy(
id="enrollment-wsbc-portal-deployment-rollback",
action="deployment.rollback",
target="wsbc-portal",
voice="blocked",
manual="allowed",
risk="high",
reason="concurrent exact-target policy",
provenance_json={"owner": "concurrent"},
)
with _session(tmp_path) as session:
_configure(session)
flush = session.flush
inserted = False
def flush_with_concurrent_collision(*args, **kwargs):
nonlocal inserted
staged_ids = {
policy.id for policy in session.new if isinstance(policy, Policy)
}
if not inserted and staged_ids == {
"enrollment-wsbc-portal-deployment-apply",
"enrollment-wsbc-portal-deployment-rollback",
}:
inserted = True
with Session(session.get_bind()) as concurrent_session:
concurrent_session.add(collision)
concurrent_session.commit()
return flush(*args, **kwargs)
session.flush = flush_with_concurrent_collision
result = _compensating_plane().enroll(
session, _plan(), idempotency_key="enroll-policy-concurrent-0001"
)
assert inserted is True
assert result["job"]["preflight"]["status"] == "ready"
assert result["enrollment"]["status"] == "failed"
assert session.get(Policy, "enrollment-wsbc-portal-deployment-apply") is None
concurrent_policy = session.get(
Policy, "enrollment-wsbc-portal-deployment-rollback"
)
assert concurrent_policy is not None
assert concurrent_policy.provenance_json == {"owner": "concurrent"}
policies = session.exec(select(Policy).where(Policy.target == "wsbc-portal")).all()
assert [(policy.id, policy.action, policy.provenance_json) for policy in policies] == [
(
"enrollment-wsbc-portal-deployment-rollback",
"deployment.rollback",
{"owner": "concurrent"},
)
]
def test_interrupted_external_enrollment_requires_explicit_recovery(tmp_path):
class InterruptedBroker(_Broker):
def enroll_target(self, _enrollment, *, secret, idempotency_key):
@@ -0,0 +1,305 @@
from __future__ import annotations
import ast
import json
import os
import stat
import subprocess
import sys
import time
from types import SimpleNamespace
import pytest
from backend.app.services.deployment_target_registry import (
DeploymentTargetRegistry,
_REGISTER_SCRIPT,
)
from test_deployment_target_registry import FakePortainer
REPOSITORY = "jens/quiz-monitor"
REVISION = "a" * 40
def requested(verify=True):
return DeploymentTargetRegistry._project(
REPOSITORY, branch="main", compose_file="docker-compose.yml",
compose_project="quiz-monitor", containers=["quiz-monitor-quiz-server-1"],
health_timeout=180, verify_oci_revision=verify,
)
def live():
return {
"branch": "main", "slug": "quiz-monitor",
"source_dir": "/srv/example/appdata/quiz-monitor/source",
"compose_files": ["docker-compose.yml"], "compose_project": "quiz-monitor",
"containers": ["quiz-monitor-quiz-server-1"], "legacy_containers": ["QuizMonitor"],
"gitea_repository_id": 6, "gitea_webhook_secret_file": "jens__quiz-monitor",
"validation": {"validation_gate_version": 1, "required": True,
"contexts": ["Managed validation / full (push)"],
"timeout_seconds": 1800, "poll_interval_seconds": 5,
"max_poll_interval_seconds": 30},
"environment": {"TEST_VALUE": "private-value-$preserve"},
"env_source": "/srv/example/appdata/quiz-monitor/runtime.env",
}
def descriptor():
return {"apiVersion": "itworx.deployment-target/v1", "applicationId": "quiz-monitor",
"primaryService": "quiz-server", "composeFile": "docker-compose.yml",
"composeProject": "quiz-monitor", "verifyOciRevision": True}
@pytest.fixture
def transaction(tmp_path, monkeypatch):
pytest.importorskip("fcntl")
if os.geteuid() != 0:
pytest.skip("fixed root-owned registry transaction requires Linux root")
registry = tmp_path / "volume" / "config" / "projects.json"
registry.parent.mkdir(parents=True, mode=0o700)
registry.parent.parent.chmod(0o700)
state = tmp_path / "state"
state.mkdir(mode=0o700)
bare = tmp_path / "repositories" / "jens" / "quiz-monitor.git"
bare.mkdir(parents=True)
script = _REGISTER_SCRIPT.replace("/controller-hub-releases/controller-config/projects.json", str(registry))
script = script.replace("/controller-config/projects.json", str(registry))
script = script.replace("/mnt/cache/appdata/gitea-deploy-runners/deploy.lock", str(tmp_path / "deploy.lock"))
script = script.replace("/mnt/cache/appdata/gitea-deploy-runners/state/registry-backups", str(state / "registry-backups"))
script = script.replace("/gitea-repositories", str(tmp_path / "repositories"))
def run(current, desired=None, *, source=None, protected=True, head=REVISION,
protected_heads=None, mutate_during_gate=False, expire_during_gate=False):
desired = desired or requested()
projects = {REPOSITORY: current, "jens/unrelated": {"unchanged": "private-other"}} if current is not None else {}
original = json.dumps(projects).encode()
registry.write_bytes(original)
registry.chmod(0o600)
calls = []
elapsed = [0]
if expire_during_gate:
monkeypatch.setattr("time.monotonic", lambda: elapsed[0])
def git(command, **_kwargs):
assert command[:5] == ["git", "--no-replace-objects", "-c",
f"safe.directory={bare}", f"--git-dir={bare}"]
args = command[5:]
if args[0] == "rev-parse":
text = head if args[1].startswith("refs/heads/") else REVISION
return SimpleNamespace(returncode=0, stdout=text + "\n")
if args[:2] == ["show", f"{REVISION}:.itworx/deployment-target.json"]:
text = source if isinstance(source, str) else json.dumps(source if source is not None else descriptor())
return SimpleNamespace(returncode=0, stdout=text)
if args[0] in {"merge-base", "cat-file"}:
return SimpleNamespace(returncode=0, stdout="")
raise AssertionError(args)
def read(path, *, query):
assert path == f"/repos/{REPOSITORY}/branches/main" and query == {}
calls.append(path)
if expire_during_gate:
elapsed[0] = 21
if mutate_during_gate and (mutate_during_gate is True or len(calls) == mutate_during_gate):
registry.write_bytes(original + b" ")
current_head = protected_heads[min(len(calls) - 1, len(protected_heads) - 1)] if protected_heads else head
return {"protected": protected, "commit": {"id": current_head}}
monkeypatch.setattr("subprocess.run", git)
monkeypatch.setitem(sys.modules, "status_server", SimpleNamespace(_registered_legacy_api_reader=lambda repo: read))
monkeypatch.setattr(sys, "argv", ["registration", REPOSITORY, REVISION, "main", json.dumps(desired)])
code = 0
try:
exec(compile(script, "fixed-registration-script", "exec"), {})
except SystemExit as error:
code = error.code
return code, json.loads(registry.read_bytes()), original, registry.read_bytes(), calls, state
return run
def test_exact_protected_descriptor_strengthens_verification_and_preserves_all_existing_metadata(transaction, capsys):
current = live()
code, result, _old, _new, calls, state = transaction(current)
assert code == 0
assert result[REPOSITORY] == {**current, "verify_oci_revision": True}
assert result["jens/unrelated"] == {"unchanged": "private-other"}
assert len(calls) == 2 # Fresh protected-main proof at preflight and atomic install.
assert len(list((state / "registry-backups").iterdir())) == 1
assert "private" not in capsys.readouterr().out
def test_known_absent_defaults_and_legacy_fields_are_an_idempotent_noop(transaction):
current = live()
code, result, old, new, calls, state = transaction(current, requested(False))
assert code == 0 and old == new and result[REPOSITORY] == current
assert not calls and not (state / "registry-backups").exists()
@pytest.mark.parametrize("change", [
{"branch": "other"}, {"slug": "other"}, {"compose_project": "other"},
{"containers": ["other-container"]}, {"compose_files": ["other.yml"]},
{"source_dir": "/srv/example/appdata/other/source"}, {"source_dir": "/srv/example/appdata/quiz-monitor/source/../other"},
{"exact_source": "false"}, {"verify_oci_revision": "false"}, {"health_timeout": False},
{"health_timeout": 300}, {"deploy_script": "scripts/privileged.sh"},
{"legacy_containers": ["../escape"]}, {"legacy_containers": ["QuizMonitor", "QuizMonitor"]},
{"legacy_containers": ["quiz-monitor-quiz-server-1"]},
{"validation": {"required": False}},
])
def test_conflicting_identity_paths_and_unsafe_metadata_never_write(transaction, change):
code, _result, old, new, calls, state = transaction({**live(), **change})
assert code == 23 and old == new
assert not calls and not (state / "registry-backups").exists()
@pytest.mark.parametrize("source", [
{}, {**descriptor(), "verifyOciRevision": False},
{**descriptor(), "verifyOciRevision": "true"},
{**descriptor(), "composeProject": "other"},
{**descriptor(), "composeFile": "other.yml"},
{**descriptor(), "healthTimeout": True},
{**descriptor(), "command": "arbitrary"},
'{"verifyOciRevision":true,"verifyOciRevision":false}',
])
def test_strengthening_requires_exact_boolean_source_contract(transaction, source):
code, _result, old, new, _calls, state = transaction(live(), source=source)
assert code == 39 and old == new and not (state / "registry-backups").exists()
def test_strengthening_rejects_noncurrent_or_unprotected_main(transaction):
for options in ({"head": "b" * 40}, {"protected": False}):
code, _result, old, new, _calls, state = transaction(live(), **options)
assert code == 39 and old == new and not (state / "registry-backups").exists()
def test_strengthening_cas_rejects_head_change_after_preflight(transaction):
code, result, old, new, calls, _state = transaction(live(), protected_heads=[REVISION, "b" * 40])
assert code == 39 and old == new and len(calls) == 2
assert "verify_oci_revision" not in result[REPOSITORY]
def test_delayed_proof_cannot_commit_after_the_registration_polling_window(transaction):
code, _result, old, new, calls, state = transaction(live(), expire_during_gate=True)
assert code == 39 and old == new and len(calls) == 1
assert not (state / "registry-backups").exists()
@pytest.mark.parametrize("gate", [1, 2])
def test_registry_byte_drift_prevents_atomic_install(transaction, gate):
code, result, _old, _new, _calls, _state = transaction(live(), mutate_during_gate=gate)
assert code == 34 and "verify_oci_revision" not in result[REPOSITORY]
def test_verification_cannot_be_weakened(transaction):
code, _result, old, new, _calls, state = transaction({**live(), "verify_oci_revision": True}, requested(False))
assert code == 23 and old == new and not (state / "registry-backups").exists()
def test_new_registration_keeps_existing_behavior(transaction):
code, result, _old, _new, calls, _state = transaction(None)
assert code == 0 and result[REPOSITORY] == requested() and not calls
def test_registration_api_rejects_string_verification_before_portainer_access():
fake = FakePortainer()
result = DeploymentTargetRegistry(fake).register_compose_target(REPOSITORY, REVISION, verify_oci_revision="false")
assert result["status"] == "invalid" and not fake.calls
@pytest.fixture
def original_git_reader(tmp_path, monkeypatch, request):
if os.name != "posix" or os.geteuid() != 0:
pytest.skip("controller Git mount ownership requires Linux root")
work = tmp_path / "work"
work.mkdir()
repositories = tmp_path / "repositories"
bare = repositories / "jens" / "quiz-monitor.git"
bare.parent.mkdir(parents=True)
monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
monkeypatch.setenv("GIT_CONFIG_GLOBAL", "/dev/null")
monkeypatch.setenv("GIT_CONFIG_COUNT", "0")
def git(*args):
return subprocess.check_output(["git", "-C", str(work), *args], text=True).strip()
git("init", "-b", "main")
git("config", "user.name", "Controller fixture")
git("config", "user.email", "controller-fixture@example.invalid")
(work / ".itworx").mkdir()
source = descriptor()
source["verifyOciRevision"] = getattr(request, "param", False)
(work / ".itworx/deployment-target.json").write_text(json.dumps(source))
(work / "docker-compose.yml").write_text("services: {}\n")
git("add", ".")
git("commit", "-m", "original protected source")
original = git("rev-parse", "HEAD")
replacement_source = {**source, "verifyOciRevision": not source["verifyOciRevision"]}
(work / ".itworx/deployment-target.json").write_text(json.dumps(replacement_source))
git("commit", "-am", "replacement object")
replacement = git("rev-parse", "HEAD")
git("reset", "--hard", original)
subprocess.run(["git", "clone", "--bare", str(work), str(bare)], check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.run(["git", f"--git-dir={bare}", "replace", original, replacement],
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
functions = [node for node in ast.parse(_REGISTER_SCRIPT).body
if isinstance(node, ast.FunctionDef)
and node.name in {"git_run", "_source_verified_strengthening"}]
namespace = {"bare_repo": bare, "subprocess": subprocess, "stat": stat,
"time": time, "json": json, "re": __import__("re"), "sys": sys,
"branch": "main", "revision": original, "repository": REPOSITORY,
"compose_file": "docker-compose.yml", "requested_project": requested(),
"strengthening_deadline": time.monotonic() + 20}
exec(compile(ast.Module(body=functions, type_ignores=[]), "fixed-git-reader", "exec"), namespace)
monkeypatch.setitem(sys.modules, "status_server", SimpleNamespace(
_registered_legacy_api_reader=lambda _repo: lambda _path, **_kwargs: {
"protected": True, "commit": {"id": original},
},
))
return bare, original, namespace
def test_original_descriptor_cannot_be_substituted_by_git_replacement(original_git_reader):
bare, original, namespace = original_git_reader
# A real replacement keeps the branch SHA but swaps the source descriptor.
replaced = subprocess.check_output(
["git", f"--git-dir={bare}", "show", f"{original}:.itworx/deployment-target.json"],
text=True,
)
assert json.loads(replaced)["verifyOciRevision"] is True
result = namespace["git_run"]("show", f"{original}:.itworx/deployment-target.json")
assert result.returncode == 0 and json.loads(result.stdout)["verifyOciRevision"] is False
assert namespace["_source_verified_strengthening"]() is False
@pytest.mark.parametrize("owner", [0, 1000])
def test_read_only_gitea_repository_owner_is_supported(original_git_reader, owner):
bare, original, namespace = original_git_reader
for directory in (bare.parent.parent, bare.parent, bare):
directory.chmod(0o755)
os.chown(directory, owner, owner)
result = namespace["git_run"]("rev-parse", "refs/heads/main")
assert result.returncode == 0 and result.stdout.strip() == original
@pytest.mark.parametrize("depth", [0, 1, 2])
@pytest.mark.parametrize("unsafe", ["owner", "writable", "symlink"])
def test_repository_local_trust_rejects_unsafe_path(original_git_reader, depth, unsafe):
bare, _original, namespace = original_git_reader
directory = (bare.parent.parent, bare.parent, bare)[depth]
if unsafe == "owner":
os.chown(directory, 1234, 1234)
elif unsafe == "writable":
directory.chmod(0o775)
else:
renamed = directory.with_name(directory.name + "-original")
directory.rename(renamed)
directory.symlink_to(renamed, target_is_directory=True)
with pytest.raises(ValueError, match="unsafe mounted repository"):
namespace["git_run"]("rev-parse", "refs/heads/main")
@pytest.mark.parametrize("original_git_reader", [True], indirect=True)
def test_replacement_cannot_hide_valid_original_protected_descriptor(original_git_reader):
_bare, _original, namespace = original_git_reader
assert namespace["_source_verified_strengthening"]() is True
+761
View File
@@ -0,0 +1,761 @@
from __future__ import annotations
import pytest
from sqlmodel import SQLModel, Session, create_engine
from backend.app.core.config import settings
from backend.app.db.models import Policy
from backend.app.services.action_registry import ACTION_RISK, ACTIONS, action_default_target
from backend.app.services.policy_engine import evaluate_policy
_ACTION = "forge.route.stage"
_TARGET = "itworx-forge"
_NETWORK = "itworx-mcp-hub-npm"
def _session(tmp_path):
engine = create_engine(f"sqlite:///{tmp_path / 'forge-route-stage.db'}")
SQLModel.metadata.create_all(engine)
return Session(engine)
def _policy(target: str = _TARGET, *, manual: str = "allowed") -> Policy:
return Policy(
id=f"route-{target}",
action=_ACTION,
target=target,
manual=manual,
voice="blocked",
risk="critical",
reason="test policy",
)
def test_route_stage_policy_requires_one_exact_manual_critical_grant(tmp_path):
with _session(tmp_path) as session:
session.add(_policy())
session.commit()
decision = evaluate_policy(session, _ACTION, _TARGET, "manual")
assert _ACTION in ACTIONS
assert ACTION_RISK[_ACTION] == "critical"
assert action_default_target(_ACTION) == _TARGET
assert decision == {
"decision": "allowed",
"reason": "Forge route staging requires one exact-target manual policy; voice is always blocked.",
"risk": "critical",
"policy_id": "route-itworx-forge",
"scope_decision": "allowed",
}
def test_route_stage_policy_rejects_wildcard_even_when_manual_is_allowed(tmp_path):
with _session(tmp_path) as session:
session.add(_policy("*"))
session.commit()
manual = evaluate_policy(session, _ACTION, _TARGET, "manual")
voice = evaluate_policy(session, _ACTION, _TARGET, "voice")
assert manual["decision"] == "blocked"
assert manual["policy_id"] is None
assert voice["decision"] == "blocked"
def test_route_stage_seed_is_default_denied_and_duplicate_exact_rows_do_not_grant(tmp_path):
from backend.app.db.init_db import _seed_policies
seeded = [policy for policy in _seed_policies() if policy.action == _ACTION]
assert len(seeded) == 1
assert seeded[0].target == _TARGET
assert seeded[0].manual == "blocked"
assert seeded[0].voice == "blocked"
assert seeded[0].risk == "critical"
with _session(tmp_path) as session:
first = _policy()
second = _policy()
second.id = "route-duplicate"
session.add(first)
session.add(second)
session.commit()
decision = evaluate_policy(session, _ACTION, _TARGET, "manual")
assert decision["decision"] == "blocked"
assert decision["policy_id"] is None
class _DockerResponse:
def __init__(self, payload, status_code=200):
self._payload = payload
self.status_code = status_code
def json(self):
return self._payload
def _docker_container(name, *, container_id, networks, state="running", labels=None, image="image"):
return {
"Id": container_id,
"Names": [f"/{name}"],
"Image": image,
"State": state,
"Labels": labels or {},
"NetworkSettings": {
"Networks": networks if isinstance(networks, dict) else {network: {} for network in networks}
},
}
def _forge_network_inventory():
return [
_docker_container(
"NginxProxyManager",
container_id="a" * 64,
networks={_NETWORK: {"NetworkID": "1" * 64, "EndpointID": "2" * 64}},
image="jlesage/nginx-proxy-manager:2.15.1",
),
_docker_container(
"itworx-forge-itworx-forge-1",
container_id="b" * 64,
networks={
"forge_app": {"NetworkID": "3" * 64, "EndpointID": "4" * 64},
_NETWORK: {"NetworkID": "1" * 64, "EndpointID": "5" * 64},
},
labels={
"com.docker.compose.project": "itworx-forge",
"com.docker.compose.service": "itworx-forge",
},
image="gitea.example.com/owner/itworx-forge-frontdoor@sha256:" + "c" * 64,
),
]
def _portainer(monkeypatch, payload, *, status_code=200):
from backend.app.connectors import portainer as portainer_module
from backend.app.connectors.portainer import PortainerConnector
apis = dict(settings.apis)
apis.update(
{
"PORTAINER_URL": "https://portainer.example",
"PORTAINER_API_KEY": "test-api-key-not-a-real-secret",
"PORTAINER_ENDPOINT_ID": "1",
"PORTAINER_VERIFY_TLS": True,
}
)
monkeypatch.setattr(settings, "apis", apis)
class _Client:
def __init__(self, **_kwargs):
pass
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def get(self, *_args, **_kwargs):
return _DockerResponse(payload, status_code=status_code)
monkeypatch.setattr(portainer_module.httpx, "Client", _Client)
return PortainerConnector()
def test_portainer_route_network_proof_requires_unique_running_fixed_containers(monkeypatch):
connector = _portainer(monkeypatch, _forge_network_inventory())
result = connector.forge_route_network_readiness()
assert result == {
"status": "ready",
"network": _NETWORK,
"npmAttached": True,
"frontdoorAttached": True,
}
def test_portainer_route_network_proof_fails_closed_on_missing_membership(monkeypatch):
payload = _forge_network_inventory()
payload[0]["NetworkSettings"]["Networks"] = {"bridge": {}}
connector = _portainer(monkeypatch, payload)
result = connector.forge_route_network_readiness()
assert result["status"] == "not_ready"
assert result["npmAttached"] is False
assert result["frontdoorAttached"] is True
assert "a" * 64 not in repr(result)
def test_portainer_route_network_proof_rejects_duplicate_or_unavailable_inventory(monkeypatch):
duplicate = _forge_network_inventory() + [_forge_network_inventory()[0]]
assert _portainer(monkeypatch, duplicate).forge_route_network_readiness()["status"] == "unknown"
connector = _portainer(monkeypatch, {"not": "a list"})
assert connector.forge_route_network_readiness()["status"] == "unknown"
def test_portainer_route_network_proof_requires_distinct_docker_containers(monkeypatch):
same_container = _docker_container(
"NginxProxyManager",
container_id="a" * 64,
networks={_NETWORK: {"NetworkID": "1" * 64, "EndpointID": "2" * 64}},
labels={
"com.docker.compose.project": "itworx-forge",
"com.docker.compose.service": "itworx-forge",
},
image="jlesage/nginx-proxy-manager:2.15.1",
)
result = _portainer(monkeypatch, [same_container]).forge_route_network_readiness()
assert result["status"] == "unknown"
def test_portainer_route_network_proof_requires_same_network_identity(monkeypatch):
inventory = _forge_network_inventory()
inventory[1]["NetworkSettings"]["Networks"][_NETWORK]["NetworkID"] = "9" * 64
result = _portainer(monkeypatch, inventory).forge_route_network_readiness()
assert result["status"] == "unknown"
def test_portainer_route_network_proof_requires_https_verified_tls_and_200(monkeypatch):
from backend.app.connectors.portainer import PortainerConnector
apis = dict(settings.apis)
apis.update(
{
"PORTAINER_URL": "http://portainer.example",
"PORTAINER_API_KEY": "test-api-key-not-a-real-secret",
"PORTAINER_ENDPOINT_ID": "1",
"PORTAINER_VERIFY_TLS": True,
}
)
monkeypatch.setattr(settings, "apis", apis)
calls = []
monkeypatch.setattr(
PortainerConnector,
"_request",
lambda *args, **kwargs: calls.append((args, kwargs)) or _DockerResponse(_forge_network_inventory()),
)
assert PortainerConnector().forge_route_network_readiness()["status"] == "unknown"
assert calls == []
assert _portainer(monkeypatch, _forge_network_inventory(), status_code=302).forge_route_network_readiness()["status"] == "unknown"
malformed = dict(settings.apis)
malformed.update({"PORTAINER_URL": "https://[bad", "PORTAINER_VERIFY_TLS": True})
monkeypatch.setattr(settings, "apis", malformed)
assert PortainerConnector().forge_route_network_readiness()["status"] == "unknown"
@pytest.mark.parametrize("network_entry", [None, {}, {"NetworkID": "", "EndpointID": ""}])
def test_portainer_route_network_proof_requires_valid_network_membership_identity(monkeypatch, network_entry):
inventory = _forge_network_inventory()
inventory[0]["NetworkSettings"]["Networks"][_NETWORK] = network_entry
result = _portainer(monkeypatch, inventory).forge_route_network_readiness()
assert result["status"] == "unknown"
assert result["npmAttached"] is None
def test_portainer_route_uses_one_verified_tls_config_snapshot_without_generic_reread(monkeypatch):
from backend.app.connectors import portainer as portainer_module
from backend.app.connectors.portainer import PortainerConnector
apis = dict(settings.apis)
apis.update(
{
"PORTAINER_URL": "https://portainer.example",
"PORTAINER_API_KEY": "snapshot-api-key",
"PORTAINER_ENDPOINT_ID": "7",
"PORTAINER_VERIFY_TLS": True,
}
)
monkeypatch.setattr(settings, "apis", apis)
calls = []
class _PinnedClient:
def __init__(self, *, timeout, verify, follow_redirects):
calls.append(("client", timeout, verify, follow_redirects))
changed = dict(settings.apis)
changed["PORTAINER_URL"] = "http://attacker.example"
changed["PORTAINER_API_KEY"] = "changed-key"
changed["PORTAINER_VERIFY_TLS"] = False
monkeypatch.setattr(settings, "apis", changed)
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def get(self, url, *, headers, follow_redirects):
calls.append(("get", url, headers, follow_redirects))
return _DockerResponse(_forge_network_inventory(), status_code=200)
monkeypatch.setattr(portainer_module.httpx, "Client", _PinnedClient)
monkeypatch.setattr(
PortainerConnector,
"_request",
lambda *_args, **_kwargs: pytest.fail("route proof must use its captured verified HTTPS config"),
)
result = PortainerConnector().forge_route_network_readiness()
assert result["status"] == "ready"
assert calls[0] == ("client", 8.0, True, False)
assert calls[1][0] == "get"
assert calls[1][1] == "https://portainer.example/api/endpoints/7/docker/containers/json?all=1"
assert calls[1][2]["X-API-Key"] == "snapshot-api-key"
assert calls[1][3] is False
def test_npm_forge_route_inventory_requires_bounded_bare_array():
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
parse = NginxProxyManagerConnector._parse_forge_route_hosts
with pytest.raises(RuntimeError, match="npm_forge_route_inventory_invalid"):
parse({"data": [{"domain_names": ["forge.example.com"]}]})
with pytest.raises(RuntimeError, match="npm_forge_route_inventory_invalid"):
parse([{"domain_names": ["forge.example.com"]}, "malformed row"])
with pytest.raises(RuntimeError, match="npm_forge_route_inventory_invalid"):
parse([{"id": index} for index in range(1001)])
assert parse([{"domain_names": ["forge.example.com"]}]) == [
{"domain_names": ["forge.example.com"]}
]
def test_npm_route_session_pins_endpoint_and_verified_tls_across_inventory_and_post(monkeypatch):
from backend.app.connectors import nginx_proxy_manager as npm_module
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
apis = dict(settings.apis)
apis.update(
{
"NPM_URL": "https://npm.example",
"NPM_IDENTITY": "operator@example.test",
"NPM_SECRET": "route-session-secret",
"NPM_VERIFY_TLS": True,
}
)
monkeypatch.setattr(settings, "apis", apis)
calls = []
api_calls = []
permissions = {
"id": 7,
"is_disabled": False,
"permissions": {"visibility": "all", "proxy_hosts": "manage"},
}
class _Response:
def __init__(self, payload, status_code=200):
self._payload = payload
self.status_code = status_code
def json(self):
return self._payload
class _Client:
def __init__(self, *, timeout, verify, follow_redirects):
calls.append(("client", timeout, verify, follow_redirects))
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def post(self, url, *, json, headers, follow_redirects):
calls.append(("login", url, json, headers, follow_redirects))
return _Response({"token": "opaque-test-token"})
def request(self, method, url, *, headers, follow_redirects, **kwargs):
calls.append(("request", method, url, headers, follow_redirects, kwargs))
api_calls.append((method, url, kwargs))
if url.endswith("/users/me"):
return _Response(permissions)
if url.endswith("/nginx/proxy-hosts") and method == "GET":
return _Response([])
if url.endswith("/nginx/proxy-hosts") and method == "POST":
return _Response({"id": 31, **kwargs["json"]}, status_code=201)
pytest.fail("route session used an unsupported NPM endpoint")
monkeypatch.setattr(npm_module.httpx, "Client", _Client)
connector = NginxProxyManagerConnector()
route_session = connector.forge_route_session()
assert route_session is not None
assert route_session.verified_tls is True
assert route_session.list_forge_route_hosts() == []
created = route_session.create_forge_disabled_proxy_host("froute-" + "e" * 32)
assert isinstance(created, dict)
assert [call[:2] for call in api_calls] == [
("GET", "https://npm.example/api/users/me"),
("GET", "https://npm.example/api/nginx/proxy-hosts"),
("GET", "https://npm.example/api/users/me"),
("POST", "https://npm.example/api/nginx/proxy-hosts"),
]
assert api_calls[0][2] == {"params": {"expand": "permissions"}}
assert api_calls[1][2] == {}
assert api_calls[2][2] == {"params": {"expand": "permissions"}}
request_calls = [call for call in calls if call[0] == "request"]
assert all(call[3]["Authorization"] == "Bearer opaque-test-token" for call in request_calls)
create_call = next(call for call in calls if call[0] == "request" and call[1] == "POST")
assert create_call[2] == "https://npm.example/api/nginx/proxy-hosts"
assert create_call[4] is False
assert create_call[5]["json"] == {
"domain_names": ["forge.example.com"],
"forward_scheme": "http",
"forward_host": "itworx-forge",
"forward_port": 8080,
"access_list_id": 0,
"certificate_id": 0,
"enabled": False,
"ssl_forced": False,
"http2_support": False,
"hsts_enabled": False,
"hsts_subdomains": False,
"caching_enabled": False,
"block_exploits": True,
"allow_websocket_upgrade": False,
"trust_forwarded_proto": False,
"locations": [],
"meta": {"appops_forge_route_id": "froute-" + "e" * 32},
"advanced_config": "",
}
changed = dict(settings.apis)
changed["NPM_URL"] = "https://different-npm.example"
monkeypatch.setattr(settings, "apis", changed)
with pytest.raises(RuntimeError, match="npm_configuration_changed"):
route_session.create_forge_disabled_proxy_host("froute-" + "e" * 32)
assert calls[0] == ("client", 10.0, True, False)
assert calls[1][0] == "login"
assert calls[1][1] == "https://npm.example/api/tokens"
assert calls[1][2]["secret"] == "route-session-secret"
assert calls[1][4] is False
assert calls[3][1:3] == ("GET", "https://npm.example/api/users/me")
assert calls[3][4] is False
assert all(call[2] is True and call[3] is False for call in calls if call[0] == "client")
assert sum(call[0] == "request" and call[1] == "POST" for call in calls) == 1
@pytest.mark.parametrize(
("user", "status_code", "invalid_json"),
[
({}, 200, False),
({"id": 7, "is_disabled": False}, 200, False),
(
{"id": 7, "is_disabled": False, "permissions": {"visibility": "all"}},
200,
False,
),
(
{
"id": 7,
"is_disabled": False,
"permissions": {"visibility": "user", "proxy_hosts": "manage"},
},
200,
False,
),
(
{
"id": 7,
"is_disabled": False,
"permissions": {"visibility": "all", "proxy_hosts": "view"},
},
200,
False,
),
(
{
"id": 7,
"is_disabled": True,
"permissions": {"visibility": "all", "proxy_hosts": "manage"},
},
200,
False,
),
(
{
"id": True,
"is_disabled": False,
"permissions": {"visibility": "all", "proxy_hosts": "manage"},
},
200,
False,
),
(
{
"id": 0,
"is_disabled": False,
"permissions": {"visibility": "all", "proxy_hosts": "manage"},
},
200,
False,
),
(
{
"id": 7,
"is_disabled": "false",
"permissions": {"visibility": "all", "proxy_hosts": "manage"},
},
200,
False,
),
(
{
"id": 7,
"is_disabled": False,
"permissions": ["all", "manage"],
},
200,
False,
),
(
{
"id": 7,
"is_disabled": False,
"permissions": {"visibility": "all", "proxy_hosts": "manage"},
},
302,
False,
),
(None, 200, True),
],
ids=(
"absent-user",
"absent-permissions",
"partial-permissions",
"wrong-visibility",
"wrong-capability",
"disabled-user",
"boolean-user-id",
"zero-user-id",
"string-disabled-flag",
"malformed-permissions",
"redirect",
"malformed-json",
),
)
def test_npm_route_session_denies_incomplete_permissions_before_inventory(
monkeypatch, user, status_code, invalid_json
):
from backend.app.connectors import nginx_proxy_manager as npm_module
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
apis = dict(settings.apis)
apis.update(
{
"NPM_URL": "https://npm.example",
"NPM_IDENTITY": "operator@example.test",
"NPM_SECRET": "route-session-secret",
"NPM_VERIFY_TLS": True,
}
)
monkeypatch.setattr(settings, "apis", apis)
api_calls = []
class _Response:
def __init__(self, payload, response_status, invalid_json_response=False):
self._payload = payload
self.status_code = response_status
self._invalid_json_response = invalid_json_response
def json(self):
if self._invalid_json_response:
raise ValueError("malformed response")
return self._payload
class _Client:
def __init__(self, *, timeout, verify, follow_redirects):
assert verify is True
assert follow_redirects is False
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def post(self, url, *, json, headers, follow_redirects):
return _Response({"token": "opaque-test-token"}, 200)
def request(self, method, url, *, headers, follow_redirects, **kwargs):
api_calls.append((method, url, kwargs))
assert url.endswith("/users/me")
return _Response(user, status_code, invalid_json)
monkeypatch.setattr(npm_module.httpx, "Client", _Client)
route_session = NginxProxyManagerConnector().forge_route_session()
assert route_session is not None
from backend.app.connectors.nginx_proxy_manager import _NpmForgeRoutePermissionDenied
with pytest.raises(_NpmForgeRoutePermissionDenied):
route_session.create_forge_disabled_proxy_host("froute-" + "e" * 32)
assert api_calls == [
("GET", "https://npm.example/api/users/me", {"params": {"expand": "permissions"}})
]
assert not any(method == "POST" for method, _url, _kwargs in api_calls)
assert all(url.endswith("/users/me") for _method, url, _kwargs in api_calls)
def test_npm_route_session_treats_permission_probe_transport_failure_as_preflight_denial(monkeypatch):
import httpx
from backend.app.connectors import nginx_proxy_manager as npm_module
from backend.app.connectors.nginx_proxy_manager import (
NginxProxyManagerConnector,
_NpmForgeRoutePermissionDenied,
)
apis = dict(settings.apis)
apis.update(
{
"NPM_URL": "https://npm.example",
"NPM_IDENTITY": "operator@example.test",
"NPM_SECRET": "route-session-secret",
"NPM_VERIFY_TLS": True,
}
)
monkeypatch.setattr(settings, "apis", apis)
api_calls = []
class _Response:
status_code = 200
def json(self):
return {"token": "opaque-test-token"}
class _Client:
def __init__(self, *, timeout, verify, follow_redirects):
assert verify is True
assert follow_redirects is False
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def post(self, url, *, json, headers, follow_redirects):
return _Response()
def request(self, method, url, *, headers, follow_redirects, **kwargs):
api_calls.append((method, url, kwargs))
request = httpx.Request(method, url)
raise httpx.ConnectTimeout("private transport detail", request=request)
monkeypatch.setattr(npm_module.httpx, "Client", _Client)
route_session = NginxProxyManagerConnector().forge_route_session()
assert route_session is not None
with pytest.raises(_NpmForgeRoutePermissionDenied):
route_session.list_forge_route_hosts()
assert api_calls == [
("GET", "https://npm.example/api/users/me", {"params": {"expand": "permissions"}})
]
def test_npm_route_session_rechecks_permission_drift_before_single_post(monkeypatch):
from backend.app.connectors import nginx_proxy_manager as npm_module
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
apis = dict(settings.apis)
apis.update(
{
"NPM_URL": "https://npm.example",
"NPM_IDENTITY": "operator@example.test",
"NPM_SECRET": "route-session-secret",
"NPM_VERIFY_TLS": True,
}
)
monkeypatch.setattr(settings, "apis", apis)
api_calls = []
user_responses = [
{"id": 7, "is_disabled": False, "permissions": {"visibility": "all", "proxy_hosts": "manage"}},
{"id": 7, "is_disabled": False, "permissions": {"visibility": "all", "proxy_hosts": "view"}},
]
class _Response:
status_code = 200
def __init__(self, payload):
self._payload = payload
def json(self):
return self._payload
class _Client:
def __init__(self, *, timeout, verify, follow_redirects):
assert verify is True
assert follow_redirects is False
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def post(self, url, *, json, headers, follow_redirects):
return _Response({"token": "opaque-test-token"})
def request(self, method, url, *, headers, follow_redirects, **kwargs):
api_calls.append((method, url, kwargs))
if url.endswith("/users/me"):
return _Response(user_responses.pop(0))
if url.endswith("/nginx/proxy-hosts") and method == "GET":
return _Response([])
pytest.fail("permission drift must prevent the NPM POST")
monkeypatch.setattr(npm_module.httpx, "Client", _Client)
route_session = NginxProxyManagerConnector().forge_route_session()
assert route_session is not None
assert route_session.list_forge_route_hosts() == []
from backend.app.connectors.nginx_proxy_manager import _NpmForgeRoutePermissionDenied
with pytest.raises(_NpmForgeRoutePermissionDenied):
route_session.create_forge_disabled_proxy_host("froute-" + "e" * 32)
assert [call[:2] for call in api_calls] == [
("GET", "https://npm.example/api/users/me"),
("GET", "https://npm.example/api/nginx/proxy-hosts"),
("GET", "https://npm.example/api/users/me"),
]
assert not any(method == "POST" for method, _url, _kwargs in api_calls)
def test_npm_route_session_refuses_insecure_config_before_login(monkeypatch):
from backend.app.connectors import nginx_proxy_manager as npm_module
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
apis = dict(settings.apis)
apis.update(
{
"NPM_URL": "http://npm.example",
"NPM_IDENTITY": "operator@example.test",
"NPM_SECRET": "route-session-secret",
"NPM_VERIFY_TLS": True,
}
)
monkeypatch.setattr(settings, "apis", apis)
monkeypatch.setattr(
npm_module.httpx,
"Client",
lambda **_kwargs: pytest.fail("insecure NPM configuration must be rejected before login"),
)
assert NginxProxyManagerConnector().forge_route_session() is None
+246
View File
@@ -0,0 +1,246 @@
from __future__ import annotations
import pytest
from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine
from backend.app.services import user_access
_REAL_CURRENT_USER = user_access.current_user
_JOB_ID = "forge-install-" + "a" * 64
_PATH = f"/api/deployments/forge-bootstrap/first-install/{_JOB_ID}/route"
@pytest.fixture
def forge_route_operator_client(tmp_path, monkeypatch: pytest.MonkeyPatch):
from backend.app.core.config import settings
from backend.app.db import session as db_session
from backend.app.db.init_db import init_db
from backend.app.main import app
engine = create_engine(
f"sqlite:///{tmp_path / 'forge-route-api.db'}",
connect_args={"check_same_thread": False},
)
monkeypatch.setattr(db_session, "engine", engine)
monkeypatch.setenv("APPOPS_REQUIRE_OPERATOR_AUTH", "false")
monkeypatch.setattr(settings, "operator_proxy_auth_enabled", False)
monkeypatch.setattr(settings, "auth_admin_subjects", ["authentik:owner"], raising=False)
monkeypatch.setattr(settings, "auth_admin_emails", [], raising=False)
monkeypatch.setattr(settings, "data_dir", str(tmp_path))
monkeypatch.setattr(user_access, "current_user", _REAL_CURRENT_USER)
SQLModel.metadata.create_all(engine)
init_db()
client = TestClient(app, client=("198.18.10.20", 42000))
yield client
engine.dispose()
def _sign_in(client: TestClient, subject: str) -> None:
from backend.app.db import session as db_session
from backend.app.services.user_access import issue_session, provision_user
with Session(db_session.engine) as session:
user = provision_user(
session,
"authentik",
"https://auth.example/application/o/appops/",
{"sub": subject, "email": f"{subject}@example.test"},
)
cookie = issue_session(session, user)
client.cookies.set("appops_user_session", cookie)
def _safe_result(status="host_staged_disabled"):
staged = status == "host_staged_disabled"
return {
"success": staged,
"status": status,
"message": "forge_route_host_staged_disabled_not_routed" if staged else "forge_route_operation_not_found",
"target": "itworx-forge",
"jobId": _JOB_ID,
"hostname": "forge.example.com",
"upstream": "http://itworx-forge:8080",
"routeState": "host_staged_disabled" if staged else "not_routed",
"routed": False,
}
def test_route_post_requires_operator_csrf_strict_empty_body_and_bounded_key(
forge_route_operator_client: TestClient,
monkeypatch: pytest.MonkeyPatch,
):
from backend.app.services import forge_route_stage
calls = []
monkeypatch.setattr(
forge_route_stage,
"stage_forge_route",
lambda session, **kwargs: calls.append((session, kwargs)) or _safe_result(),
)
unauthenticated = forge_route_operator_client.post(
_PATH,
headers={"Idempotency-Key": "forge-route-api-0001"},
content=b"",
)
assert unauthenticated.status_code == 401
_sign_in(forge_route_operator_client, "owner")
missing_csrf = forge_route_operator_client.post(
_PATH,
headers={"Idempotency-Key": "forge-route-api-0001"},
content=b"",
)
assert missing_csrf.status_code == 403
assert calls == []
invalid_body = forge_route_operator_client.post(
_PATH,
headers={
"X-AppOps-Session-CSRF": "1",
"Idempotency-Key": "forge-route-api-0001",
},
json={},
)
assert invalid_body.status_code == 400
invalid_key = forge_route_operator_client.post(
_PATH,
headers={
"X-AppOps-Session-CSRF": "1",
"Idempotency-Key": "bad key",
},
content=b"",
)
assert invalid_key.status_code == 400
oversized_key = forge_route_operator_client.post(
_PATH,
headers={
"X-AppOps-Session-CSRF": "1",
"Idempotency-Key": "x" * 129,
},
content=b"",
)
assert oversized_key.status_code == 400
duplicate_key = forge_route_operator_client.post(
_PATH,
headers=[
("X-AppOps-Session-CSRF", "1"),
("Idempotency-Key", "forge-route-api-duplicate-01"),
("Idempotency-Key", "forge-route-api-duplicate-02"),
],
content=b"",
)
assert duplicate_key.status_code == 400
assert calls == []
accepted = forge_route_operator_client.post(
_PATH,
headers={
"X-AppOps-Session-CSRF": "1",
"Idempotency-Key": "forge-route-api-0001",
},
content=b"",
)
assert accepted.status_code == 200
assert accepted.json() == _safe_result()
assert len(calls) == 1
assert calls[0][1] == {
"job_id": _JOB_ID,
"idempotency_key": "forge-route-api-0001",
"actor_identity": "authentik:owner",
}
def test_route_post_denies_read_only_user_before_service(
forge_route_operator_client: TestClient,
monkeypatch: pytest.MonkeyPatch,
):
from backend.app.services import forge_route_stage
monkeypatch.setattr(
forge_route_stage,
"stage_forge_route",
lambda *_args, **_kwargs: pytest.fail("read-only user must not reach the executor"),
)
_sign_in(forge_route_operator_client, "reader")
denied = forge_route_operator_client.post(
_PATH,
headers={
"X-AppOps-Session-CSRF": "1",
"Idempotency-Key": "forge-route-api-0002",
},
content=b"",
)
assert denied.status_code == 403
def test_route_get_is_authenticated_and_delegates_only_safe_reconcile(
forge_route_operator_client: TestClient,
monkeypatch: pytest.MonkeyPatch,
):
from backend.app.services import forge_route_stage
calls = []
monkeypatch.setattr(
forge_route_stage,
"reconcile_forge_route",
lambda session, **kwargs: calls.append((session, kwargs)) or _safe_result("not_found"),
)
unauthenticated = forge_route_operator_client.get(_PATH)
assert unauthenticated.status_code == 401
_sign_in(forge_route_operator_client, "reader")
response = forge_route_operator_client.get(_PATH)
assert response.status_code == 200
assert response.json() == _safe_result("not_found")
assert len(calls) == 1
assert calls[0][1] == {"job_id": _JOB_ID, "actor_identity": "authentik:reader"}
invalid_body = forge_route_operator_client.request("GET", _PATH, content=b"unexpected")
assert invalid_body.status_code == 400
assert len(calls) == 1
def test_empty_request_body_guard_rejects_first_nonempty_stream_chunk():
import asyncio
from fastapi import HTTPException
from starlette.requests import Request
from backend.app.api.routes_deployments import _require_empty_forge_route_request
receive_calls = []
async def receive():
receive_calls.append(True)
if len(receive_calls) == 1:
return {"type": "http.request", "body": b"x", "more_body": True}
return {"type": "http.request", "body": b"oversized remainder", "more_body": False}
request = Request(
{
"type": "http",
"method": "POST",
"path": _PATH,
"raw_path": _PATH.encode(),
"query_string": b"",
"headers": [],
"client": ("127.0.0.1", 12345),
"server": ("testserver", 80),
"scheme": "http",
},
receive,
)
with pytest.raises(HTTPException) as error:
asyncio.run(_require_empty_forge_route_request(request))
assert error.value.status_code == 400
assert len(receive_calls) == 1
@@ -0,0 +1,904 @@
from __future__ import annotations
import hashlib
import json
from contextlib import nullcontext
from datetime import UTC, datetime
import pytest
from sqlmodel import Session, SQLModel, create_engine, select
_JOB_ID = "forge-install-" + "d" * 64
_REVISION = "059" + "a" * 37
_APP_OPS_REVISION = "b" * 40
_WORKSPACE_DIGEST = "sha256:" + "b" * 64
_FRONTDOOR_DIGEST = "sha256:" + "c" * 64
_NETWORK = "itworx-mcp-hub-npm"
@pytest.fixture
def route_db(tmp_path):
engine = create_engine(f"sqlite:///{tmp_path / 'forge-route-stage.db'}")
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
yield engine, session
engine.dispose()
def _job(session, *, status="deployed"):
from backend.app.db.models import DeploymentJob
from backend.app.services.forge_bootstrap_apply import _sha256
plan = {
"api_version": "itworx.forge-first-install/v1",
"revision": _REVISION,
"workspace_image_digest": _WORKSPACE_DIGEST,
"frontdoor_image_digest": _FRONTDOOR_DIGEST,
"preflight_job_id": "forge-preflight-" + "e" * 16,
"preflight_sha256": "f" * 64,
}
historical = {
"apiVersion": "itworx.forge-first-install/v1",
"status": "deployed",
"request": "2" * 64,
"repository": "jens/itworx-forge",
"revision": _REVISION,
"operation": "forge_first_install",
"workspaceImageDigest": _WORKSPACE_DIGEST,
"frontdoorImageDigest": _FRONTDOOR_DIGEST,
"receipt": {
"revision": _REVISION,
"workspaceImageDigest": _WORKSPACE_DIGEST,
"frontdoorImageDigest": _FRONTDOOR_DIGEST,
"migration": {"service": "migrate", "status": "completed", "result": "succeeded"},
"health": {
"status": "healthy",
"services": ["api", "dashboard", "itworx-forge", "postgres", "worker"],
},
},
}
job = DeploymentJob(
id=_JOB_ID,
application_id="itworx-forge",
repository="jens/itworx-forge",
revision=_REVISION,
plan_sha256=_sha256(plan),
idempotency_sha256="1" * 64,
status=status,
stage="controller_deployed" if status == "deployed" else "applying",
plan_json=plan,
preflight_json={
"apiVersion": "itworx.forge-bootstrap-preflight/v1",
"jobId": plan["preflight_job_id"],
"sha256": plan["preflight_sha256"],
},
evidence_json={"profile": "forge-first-install", "controller": historical},
broker_request_id="2" * 64,
completed_at=datetime.now(UTC) if status == "deployed" else None,
)
session.add(job)
session.commit()
return job
def _ready(job_id=_JOB_ID, *, status="ready", observed_at=None, migration_completed=True):
from backend.app.schemas.forge_runtime_readiness import _RUNTIME_SERVICES
return {
"status": status,
"message": {
"ready": "forge_runtime_ready",
"not_ready": "forge_runtime_not_ready",
"unavailable": "forge_runtime_readiness_unavailable",
}[status],
"target": "itworx-forge",
"jobId": job_id,
"revision": _REVISION,
"workspaceImageDigest": _WORKSPACE_DIGEST,
"frontdoorImageDigest": _FRONTDOOR_DIGEST,
"observedAt": int(datetime.now(UTC).timestamp()) if observed_at is None else observed_at,
"health": {
"status": "healthy" if status == "ready" else "not_ready",
"services": [
{"name": name, "status": "healthy" if status == "ready" else "not_ready"}
for name in _RUNTIME_SERVICES
],
},
"migrationCompleted": migration_completed,
"controllerArtifact": {
"controllerVersion": "2.4.1",
"sourceRepository": "jens/unraid-appops-gateway",
"sourceRevision": _APP_OPS_REVISION,
"sourceTree": "3" * 40,
"artifactSha256": "4" * 64,
},
}
def _route_host(owner: str, *, enabled=False, domains=None):
return {
"id": 31,
"domain_names": ["forge.example.com"] if domains is None else domains,
"forward_scheme": "http",
"forward_host": "itworx-forge",
"forward_port": 8080,
"access_list_id": 0,
"certificate_id": 0,
"enabled": enabled,
"ssl_forced": False,
"http2_support": False,
"hsts_enabled": False,
"hsts_subdomains": False,
"caching_enabled": False,
"block_exploits": True,
"allow_websocket_upgrade": False,
"trust_forwarded_proto": False,
"locations": [],
"advanced_config": "",
"meta": {"appops_forge_route_id": owner},
}
class _FakeNpm:
def __init__(
self,
hosts=None,
*,
create_error=None,
permission_denied=False,
host_enabled=False,
endpoint="https://npm.example",
):
self.hosts = list(hosts or [])
self.verified_tls = True
self.endpoint_sha256 = hashlib.sha256(endpoint.encode("ascii")).hexdigest()
self.create_error = create_error
self.permission_denied = permission_denied
self.host_enabled = host_enabled
self.calls = []
self.ownership_id = None
def forge_route_session(self):
return self
def list_forge_route_hosts(self):
self.calls.append("GET")
return list(self.hosts)
def create_forge_disabled_proxy_host(self, ownership_id):
if self.permission_denied:
self.calls.append("PERMISSION_DENIED")
from backend.app.connectors.nginx_proxy_manager import _NpmForgeRoutePermissionDenied
raise _NpmForgeRoutePermissionDenied()
self.calls.append("POST")
self.ownership_id = ownership_id
if self.create_error:
raise RuntimeError("secret-looking raw upstream detail must not escape")
self.hosts.append(_route_host(ownership_id, enabled=self.host_enabled))
return {"id": 31}
class _FakePortainer:
def __init__(self, result=None):
self.result = result or {
"status": "ready",
"network": _NETWORK,
"npmAttached": True,
"frontdoorAttached": True,
}
self.calls = 0
def forge_route_network_readiness(self):
self.calls += 1
return self.result
def _allow_route_policy(session):
from backend.app.db.models import Policy
policy = Policy(
id="policy-forge-route-stage",
action="forge.route.stage",
target="itworx-forge",
manual="allowed",
voice="blocked",
risk="critical",
)
session.add(policy)
session.commit()
return policy
def _wire_service(monkeypatch, *, ready=None, after_readiness=None):
from backend.app.services import forge_route_stage
from backend.app.services.forge_bootstrap_apply import ForgeBootstrapApplyService
monkeypatch.setattr(forge_route_stage, "_forge_oidc_process_lock", lambda: nullcontext())
monkeypatch.setattr(forge_route_stage, "_secret_store_ready", lambda: True)
def current_readiness(_service, session, job_id):
if after_readiness is not None:
after_readiness(session)
return ready or _ready(job_id)
monkeypatch.setattr(ForgeBootstrapApplyService, "current_readiness", current_readiness)
monkeypatch.setattr(forge_route_stage, "_audit", lambda *args, **kwargs: True)
return forge_route_stage
def test_stage_persists_encrypted_one_shot_intent_before_single_fixed_post(
route_db, monkeypatch
):
from backend.app.db.models import Secret
from backend.app.services.secret_store import get_secret
engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm()
fake_portainer = _FakePortainer()
stage = _wire_service(monkeypatch)
observed_intent = []
original_create = fake_npm.create_forge_disabled_proxy_host
observer_errors = []
def inspect_intent_before_post(owner):
try:
with Session(engine) as observer:
secret_ref = stage._journal_secret_ref(job.id)
row = observer.get(Secret, secret_ref)
assert row is not None
assert "route-stage-key-0001" not in row.encrypted_value
assert owner not in row.encrypted_value
record = json.loads(get_secret(observer, secret_ref, touch=False))
assert record["phase"] == "pending"
assert record["idempotencyKeySha256"] == __import__("hashlib").sha256(
b"route-stage-key-0001"
).hexdigest()
assert record["ownershipId"] == owner
expected_endpoint = hashlib.sha256(b"https://npm.example").hexdigest()
assert record["npmEndpointSha256"] == expected_endpoint
assert "npm.example" not in json.dumps(record)
assert "nonce" not in record
observed_intent.append(record)
except Exception as exc:
observer_errors.append(repr(exc))
raise
return original_create(owner)
fake_npm.create_forge_disabled_proxy_host = inspect_intent_before_post
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-key-0001",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=fake_portainer,
)
assert observed_intent, {"result": result, "calls": fake_npm.calls, "errors": observer_errors}
assert result["status"] == "host_staged_disabled", result
assert result["routeState"] == "host_staged_disabled"
assert result["routed"] is False
assert result["hostname"] == "forge.example.com"
assert fake_npm.calls == ["GET", "POST", "GET"]
assert fake_portainer.calls == 1
assert observed_intent[0]["revision"] == _REVISION
assert observed_intent[0]["desired"]["trust_forwarded_proto"] is False
assert observed_intent[0]["desired"]["locations"] == []
assert observed_intent[0]["workspaceImageDigest"] == _WORKSPACE_DIGEST
assert observed_intent[0]["frontdoorImageDigest"] == _FRONTDOOR_DIGEST
assert "secret-looking raw upstream detail" not in json.dumps(result)
def test_stage_records_permission_denial_as_aborted_before_post(route_db, monkeypatch):
from backend.app.services import forge_route_stage
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm(permission_denied=True)
stage = _wire_service(monkeypatch)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-key-0001",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
state, record = forge_route_stage._load_record(session, job.id)
assert result["status"] == "blocked"
assert result["message"] == "forge_route_npm_permission_required"
assert fake_npm.calls == ["GET", "PERMISSION_DENIED"]
assert "POST" not in fake_npm.calls
assert state == "present" and record is not None
assert record["phase"] == "aborted_before_post"
@pytest.mark.parametrize(
"readiness,network",
[
(_ready(status="not_ready"), None),
(_ready(observed_at=1), None),
(_ready(migration_completed=False), None),
(None, {"status": "ready", "network": _NETWORK, "npmAttached": True, "frontdoorAttached": "true"}),
],
)
def test_stage_fails_closed_on_stale_or_incomplete_readiness_or_network(
route_db, monkeypatch, readiness, network
):
engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm()
fake_portainer = _FakePortainer(network) if network is not None else _FakePortainer()
stage = _wire_service(monkeypatch, ready=readiness)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-key-0002",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=fake_portainer,
)
assert result["status"] == "blocked"
assert "POST" not in fake_npm.calls
@pytest.mark.parametrize(
"field,value",
[
("jobId", "forge-install-" + "9" * 64),
("revision", "f" * 40),
("workspaceImageDigest", "sha256:" + "9" * 64),
("frontdoorImageDigest", "sha256:" + "8" * 64),
],
)
def test_stage_fails_closed_on_wrong_current_readiness_identity(route_db, monkeypatch, field, value):
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm()
readiness = _ready(job.id)
readiness[field] = value
stage = _wire_service(monkeypatch, ready=readiness)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-wrong-readiness-01",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
assert result["status"] == "blocked"
assert fake_npm.calls.count("POST") == 0
@pytest.mark.parametrize("case", ["job_status", "repository", "target", "profile_receipt"])
def test_stage_fails_closed_on_nonexact_first_install_job(route_db, monkeypatch, case):
from backend.app.services.forge_bootstrap_apply import _sha256
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
if case == "job_status":
job.status = "applying"
elif case == "repository":
job.repository = "jens/another-repository"
elif case == "target":
job.application_id = "another-target"
else:
job.plan_json = {**job.plan_json, "frontdoor_image_digest": "sha256:" + "9" * 64}
job.plan_sha256 = _sha256(job.plan_json)
session.add(job)
session.commit()
fake_npm = _FakeNpm()
stage = _wire_service(monkeypatch)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-nonexact-job-01",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
assert result["status"] == "blocked"
assert fake_npm.calls == []
@pytest.mark.parametrize(
"domain,expected_status",
[
("*.example.com", "conflict"),
("*.tech", "conflict"),
("*.unrelated.example", "host_staged_disabled"),
],
)
def test_stage_blocks_covering_wildcards_but_allows_unrelated_wildcard(
route_db, monkeypatch, domain, expected_status
):
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm([_route_host("froute-" + "8" * 32, domains=[domain])])
stage = _wire_service(monkeypatch)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-wildcard-0001",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
assert result["status"] == expected_status
assert fake_npm.calls.count("POST") == (1 if expected_status == "host_staged_disabled" else 0)
def test_stage_rejects_malformed_npm_domain_inventory_without_post(route_db, monkeypatch):
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm([_route_host("froute-" + "8" * 32, domains=["bad..example.com"])])
stage = _wire_service(monkeypatch)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-malformed-domain-01",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
assert result["status"] == "failed"
assert fake_npm.calls == ["GET"]
def test_stage_lock_time_policy_recheck_prevents_npm_post(route_db, monkeypatch):
engine, session = route_db
job = _job(session)
policy = _allow_route_policy(session)
fake_npm = _FakeNpm()
stage = _wire_service(
monkeypatch,
after_readiness=lambda active_session: _block_route_policy(active_session),
)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-key-0003",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
session.refresh(policy)
assert result["status"] == "blocked"
assert policy.manual == "blocked"
assert "POST" not in fake_npm.calls
def test_readiness_expiring_after_intent_commit_aborts_before_npm_post(route_db, monkeypatch):
from backend.app.services import forge_route_stage
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm()
fake_time = [1000]
monkeypatch.setattr(forge_route_stage.time, "time", lambda: fake_time[0])
stage = _wire_service(monkeypatch, ready=_ready(job.id, observed_at=1000))
original_save = stage._save_record
first_save = [True]
def save_then_expire(active_session, record):
result = original_save(active_session, record)
if first_save[0]:
first_save[0] = False
fake_time[0] = 1031
return result
monkeypatch.setattr(stage, "_save_record", save_then_expire)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-expired-ready-01",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
assert result["status"] == "blocked"
assert result["message"] == "forge_route_current_readiness_expired"
assert "POST" not in fake_npm.calls
def _block_route_policy(session):
from backend.app.db.models import Policy
policy = session.exec(select(Policy).where(Policy.action == "forge.route.stage")).one()
policy.manual = "blocked"
session.add(policy)
session.commit()
def test_foreign_host_collision_is_conflict_and_never_adopted(route_db, monkeypatch):
engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm([_route_host("froute-" + "9" * 32)])
stage = _wire_service(monkeypatch)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-key-0004",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
assert result["status"] == "conflict"
assert "POST" not in fake_npm.calls
assert fake_npm.hosts[0]["meta"]["appops_forge_route_id"] == "froute-" + "9" * 32
def test_get_reconciliation_audits_phase_change_once_and_skips_unchanged_write(route_db, monkeypatch):
from backend.app.services import forge_route_stage
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm(create_error=RuntimeError("uncertain"))
stage = _wire_service(monkeypatch)
first = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-get-audit-0001",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
assert first["status"] == "outcome_unknown"
fake_npm.create_error = None
fake_npm.hosts = [_route_host(fake_npm.ownership_id)]
audit_calls = []
def audit_and_commit(session, **kwargs):
audit_calls.append(kwargs)
session.commit()
return True
monkeypatch.setattr(forge_route_stage, "_audit", audit_and_commit)
reconciled = forge_route_stage.reconcile_forge_route(
session,
job_id=job.id,
actor_identity="authentik:operator-42",
npm=fake_npm,
)
assert reconciled["status"] == "host_staged_disabled"
assert len(audit_calls) == 1
assert audit_calls[0]["actor_identity"] == "authentik:operator-42"
assert audit_calls[0]["decision"] == "allowed"
assert audit_calls[0]["policy_id"] is None
assert audit_calls[0]["result"]["routed"] is False
assert audit_calls[0]["idempotency_hash"] == hashlib.sha256(
b"route-stage-get-audit-0001"
).hexdigest()
original_save = forge_route_stage._save_record
writes = []
def track_save(session, record):
writes.append(record["phase"])
return original_save(session, record)
monkeypatch.setattr(forge_route_stage, "_save_record", track_save)
polled = forge_route_stage.reconcile_forge_route(
session,
job_id=job.id,
actor_identity="authentik:operator-42",
npm=fake_npm,
)
assert polled["status"] == "host_staged_disabled"
assert writes == []
assert len(audit_calls) == 1
@pytest.mark.parametrize("error_message", ["timeout", "npm_api_error"])
def test_uncertain_post_replays_and_reconciles_with_get_only(route_db, monkeypatch, error_message):
from backend.app.services import forge_route_stage
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm(create_error=RuntimeError(error_message))
stage = _wire_service(monkeypatch)
first = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-key-0005",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
replay = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-key-0005",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
other_key = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-key-other",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
assert first["status"] == "outcome_unknown"
assert replay["status"] == "outcome_unknown"
assert other_key["status"] == "conflict", other_key
assert fake_npm.calls.count("POST") == 1
assert fake_npm.calls[:4] == ["GET", "POST", "GET", "GET"]
fake_npm.create_error = None
fake_npm.hosts = [_route_host(fake_npm.ownership_id)]
reconciled = forge_route_stage.reconcile_forge_route(
session,
job_id=job.id,
actor_identity="authentik:owner",
npm=fake_npm,
)
assert reconciled["status"] == "host_staged_disabled"
assert fake_npm.calls.count("POST") == 1
def test_reconcile_refuses_changed_npm_endpoint_without_get(route_db, monkeypatch):
from backend.app.services import forge_route_stage
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
original_npm = _FakeNpm(
create_error=RuntimeError("timeout"), endpoint="https://npm.example/npm-a"
)
stage = _wire_service(monkeypatch)
first = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-endpoint-bind-01",
actor_identity="authentik:owner",
npm=original_npm,
portainer=_FakePortainer(),
)
assert first["status"] == "outcome_unknown"
state, record = forge_route_stage._load_record(session, job.id)
assert state == "present" and record is not None
assert record["npmEndpointSha256"] == hashlib.sha256(
b"https://npm.example/npm-a"
).hexdigest()
assert "/npm-a" not in json.dumps(record)
changed_npm = _FakeNpm(endpoint="https://npm.example/npm-b")
reconciled = forge_route_stage.reconcile_forge_route(
session,
job_id=job.id,
actor_identity="authentik:owner",
npm=changed_npm,
)
assert first["status"] == "outcome_unknown"
assert reconciled["status"] == "conflict"
assert reconciled["message"] == "forge_route_endpoint_binding_changed"
assert changed_npm.calls == []
def test_concurrent_same_key_requests_serialize_to_one_npm_post(route_db, monkeypatch):
from concurrent.futures import ThreadPoolExecutor
from threading import Lock
from backend.app.services import forge_route_stage
engine, session = route_db
job = _job(session)
job_id = job.id
_allow_route_policy(session)
fake_npm = _FakeNpm()
stage = _wire_service(monkeypatch)
route_lock = Lock()
monkeypatch.setattr(forge_route_stage, "_forge_oidc_process_lock", lambda: route_lock)
def call_stage():
with Session(engine) as worker_session:
return stage.stage_forge_route(
worker_session,
job_id=job_id,
idempotency_key="route-stage-concurrent-0001",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(lambda _index: call_stage(), range(2)))
assert [item["status"] for item in results] == [
"host_staged_disabled",
"host_staged_disabled",
]
assert fake_npm.calls.count("POST") == 1
@pytest.mark.parametrize("conflicting_hosts", [
lambda owner: [_route_host("froute-" + "9" * 32)],
lambda owner: [_route_host(owner), _route_host(owner)],
])
def test_reconciliation_never_adopts_wrong_or_duplicate_ownership_marker(
route_db, monkeypatch, conflicting_hosts
):
from backend.app.services import forge_route_stage
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm(create_error=RuntimeError("timeout"))
stage = _wire_service(monkeypatch)
first = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-marker-conflict-01",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
assert first["status"] == "outcome_unknown"
fake_npm.hosts = conflicting_hosts(fake_npm.ownership_id)
reconciled = forge_route_stage.reconcile_forge_route(
session,
job_id=job.id,
actor_identity="authentik:owner",
npm=fake_npm,
)
assert reconciled["status"] == "conflict"
assert fake_npm.calls.count("POST") == 1
def test_stage_refuses_string_boolean_or_enabled_host_readback(route_db, monkeypatch):
engine, session = route_db
job = _job(session)
_allow_route_policy(session)
fake_npm = _FakeNpm(host_enabled="false")
stage = _wire_service(monkeypatch)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-key-0006",
actor_identity="authentik:owner",
npm=fake_npm,
portainer=_FakePortainer(),
)
assert result["status"] == "conflict"
assert fake_npm.calls.count("POST") == 1
assert fake_npm.hosts[0]["enabled"] == "false"
def test_readiness_binds_distinct_authenticated_controller_revision(route_db, monkeypatch):
from backend.app.services import forge_route_stage
_engine, session = route_db
job = _job(session)
profile = forge_route_stage._verified_job(session, job.id)[1]
readiness = _ready(job.id)
identity = forge_route_stage._readiness_identity(readiness, job=job, profile=profile)
assert identity is not None
assert identity["revision"] == _REVISION
assert identity["controllerArtifact"]["sourceRevision"] == _APP_OPS_REVISION
assert identity["revision"] != identity["controllerArtifact"]["sourceRevision"]
readiness["controllerArtifact"]["sourceRepository"] = "jens/itworx-forge"
assert forge_route_stage._readiness_identity(readiness, job=job, profile=profile) is None
def test_record_validation_recomputes_readiness_identity_digest(route_db, monkeypatch):
from backend.app.services import forge_route_stage
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
stage = _wire_service(monkeypatch)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-record-hash-01",
actor_identity="authentik:owner",
npm=_FakeNpm(),
portainer=_FakePortainer(),
)
assert result["status"] == "host_staged_disabled"
state, record = forge_route_stage._load_record(session, job.id)
assert state == "present" and record is not None
record["readinessIdentity"]["observedAt"] += 1
assert forge_route_stage._record_valid(record, job.id) is False
def test_record_loader_rejects_duplicate_keys_and_noncanonical_json(route_db, monkeypatch):
from backend.app.db.models import Secret
from backend.app.services import forge_route_stage
from backend.app.services.secret_store import get_secret
_engine, session = route_db
job = _job(session)
_allow_route_policy(session)
stage = _wire_service(monkeypatch)
result = stage.stage_forge_route(
session,
job_id=job.id,
idempotency_key="route-stage-record-json-01",
actor_identity="authentik:owner",
npm=_FakeNpm(),
portainer=_FakePortainer(),
)
assert result["status"] == "host_staged_disabled"
secret_ref = forge_route_stage._journal_secret_ref(job.id)
assert session.get(Secret, secret_ref) is not None
original_get_secret = get_secret(session, secret_ref, touch=False)
assert original_get_secret is not None
monkeypatch.setattr(forge_route_stage, "get_secret", lambda *_args, **_kwargs: original_get_secret[:-1] + ',"phase":"pending"}')
assert forge_route_stage._load_record(session, job.id) == ("invalid", None)
monkeypatch.setattr(forge_route_stage, "get_secret", lambda *_args, **_kwargs: " " + original_get_secret)
assert forge_route_stage._load_record(session, job.id) == ("invalid", None)
def test_route_host_readback_requires_exact_false_trust_forwarded_proto():
from backend.app.services.forge_route_stage import (
_desired_state,
_host_is_exact_disabled,
)
owner = "froute-" + "a" * 32
desired = _desired_state(owner)
assert desired["trust_forwarded_proto"] is False
assert desired["locations"] == []
record = {"desired": desired, "ownershipId": owner}
host = _route_host(owner)
assert _host_is_exact_disabled(host, record) is True
for untrusted_value in (True, 0, "false", None):
changed = dict(host)
changed["trust_forwarded_proto"] = untrusted_value
assert _host_is_exact_disabled(changed, record) is False
missing = dict(host)
missing.pop("trust_forwarded_proto")
assert _host_is_exact_disabled(missing, record) is False
@@ -180,19 +180,70 @@ def test_descriptor_can_select_primary_service_for_multi_service_project():
assert contract is not None
assert contract["applicationId"] == "api"
assert contract["composeProject"] == "api"
assert contract["primaryService"] == "api"
assert contract["serviceNames"] == ["api", "worker"]
assert contract["containers"] == ["api-api-1", "api-worker-1"]
def test_descriptor_cannot_diverge_compose_project_from_application_id():
with pytest.raises(ValueError, match="composeProject must equal applicationId"):
def test_descriptor_separates_application_identity_compose_project_and_primary_service():
descriptor = {
"apiVersion": "itworx.deployment-target/v1",
"applicationId": "quizmonitor",
"composeProject": "quiz-monitor",
"primaryService": "quiz-server",
"composeFile": "docker-compose.yml",
}
service = FakeComposeInspection(
[".itworx", "docker-compose.yml"],
{
".itworx/deployment-target.json": json.dumps(descriptor),
"docker-compose.yml": "services:\n quiz-server:\n image: example/quiz-server:1\n",
},
)
contract = service._compose_contract(
FakeClient(), repository_name="QuizMonitor", revision="d" * 40
)
assert contract == {
"applicationId": "quizmonitor",
"composeFile": "docker-compose.yml",
"composeProject": "quiz-monitor",
"primaryService": "quiz-server",
"containers": ["quiz-monitor-quiz-server-1"],
"serviceNames": ["quiz-server"],
"healthTimeout": 180,
"verifyOciRevision": False,
}
def test_descriptor_rejects_invalid_compose_project_and_missing_primary_service():
with pytest.raises(ValueError, match="composeProject is invalid"):
GiteaProjectAutoRegistration._safe_descriptor(
{
"apiVersion": "itworx.deployment-target/v1",
"applicationId": "api",
"composeProject": "different",
"applicationId": "quizmonitor",
"composeProject": "../quiz-monitor",
"primaryService": "quiz-server",
},
"Example",
"QuizMonitor",
)
service = FakeComposeInspection(
[".itworx", "compose.yml"],
{
".itworx/deployment-target.json": json.dumps({
"apiVersion": "itworx.deployment-target/v1",
"applicationId": "quizmonitor",
"composeProject": "quiz-monitor",
"primaryService": "quiz-server",
}),
"compose.yml": "services:\n web:\n image: example/web:1\n",
},
)
with pytest.raises(ValueError, match="primaryService must name a Compose service"):
service._compose_contract(
FakeClient(), repository_name="QuizMonitor", revision="e" * 40
)
@@ -236,6 +287,39 @@ def test_sync_catalogs_every_repo_but_only_activates_proven_compose_targets():
assert all(item.voice == "blocked" for item in policies)
def test_sync_registers_quizmonitor_identity_without_changing_compose_project_or_service():
registry = FakeTargetRegistry()
contract = {
"applicationId": "quizmonitor",
"composeFile": "docker-compose.yml",
"composeProject": "quiz-monitor",
"primaryService": "quiz-server",
"containers": ["quiz-monitor-quiz-server-1"],
"serviceNames": ["quiz-server"],
"healthTimeout": 180,
"verifyOciRevision": False,
}
service = FakeProjectSync(
[repo("QuizMonitor")],
{"QuizMonitor": contract},
target_registry=registry,
)
with make_session() as session:
result = service.sync(session)
catalog = GiteaProjectAutoRegistration.catalog(session)
assert result["summary"]["registered"] == 1
assert len(registry.calls) == 1
repository, revision, kwargs = registry.calls[0]
assert repository == "jens/quizmonitor"
assert kwargs["compose_project"] == "quiz-monitor"
assert kwargs["containers"] == ["quiz-monitor-quiz-server-1"]
assert catalog["projects"][0]["applicationId"] == "quizmonitor"
assert catalog["projects"][0]["composeProject"] == "quiz-monitor"
assert catalog["projects"][0]["primaryService"] == "quiz-server"
def test_unchanged_catalog_entries_do_not_repeat_registration_work():
registry = FakeTargetRegistry()
repositories = [repo("Deployable")]
+354 -21
View File
@@ -194,6 +194,266 @@ def test_run_identity_requires_exact_protected_push_and_no_pull_request(modules)
assert not importer._run_matches({**run, **change}, revision)
def test_connector_ingestion_binding_is_scoped_to_authenticated_run_attempt(modules):
controller = modules.controller
revision = "a" * 40
run_id, attempt = 21531, 2
repository = controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES["connector-ingestion"]
archive_ref = f"{repository}:{revision}-run-{run_id}-attempt-{attempt}"
digest = "d" * 64
image_ref = f"{archive_ref}@sha256:{digest}"
image_id = "sha256:" + "e" * 64
binding = controller.mcp_hub_connector_ingestion_binding(
revision, run_id, attempt, image_ref, image_id
)
assert binding == {
"workflowRunId": run_id,
"workflowRunAttempt": attempt,
"archiveRef": archive_ref,
"imageRef": image_ref,
"imageId": image_id,
}
for invalid_ref in (
f"{repository}:{revision}-run-{run_id}@sha256:{digest}",
f"{repository}:{revision}-run-{run_id}-attempt-1@sha256:{digest}",
f"attacker.example/{repository.rsplit('/', 1)[-1]}:{revision}-run-{run_id}-attempt-{attempt}@sha256:{digest}",
f"{archive_ref}@sha256:{'F' * 64}",
f"{archive_ref}@sha256:{digest}:extra",
):
with pytest.raises(controller.DeployError):
controller.mcp_hub_connector_ingestion_binding(
revision, run_id, attempt, invalid_ref, image_id
)
with pytest.raises(controller.DeployError):
controller.mcp_hub_connector_ingestion_binding(
revision, run_id, 0, image_ref, image_id
)
def test_candidate_ingestion_binding_overrides_only_fixed_hub_render_environment(
modules,
):
controller = modules.controller
revision, tree = "a" * 40, "b" * 40
run_id, attempt = 21531, 2
repository = controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES["connector-ingestion"]
archive_ref = f"{repository}:{revision}-run-{run_id}-attempt-{attempt}"
image_ref = f"{archive_ref}@sha256:{'d' * 64}"
binding = controller.mcp_hub_connector_ingestion_binding(
revision, run_id, attempt, image_ref, "sha256:" + "e" * 64
)
project = {
"compose_project": controller.MCP_HUB_PROJECT,
"_mcp_hub_contract": controller.MCP_HUB_TOPOLOGY_CONTRACT,
"_mcp_hub_connector_ingestion_binding": binding,
"environment": {"CONNECTOR_INGESTION_IMAGE": "attacker.invalid/image:latest"},
}
metadata = {
"id": revision[:12],
"repository": controller.MCP_HUB_REPOSITORY,
"revision": revision,
"tree": tree,
"treeDigest": hashlib.sha256(f"git-tree:{tree}".encode()).hexdigest(),
"builtAt": "2026-10-03T12:00:00.000Z",
}
environment = controller.deployment_environment(
project, revision, controller.MCP_HUB_REPOSITORY, metadata
)
assert environment["CONNECTOR_INGESTION_IMAGE"] == image_ref
project["_mcp_hub_connector_ingestion_binding"]["archiveRef"] += "-tampered"
with pytest.raises(controller.DeployError):
controller.deployment_environment(
project, revision, controller.MCP_HUB_REPOSITORY, metadata
)
def test_registered_hub_cannot_supply_private_ingestion_binding(modules):
controller = modules.controller
raw = {
"branch": "main",
"slug": controller.MCP_HUB_PROJECT,
"compose_project": controller.MCP_HUB_PROJECT,
"containers": list(controller.MCP_HUB_LEGACY_CONTAINERS),
"source_dir": (
controller.MCP_HUB_APPDATA_ROOT / "deployment" / "repository"
).as_posix(),
"compose_dir": "infrastructure/compose",
"compose_files": list(controller.MCP_HUB_LEGACY_COMPOSE_FILES),
"env_source": (controller.MCP_HUB_APPDATA_ROOT / "deploy.env").as_posix(),
"_mcp_hub_connector_ingestion_binding": {"imageRef": "attacker.invalid/x"},
}
effective = controller.effective_mcp_hub_project(
controller.MCP_HUB_REPOSITORY, raw
)
assert "_mcp_hub_connector_ingestion_binding" not in effective
def test_import_manifest_ingestion_binding_rejects_wrong_run_or_archive_tag(modules):
importer = modules.importer
revision, tree = "a" * 40, "b" * 40
run = {"id": 21531, "run_attempt": 2}
repository = importer.controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES[
"connector-ingestion"
]
archive_ref = f"{repository}:{revision}-run-{run['id']}-attempt-{run['run_attempt']}"
row = {
"imageRef": f"{archive_ref}@sha256:{'d' * 64}",
"archiveRef": archive_ref,
"imageId": "sha256:" + "e" * 64,
}
manifest = {
"schema": importer.ARTIFACT_SCHEMA,
"repository": importer.REPOSITORY,
"sourceRevision": revision,
"sourceTree": tree,
"workflowPath": importer.WORKFLOW_PATH,
"workflowRunId": run["id"],
"artifactName": importer.ARTIFACT_PREFIX + revision,
"images": {
service: row if service == "connector-ingestion" else {}
for service in importer.controller.MCP_HUB_SERVICES
},
}
binding = importer._manifest_ingestion_binding(
manifest, revision=revision, tree=tree, run=run
)
assert binding["imageRef"] == row["imageRef"]
assert binding["archiveRef"] == archive_ref
for changed in (
{**manifest, "workflowRunId": run["id"] + 1},
{
**manifest,
"images": {
**manifest["images"],
"connector-ingestion": {
**row,
"archiveRef": archive_ref.replace("-attempt-2", "-attempt-1"),
},
},
},
):
with pytest.raises(importer.CandidateRejected):
importer._manifest_ingestion_binding(
changed, revision=revision, tree=tree, run=run
)
@pytest.mark.parametrize("drift", [None, "image_id", "repo_digest", "provenance"])
def test_import_immutably_pulls_and_reads_back_connector_ingestion(
modules, monkeypatch, drift
):
importer = modules.importer
controller = modules.controller
revision, tree, run_id, attempt = "a" * 40, "b" * 40, 21531, 2
built_at = "2026-10-03T12:00:00.000Z"
build = {
"id": revision[:12],
"repository": controller.MCP_HUB_REPOSITORY,
"revision": revision,
"tree": tree,
"treeDigest": hashlib.sha256(f"git-tree:{tree}".encode()).hexdigest(),
"builtAt": built_at,
}
labels = {
"org.opencontainers.image.source": controller.MCP_HUB_OCI_IMAGE_SOURCE,
"org.opencontainers.image.revision": revision,
"org.opencontainers.image.source-tree": tree,
"org.opencontainers.image.version": revision[:12],
"org.opencontainers.image.created": built_at,
"org.opencontainers.image.source-date-epoch": "1790985600",
}
images = {}
for index, service in enumerate(controller.MCP_HUB_SERVICES, start=1):
external = importer.EXTERNAL_IMAGES.get(service)
if external:
image_ref, archive_ref = external[0], external[1]
elif service == "connector-ingestion":
repo = controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES[service]
archive_ref = f"{repo}:{revision}-run-{run_id}-attempt-{attempt}"
image_ref = f"{archive_ref}@sha256:{'d' * 64}"
elif service == "contextforge":
image_ref = archive_ref = f"itworx-mcp-hub/contextforge-schema:{revision}"
else:
image_ref = archive_ref = f"fixture/{service}:{revision}"
images[service] = {
"imageRef": image_ref,
"archiveRef": archive_ref,
"imageId": "sha256:" + f"{index:064x}",
}
ingestion = images["connector-ingestion"]
immutable_ref = ingestion["imageRef"]
repo = controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES["connector-ingestion"]
immutable_inspect = {
"Id": ingestion["imageId"],
"RepoDigests": [f"{repo}@sha256:{'d' * 64}"],
"Config": {"User": "10001:10001", "Labels": dict(labels)},
}
tagged_inspect = {"Id": ingestion["imageId"], "Config": {"Labels": dict(labels)}}
if drift == "image_id":
immutable_inspect["Id"] = "sha256:" + "f" * 64
elif drift == "repo_digest":
immutable_inspect["RepoDigests"] = []
elif drift == "provenance":
immutable_inspect["Config"]["Labels"][
"org.opencontainers.image.source-tree"
] = "c" * 40
monkeypatch.setattr(
importer,
"_inspect_image",
lambda ref: tagged_inspect if ref == ingestion["archiveRef"] else immutable_inspect,
)
monkeypatch.setattr(
importer,
"_require_loaded_image",
lambda _service, row, **_kwargs: {
"Id": row["imageId"],
"Config": {"User": "10001:10001", "Labels": dict(labels)},
},
)
docker_calls = []
monkeypatch.setattr(
importer,
"_fixed_docker",
lambda *args, **kwargs: docker_calls.append((args, kwargs)),
)
if drift is None:
result = importer._verify_loaded_images(
images,
revision,
build,
"1790985600",
pull_external=True,
ingestion_run_id=run_id,
ingestion_run_attempt=attempt,
)
assert result["connector-ingestion"] == ingestion["imageId"]
assert any(
call[0] == ("pull", immutable_ref)
for call in docker_calls
)
else:
with pytest.raises(importer.CandidateRejected):
importer._verify_loaded_images(
images,
revision,
build,
"1790985600",
pull_external=True,
ingestion_run_id=run_id,
ingestion_run_attempt=attempt,
)
def test_selected_run_and_jobs_accept_the_live_gitea_job_shape(modules):
importer = modules.importer
revision = "a" * 40
@@ -353,12 +613,15 @@ def test_fake_local_import_publishes_candidate_and_requires_review_for_findings(
images = {}
for index, service in enumerate(controller.MCP_HUB_SERVICES, start=1):
external = importer.EXTERNAL_IMAGES.get(service)
image_ref = (
external[0]
if external is not None
else f"registry.example/itworx/{service}:{revision[:12]}"
)
archive_ref = external[1] if external is not None else image_ref
if external is not None:
image_ref, archive_ref = external[0], external[1]
elif service == "connector-ingestion":
repository = controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES[service]
archive_ref = f"{repository}:{revision}-run-21531-attempt-2"
image_ref = f"{archive_ref}@sha256:{'f' * 64}"
else:
image_ref = f"registry.example/itworx/{service}:{revision[:12]}"
archive_ref = image_ref
model_services[service] = {"image": image_ref}
images[service] = {
"imageRef": image_ref,
@@ -449,8 +712,8 @@ def test_fake_local_import_publishes_candidate_and_requires_review_for_findings(
)
monkeypatch.setattr(importer, "_remote_main_tree", lambda *_args: tree)
monkeypatch.setattr(importer, "_fixed_workflow", lambda *_args: None)
run = {"id": 21531, "run_attempt": 1}
run_state = {"attempt": 1}
run = {"id": 21531, "run_attempt": 2}
run_state = {"attempt": 2}
monkeypatch.setattr(
importer,
"_selected_run",
@@ -467,14 +730,35 @@ def test_fake_local_import_publishes_candidate_and_requires_review_for_findings(
monkeypatch.setattr(importer, "_candidate_artifact", lambda *_args: artifact)
monkeypatch.setattr(importer, "_docker_root", lambda: tmp_path)
monkeypatch.setattr(importer, "_check_disk_headroom", lambda **_kwargs: None)
monkeypatch.setattr(importer, "_parse_artifact_zip", lambda *_args, **_kwargs: (
{},
report_data,
hashlib.sha256(artifact_bytes).hexdigest(),
"d" * 64,
"e" * 64,
123,
))
artifact_manifest = {
"schema": importer.ARTIFACT_SCHEMA,
"repository": importer.REPOSITORY,
"sourceRevision": revision,
"sourceTree": tree,
"workflowPath": importer.WORKFLOW_PATH,
"workflowRunId": run["id"],
"artifactName": importer.ARTIFACT_PREFIX + revision,
"images": {
service: {
"imageRef": row["imageRef"],
"imageId": row["imageId"],
"archiveRef": row["archiveRef"],
}
for service, row in images.items()
},
}
def fake_parse_artifact(*_args, **_kwargs):
return (
artifact_manifest,
report_data,
hashlib.sha256(artifact_bytes).hexdigest(),
"d" * 64,
"e" * 64,
123,
)
monkeypatch.setattr(importer, "_parse_artifact_zip", fake_parse_artifact)
monkeypatch.setattr(
importer,
"_validate_manifest",
@@ -486,11 +770,17 @@ def test_fake_local_import_publishes_candidate_and_requires_review_for_findings(
def fake_fixed_docker(*args, **_kwargs):
if rerun_during_load and args and args[0] == "load":
run_state["attempt"] = 2
run_state["attempt"] = 3
return object()
monkeypatch.setattr(importer, "_fixed_docker", fake_fixed_docker)
monkeypatch.setattr(importer, "_verify_loaded_images", lambda *_args, **_kwargs: {})
verified_runs = []
def verify_loaded(_images, _revision, _build, _epoch, **kwargs):
verified_runs.append(kwargs)
return {}
monkeypatch.setattr(importer, "_verify_loaded_images", verify_loaded)
monkeypatch.setattr(
controller,
"_mcp_hub_check_candidate_exceptions",
@@ -502,7 +792,18 @@ def test_fake_local_import_publishes_candidate_and_requires_review_for_findings(
lambda *_args: draft,
)
def fake_plan(_repository, _revision, *, built_at, staging_directory, **_kwargs):
planned_bindings = []
def fake_plan(
_repository,
_revision,
*,
built_at,
staging_directory,
connector_ingestion_binding,
**_kwargs,
):
planned_bindings.append(connector_ingestion_binding)
Path(staging_directory).mkdir(mode=0o700)
frozen = {
"schema": prepare.BUILD_SCHEMA,
@@ -519,7 +820,15 @@ def test_fake_local_import_publishes_candidate_and_requires_review_for_findings(
monkeypatch.setattr(prepare, "plan_candidate", fake_plan)
def fake_capture(_repository, _revision, *, staging_directory, **_kwargs):
def fake_capture(
_repository,
_revision,
*,
staging_directory,
connector_ingestion_binding,
**_kwargs,
):
assert connector_ingestion_binding == planned_bindings[0]
image_rows = {
service: {
"imageRef": images[service]["imageRef"],
@@ -570,6 +879,15 @@ def test_fake_local_import_publishes_candidate_and_requires_review_for_findings(
_private_file(destination, artifact_bytes)
return None
def fake_no_overwrite_publish(stage, final_directory):
if final_directory.exists() or final_directory.is_symlink():
importer._reject(
"MCP Hub candidate destination already exists; import never overwrites"
)
final_directory.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
stage.rename(final_directory)
monkeypatch.setattr(importer, "_publish_candidate", fake_no_overwrite_publish)
candidate_directory = state_root / "mcp-hub-release-candidates" / revision
if rerun_during_load:
with pytest.raises(importer.CandidateRejected, match="run changed"):
@@ -589,7 +907,22 @@ def test_fake_local_import_publishes_candidate_and_requires_review_for_findings(
download_artifact=download_artifact,
)
assert candidate_directory.is_dir()
assert stat.S_IMODE(candidate_directory.stat().st_mode) == 0o700
assert planned_bindings[0]["workflowRunId"] == run["id"]
assert planned_bindings[0]["workflowRunAttempt"] == run["run_attempt"]
assert verified_runs == [
{
"pull_external": True,
"ingestion_run_id": run["id"],
"ingestion_run_attempt": run["run_attempt"],
},
{
"pull_external": False,
"ingestion_run_id": run["id"],
"ingestion_run_attempt": run["run_attempt"],
},
]
if importer.os.name != "nt":
assert stat.S_IMODE(candidate_directory.stat().st_mode) == 0o700
candidate_before_review = (candidate_directory / "candidate.json").read_bytes()
receipt_before_review = (candidate_directory / "import-receipt.json").read_bytes()
collision_stage = tmp_path / "second-candidate"
@@ -0,0 +1,177 @@
from __future__ import annotations
import hashlib
import io
import json
import subprocess
import sys
from email.message import Message
from types import SimpleNamespace
import pytest
from test_deploy_broker import BROKER_ROOT, _load_module
@pytest.fixture
def controller(monkeypatch):
module = _load_module("component_transport_test", BROKER_ROOT / "status_server.py")
monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_a, **_k: True)
monkeypatch.setattr(module, "_self_upgrade_secret", lambda: b"transport-test-key-" + b"x" * 48)
projects = {module.REGISTERED_AUTO_APPLY_REPOSITORY: {"branch": "main", "validation": {"required": True}},
"other/unchanged": {"branch": "main"}}
monkeypatch.setattr(module, "_load_projects", lambda: projects)
monkeypatch.setattr(module, "_write_projects", lambda updated: (projects.clear(), projects.update(updated)))
monkeypatch.setattr(module, "_validation_policy", lambda *_a: {"required": True})
return module, projects
def call(module, path, payload, method="_handle_registered_auto_apply"):
handler = module.Handler.__new__(module.Handler)
body = json.dumps(payload).encode()
handler.path = path
handler.headers = Message()
handler.headers["Content-Length"] = str(len(body))
handler.headers["Content-Type"] = "application/json"
handler.rfile = io.BytesIO(body)
handler._trusted_client = lambda: True
responses = []
handler._send_json = lambda status, data: responses.append((status, json.loads(data)))
getattr(handler, method)()
return responses[0]
def intent(module, projects):
project = projects[module.REGISTERED_AUTO_APPLY_REPOSITORY]
return {"repository": module.REGISTERED_AUTO_APPLY_REPOSITORY, "held": True,
"expectedRegistrySha256": hashlib.sha256(json.dumps(project, sort_keys=True, separators=(",", ":")).encode()).hexdigest(),
"reason": "Limit rollout to the validated Gitea component", "requestId": "11111111-1111-4111-8111-111111111111"}
def test_hold_is_cas_bound_and_retains_registered_policy(controller):
module, projects = controller
payload = intent(module, projects)
original = dict(projects[module.REGISTERED_AUTO_APPLY_REPOSITORY])
code, report = call(module, module.REGISTERED_AUTO_APPLY_PATH, payload)
assert code == 200 and report["status"] == "held" and report["autoApply"] is False
updated = projects[module.REGISTERED_AUTO_APPLY_REPOSITORY]
assert updated["branch"] == original["branch"]
assert updated["validation"] == original["validation"]
assert projects["other/unchanged"] == {"branch": "main"}
assert payload["reason"] not in json.dumps(updated)
assert call(module, module.REGISTERED_AUTO_APPLY_PATH, payload)[1]["reason"] == "registered_target_policy_changed"
code, preflight = call(module, module.REGISTERED_AUTO_APPLY_PATH + "/preflight", {"repository": payload["repository"]})
assert code == 200 and preflight["autoApply"] is False
payload.update(held=False, expectedRegistrySha256=preflight["registrySha256"])
assert call(module, module.REGISTERED_AUTO_APPLY_PATH, payload)[1]["status"] == "released"
@pytest.mark.parametrize("change", [{"repository": "other/unchanged"}, {"held": "true"}, {"image": "caller/image"},
{"requestId": "bad"}, {"expectedRegistrySha256": "0" * 64}])
def test_hold_rejects_unknown_target_input_and_stale_fingerprint(controller, change):
module, projects = controller
before = json.dumps(projects, sort_keys=True)
code, _ = call(module, module.REGISTERED_AUTO_APPLY_PATH, {**intent(module, projects), **change})
assert code in {400, 409}
assert json.dumps(projects, sort_keys=True) == before
def test_hold_authentication_and_inflight_deployment_remain_fail_closed(controller, monkeypatch):
module, projects = controller
payload = intent(module, projects)
monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_a, **_k: False)
assert call(module, module.REGISTERED_AUTO_APPLY_PATH, payload)[0] == 403
monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_a, **_k: True)
module._registered_auto_apply_lock.acquire()
try:
assert call(module, module.REGISTERED_AUTO_APPLY_PATH, payload)[1]["reason"] == "registered_target_deployment_active"
finally:
module._registered_auto_apply_lock.release()
def test_execution_rechecks_hold_under_same_lock(controller, monkeypatch):
module, projects = controller
invoked = []
def execute(command, **_kwargs):
assert module._registered_auto_apply_lock.acquire(blocking=False) is False
invoked.append(command)
return subprocess.CompletedProcess(command, 0)
monkeypatch.setattr(module.subprocess, "run", execute)
projects[module.REGISTERED_AUTO_APPLY_REPOSITORY]["auto_apply"] = False
assert module._run_registered_deploy(module.REGISTERED_AUTO_APPLY_REPOSITORY, "a" * 40).returncode == 4
assert invoked == []
projects[module.REGISTERED_AUTO_APPLY_REPOSITORY]["auto_apply"] = True
assert module._run_registered_deploy(module.REGISTERED_AUTO_APPLY_REPOSITORY, "a" * 40).returncode == 0
assert invoked == [["python3", str(module.ROOT / "deploy.py"), "deploy", module.REGISTERED_AUTO_APPLY_REPOSITORY, "a" * 40]]
def test_component_authentication_blocks_before_reading_or_mutating(controller, monkeypatch):
module, _ = controller
monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_a, **_k: False)
assert call(module, "/v1/appops/hub-components/apply", {"image": "caller/image"}, "_handle_hub_component")[0] == 403
def test_component_worker_serializes_hold_and_records_sanitized_failure(controller, monkeypatch):
module, _ = controller
failures = []
plan_id = "a" * 64
def apply(*_args, **_kwargs):
assert module._registered_auto_apply_lock.acquire(blocking=False) is False
raise RuntimeError("secret runtime configuration must never escape")
fake = SimpleNamespace(apply=apply, PromotionRejected=ValueError,
record_worker_failure=lambda *args, **_kw: failures.append(args))
monkeypatch.setitem(sys.modules, "immutable_service_promotion", fake)
module._component_worker_plans.add(plan_id)
module._run_component_worker({"planId": plan_id}, b"x" * 64)
assert failures == [(plan_id, "component_worker_failed")]
assert plan_id not in module._component_worker_plans
assert module._registered_auto_apply_lock.acquire(blocking=False)
module._registered_auto_apply_lock.release()
def test_component_queue_is_idempotent_and_failed_start_is_durable(controller, monkeypatch):
module, _ = controller
failures, starts = [], []
plan_id = "a" * 64
fake = SimpleNamespace(accept_apply=lambda *_a, **_k: {"status": "queued", "planId": plan_id},
PromotionRejected=ValueError,
record_worker_failure=lambda *args, **_kw: failures.append(args))
monkeypatch.setitem(sys.modules, "immutable_service_promotion", fake)
monkeypatch.setattr(module.threading, "Thread", lambda **kwargs: SimpleNamespace(start=lambda: starts.append(kwargs)))
payload = {"planId": plan_id}
assert call(module, "/v1/appops/hub-components/apply", payload, "_handle_hub_component")[0] == 202
assert call(module, "/v1/appops/hub-components/apply", payload, "_handle_hub_component")[0] == 202
assert len(starts) == 1
module._component_worker_plans.clear()
def fail_start(**_kwargs):
raise RuntimeError("thread unavailable")
monkeypatch.setattr(module.threading, "Thread", fail_start)
assert call(module, "/v1/appops/hub-components/apply", payload, "_handle_hub_component")[1]["reason"] == "component_worker_start_failed"
assert failures == [(plan_id, "component_worker_start_failed")]
assert not module._component_worker_plans
@pytest.mark.parametrize("validated", [False, True])
def test_hold_retains_push_validation_and_candidate_preparation_without_deploy(controller, monkeypatch, tmp_path, validated):
module, projects = controller
repository = module.REGISTERED_AUTO_APPLY_REPOSITORY
projects[repository]["auto_apply"] = False
revision = "a" * 40
semantic = module._webhook_semantic_digest(34, "refs/heads/main", revision)
marker = tmp_path / (semantic + ".json")
record = {"schema": module._WEBHOOK_SCHEMA, "repository": repository, "gitea_repository_id": 34,
"ref": "refs/heads/main", "revision": revision, "semantic_sha256": semantic,
"body_sha256": "b" * 64, "status": "validation_passed" if validated else "awaiting_validation"}
monkeypatch.setattr(module, "_read_inbox_record", lambda _path: record)
monkeypatch.setattr(module, "_webhook_policy", lambda *_a: (34, "main"))
monkeypatch.setattr(module, "_record_policy_for_processing", lambda *_a: True)
monkeypatch.setattr(module, "_process_validation", lambda *_a: False)
monkeypatch.setattr(module, "_atomic_inbox_update", lambda *_a: None)
monkeypatch.setattr(module, "_supersede_older_deliveries", lambda *_a: None)
prepared = []
monkeypatch.setattr(module, "_prepare_hub_candidate_delivery", lambda *_a: prepared.append(revision) or True)
monkeypatch.setattr(module, "_run_registered_deploy", lambda *_a: pytest.fail("held target must never deploy"))
module._process_webhook_record(marker)
assert prepared == ([revision] if validated else [])
assert record["status"] == ("validation_passed" if validated else "awaiting_validation")
if validated:
assert record["reason"] == "registered_target_auto_apply_held"
@@ -0,0 +1,667 @@
from __future__ import annotations
import contextlib
import copy
import importlib.util
import io
import json
import tarfile
import types
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location(
"immutable_promotion_test",
ROOT / "ops/gitea-deploy-broker/immutable_service_promotion.py",
)
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
KEY = b"test-receipt-key" * 3
REVISION = "b" * 40
class Runtime:
def __init__(self, path):
self.receipt_root = path / "receipts"
self.state = {
"target": {
"containerId": "a" * 64,
"imageId": "sha256:" + "a" * 64,
"revision": m.OLD_REVISION,
"spec": {
"image": "sha256:" + "a" * 64,
"environment": {"GITEA_CONNECTOR_BUILD_SHA": m.OLD_REVISION},
},
},
"nonTargets": {
"postgres": {"containerId": "c" * 64, "imageId": "sha256:" + "c" * 64}
},
}
self.calls = []
self.fail_health = False
self.fail_rollback = False
self.artifact = {
"revision": REVISION,
"imageId": "sha256:" + "b" * 64,
"candidateSha256": "d" * 64,
}
def lock(self):
return contextlib.nullcontext()
def snapshot(self):
return copy.deepcopy(self.state)
def candidate(self, revision, **kwargs):
self.calls.append("candidate")
assert revision == REVISION
return copy.deepcopy(self.artifact)
desired = m.ControllerRuntime.desired
def promote(self, desired):
self.calls.append("promote")
self.state["target"] = {**copy.deepcopy(desired), "containerId": "e" * 64}
def verify(self, desired, baseline, *, rollback=False):
if self.fail_health and not rollback:
raise ValueError("private diagnostic must not escape")
assert self.state["nonTargets"] == baseline["nonTargets"]
return {"containerId": self.state["target"]["containerId"]}
def rollback(self, baseline, desired):
self.calls.append("rollback")
if self.fail_rollback:
raise ValueError("secret rollback diagnostic")
self.state = copy.deepcopy(baseline)
def verify_record(self, record):
self.calls.append("verify_record")
@pytest.fixture
def fixture(tmp_path):
runtime = Runtime(tmp_path)
request = {
"repository": "Jens/ITWorx_MCP_Hub",
"service": m.SERVICE,
"revision": REVISION,
"expectedBaseline": m.digest(runtime.state),
"reason": "Repair verified Gitea compatibility",
"requestId": "d6fe0ba3-81f0-4ed6-b36d-10b12dbf55e4",
}
return runtime, request
def plan(runtime, request):
return m.preflight(request, api_json=None, receipt_key=KEY, runtime=runtime)
def arguments(request, result):
return {**request, "planId": result["planId"], "planDigest": result["planDigest"]}
@pytest.mark.parametrize(
"change",
[
{"service": "postgres"},
{"repository": "Jens/other"},
{"revision": "main"},
{"revision": "B" * 40},
{"requestId": "bad"},
{"image": "evil"},
{"command": "sh"},
{"expectedBaseline": "bad"},
{"reason": "line\nbreak"},
],
)
def test_closed_request_rejects_untrusted_target_or_extra_fields(fixture, change):
runtime, request = fixture
with pytest.raises(m.PromotionRejected):
plan(runtime, {**request, **change})
assert not runtime.calls
def test_preflight_is_readonly_and_receipts_do_not_expose_configuration(fixture):
runtime, request = fixture
result = plan(runtime, request)
assert result["status"] == "prepared"
assert result["expiresAt"] - result["createdAt"] == 300
assert runtime.calls == ["candidate"]
assert "spec" not in json.dumps(result) and "environment" not in json.dumps(result)
assert result["planId"] == m.digest(m.request(request))
def test_queue_is_durable_idempotent_and_no_runtime_operation(fixture):
runtime, request = fixture
result = plan(runtime, request)
runtime.calls.clear()
args = arguments(request, result)
queued = m.accept_apply(args, receipt_key=KEY, runtime=runtime)
assert queued["status"] == "queued"
assert m.accept_apply(args, receipt_key=KEY, runtime=runtime) == queued
assert runtime.calls == []
assert len(list(runtime.receipt_root.rglob("*.json"))) == 2
def test_apply_rechecks_artifact_and_baseline_then_only_promotes_once(fixture):
runtime, request = fixture
result = plan(runtime, request)
args = arguments(request, result)
m.accept_apply(args, receipt_key=KEY, runtime=runtime)
applied = m.apply(
args, api_json=None, download_artifact=None, receipt_key=KEY, runtime=runtime
)
assert applied["status"] == "applied"
assert runtime.calls == ["candidate", "candidate", "promote"]
assert (
m.apply(
args,
api_json=None,
download_artifact=None,
receipt_key=KEY,
runtime=runtime,
)
== applied
)
assert runtime.calls.count("promote") == 1
assert runtime.state["nonTargets"]["postgres"]["containerId"] == "c" * 64
assert (
m.status({"planId": result["planId"]}, receipt_key=KEY, runtime=runtime)
== applied
)
def test_unrelated_baseline_drift_blocks_before_any_import_or_write(fixture):
runtime, request = fixture
result = plan(runtime, request)
runtime.calls.clear()
runtime.state["nonTargets"]["postgres"]["containerId"] = "f" * 64
with pytest.raises(m.PromotionRejected, match="baseline_changed"):
m.apply(
arguments(request, result),
api_json=None,
download_artifact=None,
receipt_key=KEY,
runtime=runtime,
)
assert not runtime.calls
def test_artifact_change_after_preflight_blocks_without_promoting(fixture):
runtime, request = fixture
result = plan(runtime, request)
runtime.artifact["candidateSha256"] = "f" * 64
with pytest.raises(m.PromotionRejected, match="artifact_changed"):
m.apply(
arguments(request, result),
api_json=None,
download_artifact=None,
receipt_key=KEY,
runtime=runtime,
)
assert "promote" not in runtime.calls
def test_health_failure_restores_exact_baseline_and_records_rollback(fixture):
runtime, request = fixture
before = runtime.snapshot()
result = plan(runtime, request)
runtime.fail_health = True
outcome = m.apply(
arguments(request, result),
api_json=None,
download_artifact=None,
receipt_key=KEY,
runtime=runtime,
)
assert outcome["status"] == "rolled_back"
assert runtime.snapshot() == before
assert "private" not in json.dumps(outcome)
def test_rollback_failure_is_durable_and_cannot_blindly_retry(fixture):
runtime, request = fixture
result = plan(runtime, request)
runtime.fail_health = runtime.fail_rollback = True
args = arguments(request, result)
with pytest.raises(m.PromotionRejected, match="rollback_recovery_required"):
m.apply(
args,
api_json=None,
download_artifact=None,
receipt_key=KEY,
runtime=runtime,
)
assert (
m.status({"planId": result["planId"]}, receipt_key=KEY, runtime=runtime)[
"status"
]
== "recovery_required"
)
with pytest.raises(m.PromotionRejected, match="recovery_required"):
m.apply(
args,
api_json=None,
download_artifact=None,
receipt_key=KEY,
runtime=runtime,
)
assert runtime.calls.count("promote") == 1
def test_expired_plan_and_different_intent_are_rejected(fixture, monkeypatch):
runtime, request = fixture
result = plan(runtime, request)
args = arguments(request, result)
with pytest.raises(m.PromotionRejected, match="plan_mismatch"):
m.accept_apply(
{**args, "reason": "Changed reason"}, receipt_key=KEY, runtime=runtime
)
monkeypatch.setattr(m.time, "time", lambda: result["expiresAt"] + 1)
with pytest.raises(m.PromotionRejected, match="plan_expired"):
m.accept_apply(args, receipt_key=KEY, runtime=runtime)
def test_signature_tampering_chain_gaps_and_weak_key_fail_closed(fixture):
runtime, request = fixture
result = plan(runtime, request)
path = next(runtime.receipt_root.rglob("*.json"))
data = json.loads(path.read_text())
data["record"]["revision"] = "f" * 40
path.write_text(json.dumps(data))
with pytest.raises(m.PromotionRejected, match="signature_invalid"):
m.status({"planId": result["planId"]}, receipt_key=KEY, runtime=runtime)
with pytest.raises(m.PromotionRejected, match="key_invalid"):
m.ReceiptStore(runtime.receipt_root, b"weak")
def test_async_rejection_records_only_safe_reason_and_never_overwrites_recovery(
fixture,
):
runtime, request = fixture
result = plan(runtime, request)
m.accept_apply(arguments(request, result), receipt_key=KEY, runtime=runtime)
rejected = m.record_worker_failure(
result["planId"], "secret=raw error!", receipt_key=KEY, runtime=runtime
)
assert rejected["status"] == "rejected"
assert rejected["reasonCode"] == "component_worker_failed"
assert (
m.record_worker_failure(
result["planId"], "later", receipt_key=KEY, runtime=runtime
)
== rejected
)
@pytest.fixture
def adapter():
runtime = m.ControllerRuntime.__new__(m.ControllerRuntime)
runtime.docker = lambda *_args: [
{
"Id": "sha256:" + "a" * 64,
"Config": {
"Labels": {
"org.opencontainers.image.revision": m.OLD_REVISION,
"org.opencontainers.image.source": "https://gitea.example.com/jens/itworx-mcp-hub",
}
},
}
]
runtime.runtime_fingerprint = lambda: {
"blob": m.ABI_BLOBS["services/runtime/connector_gitea.py"][0]
}
env = {
"SERVICE_NAME": "connector-gitea",
"GITEA_CONNECTOR_BUILD_SHA": m.OLD_REVISION,
"GITEA_ENDPOINT": "https://gitea.example.com",
"GITEA_TOKEN_FILE": "/run/secrets/gitea_mcp_token",
"GITEA_WRITE_TOKEN_FILE": "/run/secrets/gitea_mcp_write_token",
"SECRET_EXPECTED_UID": "99",
"SECRET_EXPECTED_GID": "100",
"CONNECTOR_IDENTITY_KEY_FILE": "/run/secrets/connector_identity_signing_key",
"GPG_KEY": "public image fingerprint",
}
mounts = [
{
"Type": "bind",
"RW": False,
"Source": "/srv/example/appdata/itworx-mcp-hub/secrets/" + name,
"Destination": "/run/secrets/" + name,
}
for name in (
"gitea_mcp_token",
"gitea_mcp_write_token",
"connector_identity_signing_key",
)
]
mounts.append(
{
"Type": "bind",
"RW": False,
"Source": f"/srv/example/appdata/itworx-mcp-hub/deployment/releases/{m.OLD_REVISION}/services/runtime/connector_gitea.py",
"Destination": "/opt/hub/services/runtime/connector_gitea.py",
}
)
row = {
"Id": "a" * 64,
"Image": "sha256:" + "a" * 64,
"Config": {
"Entrypoint": ["python", "-m", "services.runtime"],
"Cmd": None,
"User": "99:100",
"WorkingDir": "/opt/hub",
"Tty": False,
"OpenStdin": False,
"ExposedPorts": {"8080/tcp": {}},
"Env": [k + "=" + v for k, v in env.items()],
"Labels": {},
"Healthcheck": {
"Interval": 60000000000,
"Retries": 5,
"StartPeriod": 10000000000,
"Timeout": 3000000000,
"Test": [
"CMD",
"python",
"-c",
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=2)",
],
},
},
"HostConfig": {
"ReadonlyRootfs": True,
"Privileged": False,
"CapDrop": ["ALL"],
"SecurityOpt": ["no-new-privileges:true"],
"Memory": 536870912,
"MemorySwap": -1,
"MemoryReservation": 0,
"NetworkMode": "itworx-mcp-hub-connectors",
"LogConfig": {
"Type": "json-file",
"Config": {"max-file": "3", "max-size": "10m"},
},
"ShmSize": 67108864,
"AutoRemove": False,
"CgroupnsMode": "private",
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger",
],
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware",
"/sys/devices/virtual/powercap",
],
"NanoCpus": 1000000000,
"PidsLimit": 128,
"Tmpfs": {"/tmp": "size=512m,mode=1777"},
"IpcMode": "private",
"RestartPolicy": {"Name": "unless-stopped", "MaximumRetryCount": 0},
},
"Mounts": mounts,
"NetworkSettings": {
"Networks": {
n: {"Aliases": [m.CONTAINER, m.SERVICE]}
for n in (
"itworx-mcp-hub-connectors",
"itworx-mcp-hub-observe",
"itworx-mcp-hub-egress",
)
}
},
}
return runtime, row
def test_known_live_target_preserves_public_metadata_and_only_image_build_sha_changes(
adapter,
):
runtime, row = adapter
target = runtime.target(row)
desired = runtime.desired(
{"target": target}, {"imageId": "sha256:" + "b" * 64, "revision": REVISION}
)
changed = copy.deepcopy(desired)
changed.update(imageId=target["imageId"], revision=target["revision"])
changed["spec"]["image"] = target["imageId"]
changed["spec"]["environment"]["GITEA_CONNECTOR_BUILD_SHA"] = m.OLD_REVISION
assert changed == target
assert target["spec"]["environment"]["SECRET_EXPECTED_UID"] == "99"
assert target["spec"]["environment"]["GPG_KEY"] == "public image fingerprint"
@pytest.mark.parametrize(
"field,value",
[
("Privileged", True),
("CapAdd", ["SYS_ADMIN"]),
("Devices", [{"PathOnHost": "/dev/sda"}]),
("ExtraHosts", ["attacker:127.0.0.1"]),
("PidMode", "host"),
("Memory", 0),
("ReadonlyRootfs", False),
],
)
def test_unexpected_security_runtime_composition_is_rejected(adapter, field, value):
runtime, row = adapter
row["HostConfig"][field] = value
with pytest.raises(m.PromotionRejected, match="configuration_unexpected"):
runtime.target(row)
@pytest.mark.parametrize(
"key,value",
[
("GITEA_TOKEN", "secret"),
("GITEA_ENDPOINT", "https://attacker.invalid"),
("PYTHONPATH", "/tmp/evil"),
("SECRET_EXPECTED_UID", "0"),
],
)
def test_env_drift_inline_secret_or_alternate_upstream_is_rejected(adapter, key, value):
runtime, row = adapter
row["Config"]["Env"] = [
v for v in row["Config"]["Env"] if not v.startswith(key + "=")
]
row["Config"]["Env"].append(key + "=" + value)
with pytest.raises(m.PromotionRejected):
runtime.target(row)
def test_fixed_promote_contains_only_one_service_no_database_or_build_commands(
adapter, tmp_path, monkeypatch
):
runtime, row = adapter
runtime.receipt_root = tmp_path
target = runtime.target(row)
calls = []
def run(argv, **kwargs):
calls.append(argv)
config = json.loads(Path(argv[5]).read_text())
assert set(config["services"]) == {m.SERVICE}
assert (
config["services"][m.SERVICE]["environment"]
== target["spec"]["environment"]
)
assert (
kwargs["stdout"] == m.subprocess.DEVNULL
and kwargs["stderr"] == m.subprocess.DEVNULL
)
return types.SimpleNamespace(returncode=0)
monkeypatch.setattr(m.subprocess, "run", run)
runtime.docker = lambda *_args: [{"Id": "d" * 64}]
runtime.promote(target)
assert calls[0][:5] == [
"docker",
"compose",
"--project-name",
"itworx-mcp-hub",
"--file",
]
assert calls[0][6:] == [
"up",
"--detach",
"--no-deps",
"--no-build",
"--pull",
"never",
"connector-gitea",
]
assert not list(tmp_path.glob(".component-*"))
def source_archive(
*, uid=0, regular=True, mode=0o664, content=b"reviewed", name="connector_gitea.py"
):
buffer = io.BytesIO()
with tarfile.open(fileobj=buffer, mode="w") as archive:
member = tarfile.TarInfo(name)
member.uid = uid
member.gid = 100
member.mode = mode
member.mtime = 1234
member.type = tarfile.REGTYPE if regular else tarfile.SYMTYPE
member.size = len(content) if regular else 0
archive.addfile(member, io.BytesIO(content) if regular else None)
return buffer.getvalue()
@pytest.mark.parametrize(
"value",
[
{"uid": 99},
{"uid": 0, "regular": False},
{"uid": 0, "regular": True, "mode": 0o666},
{"uid": 0, "regular": True, "mode": 0o664},
],
)
def test_runtime_source_change_symlink_unowned_or_writable_file_rejected(
value, monkeypatch
):
runtime = m.ControllerRuntime.__new__(m.ControllerRuntime)
raw = source_archive(
uid=value.get("uid", 0),
regular=value.get("regular", True),
mode=value.get("mode", 0o664),
content=b"changed",
)
monkeypatch.setattr(
m.subprocess,
"run",
lambda *_args, **_kwargs: types.SimpleNamespace(returncode=0, stdout=raw),
)
with pytest.raises(m.PromotionRejected, match="runtime_source_unreviewed"):
runtime.runtime_fingerprint()
def test_source_fingerprint_uses_readonly_archive_even_when_target_is_stopped(
monkeypatch,
):
runtime = m.ControllerRuntime.__new__(m.ControllerRuntime)
data = b"reviewed"
blob = m.hashlib.sha1(b"blob " + str(len(data)).encode() + b"\0" + data).hexdigest()
monkeypatch.setitem(
m.ABI_BLOBS, "services/runtime/connector_gitea.py", (blob, "new")
)
def run(argv, **kwargs):
assert argv == [
"docker",
"cp",
m.CONTAINER + ":/opt/hub/services/runtime/connector_gitea.py",
"-",
]
assert kwargs["stderr"] == m.subprocess.DEVNULL
return types.SimpleNamespace(returncode=0, stdout=source_archive(content=data))
monkeypatch.setattr(m.subprocess, "run", run)
assert runtime.runtime_fingerprint() == {
"uid": 0,
"gid": 100,
"mode": 0o664,
"size": 8,
"mtime": "1234",
"blob": blob,
}
def test_nonterminal_status_never_waits_for_the_deployment_lock(fixture):
runtime, request = fixture
result = plan(runtime, request)
m.accept_apply(arguments(request, result), receipt_key=KEY, runtime=runtime)
runtime.lock = lambda: pytest.fail("status must not block behind runtime promotion")
assert (
m.status({"planId": result["planId"]}, receipt_key=KEY, runtime=runtime)[
"status"
]
== "queued"
)
def test_component_requires_explicit_auto_apply_hold():
runtime = m.ControllerRuntime.__new__(m.ControllerRuntime)
runtime.controller = types.SimpleNamespace(
load_projects=lambda: {m.REPOSITORY: {"auto_apply": True}}
)
with pytest.raises(m.PromotionRejected, match="auto_apply_hold_required"):
runtime.snapshot()
def test_rollback_never_overwrites_a_recreated_or_edited_operator_target(adapter):
runtime, row = adapter
before = runtime.target(row)
baseline = {"target": before, "nonTargets": {"postgres": "unchanged"}}
desired = runtime.desired(
baseline, {"revision": REVISION, "imageId": "sha256:" + "b" * 64}
)
runtime.promoted_id = "d" * 64
observed = {
"target": {**desired, "containerId": "e" * 64},
"nonTargets": baseline["nonTargets"],
}
runtime.snapshot = lambda: observed
runtime.promote = lambda *_: pytest.fail("must never overwrite operator container")
with pytest.raises(m.PromotionRejected, match="rollback_cas_failed"):
runtime.rollback(baseline, desired)
observed["target"]["containerId"] = runtime.promoted_id
observed["target"]["runtimeFingerprint"] = {"blob": "changed"}
with pytest.raises(m.PromotionRejected, match="rollback_cas_failed"):
runtime.rollback(baseline, desired)
def test_rollback_loaded_image_provenance_must_match_bound_revision(adapter):
runtime, row = adapter
runtime.docker = lambda *_: [
{
"Id": row["Image"],
"Config": {"Labels": {"org.opencontainers.image.revision": "f" * 40}},
}
]
with pytest.raises(m.PromotionRejected, match="rollback_image_unverified"):
runtime.target(row)
def test_baseline_digest_changes_when_bound_source_stat_or_blob_changes(fixture):
runtime, _request = fixture
runtime.state["target"]["runtimeFingerprint"] = {"blob": "old", "inode": 5}
before = m.digest(runtime.state)
runtime.state["target"]["runtimeFingerprint"]["inode"] = 6
assert m.digest(runtime.state) != before
@@ -1,3 +1,4 @@
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
@@ -16,3 +17,47 @@ def test_preflight_preserves_docs_only_fast_path():
text = WORKFLOW.read_text(encoding="utf-8")
assert "runtime_change" in text
assert "*.md|*.mdx|docs/*" in text
def test_backend_budget_is_bounded_without_weakening_validation():
text = WORKFLOW.read_text(encoding="utf-8")
expected_timeouts = {
"preflight": 10,
"backend": 60,
"frontend": 30,
"security": 20,
"polyglot": 30,
"full": 10,
}
for job, expected_minutes in expected_timeouts.items():
match = re.search(
rf"(?ms)^ {re.escape(job)}:\n(?P<body>.*?)(?=^ [a-z][a-z0-9_-]*:\n|\Z)",
text,
)
assert match is not None, f"missing managed-validation job {job}"
timeout = re.search(r"(?m)^ timeout-minutes: (\d+)\s*$", match["body"])
assert timeout is not None, f"missing timeout for {job}"
assert int(timeout.group(1)) == expected_minutes
for lane in ("preflight", "backend", "frontend", "security", "polyglot", "full"):
assert f" {lane}:" in text
assert "needs: [preflight, backend, frontend, security, polyglot]" in text
assert "All managed validation lanes passed." in text
backend = re.search(
r"(?ms)^ backend:\n(?P<body>.*?)(?=^ [a-z][a-z0-9_-]*:\n|\Z)",
text,
)
assert backend is not None
backend_body = backend["body"]
assert 'if [[ "${profile}" == test || "${profile}" == full ]]; then' in backend_body
pytest_commands = [
line.strip() for line in backend_body.splitlines() if "-m pytest " in line
]
assert pytest_commands == [
'"${py}" -m pytest backend/tests -q --durations=20'
]
assert "continue-on-error" not in backend_body
assert "--ignore" not in backend_body
assert "--deselect" not in backend_body
@@ -4,6 +4,7 @@ import hashlib
import importlib
import json
import os
import shutil
import subprocess
import sys
import types
@@ -64,6 +65,8 @@ def harness(modules, tmp_path, monkeypatch):
state = types.SimpleNamespace(
source=tmp_path / "releases" / revision,
compose_marker="baseline",
clock=now,
ingestion_digest=None,
images={},
mutations=[],
inspect_calls=[],
@@ -119,9 +122,13 @@ def harness(modules, tmp_path, monkeypatch):
"validate_commit",
lambda _repo, branch, commit: commit if branch == "main" else "",
)
monkeypatch.setattr(
controller, "deployment_metadata", lambda *_args, **_kwargs: dict(metadata)
)
def deployment_metadata(_repo, _revision, _repository, *, built_at=None):
result = dict(metadata)
result["builtAt"] = built_at or state.clock
datetime.fromisoformat(result["builtAt"].replace("Z", "+00:00"))
return result
monkeypatch.setattr(controller, "deployment_metadata", deployment_metadata)
monkeypatch.setattr(controller, "mcp_hub_source_date_epoch", lambda *_args: epoch)
def stage(_project, _repo, commit):
@@ -144,11 +151,18 @@ def harness(modules, tmp_path, monkeypatch):
for index, service in enumerate(controller.MCP_HUB_SERVICES, 1):
if service in controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES:
local = controller.MCP_HUB_LOCAL_BUILD_IMAGE_REPOSITORIES.get(service)
reference = (
f"{local}:{revision[:12]}"
if local
else f"{controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES[service]}@sha256:{index:064x}"
)
if service == "connector-ingestion":
reference = (
f"{controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES[service]}@sha256:{state.ingestion_digest}"
if state.ingestion_digest
else ""
)
else:
reference = (
f"{local}:{revision[:12]}"
if local
else f"{controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES[service]}@sha256:{index:064x}"
)
elif service == "contextforge":
reference = f"itworx-mcp-hub/contextforge-schema:{revision}"
elif service == "ingestion-clamd":
@@ -174,6 +188,13 @@ def harness(modules, tmp_path, monkeypatch):
)
monkeypatch.setattr(utility, "_is_host_root", lambda: True)
utility.freeze_candidate(controller.MCP_HUB_REPOSITORY, revision)
freeze_dir = tmp_path / "mcp-hub-source-freezes" / revision
freeze_doc = json.loads((freeze_dir / "freeze.json").read_text())
# Publication happens after freeze and advances the wall clock. The plan
# uses the immutable timestamp and the now-available registry digest.
state.clock = "2026-10-04T02:03:04.567Z"
state.ingestion_digest = "f" * 64
plan = utility.plan_candidate(controller.MCP_HUB_REPOSITORY, revision)
candidate_dir = tmp_path / "mcp-hub-release-candidates" / revision
plan_doc = json.loads((candidate_dir / "build.json").read_text())
@@ -255,6 +276,8 @@ def harness(modules, tmp_path, monkeypatch):
now=now,
plan=plan,
plan_doc=plan_doc,
freeze_dir=freeze_dir,
freeze_doc=freeze_doc,
)
@@ -265,13 +288,225 @@ def test_plan_freezes_protected_metadata_without_candidate_or_approval(harness):
assert document["repository"] == h.controller.MCP_HUB_REPOSITORY
assert document["revision"] == h.revision
assert document["build"] == h.metadata
assert document["build"]["builtAt"] == h.freeze_doc["build"]["builtAt"]
assert document["build"]["builtAt"] != h.state.clock
assert document["sourceDateEpoch"] == h.epoch
assert len(document["composeSha256"]) == 64
assert h.state.ingestion_digest in str(h.model)
assert not (h.candidate_dir / "candidate.json").exists()
assert not (h.candidate_dir / "approval.json").exists()
assert h.state.mutations == []
def _remove_candidate_and_freeze(h):
shutil.rmtree(h.candidate_dir, ignore_errors=True)
shutil.rmtree(h.freeze_dir.parent, ignore_errors=True)
def _prepare_freeze_dir(h):
h.freeze_dir.parent.mkdir(mode=0o700)
h.freeze_dir.mkdir(mode=0o700)
def test_plan_requires_existing_freeze_before_any_candidate_write(harness):
h = harness
_remove_candidate_and_freeze(h)
with pytest.raises(
h.controller.DeployError,
match="run freeze, publish the fixed ingestion image, then run plan",
):
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert not h.candidate_dir.exists()
def test_freeze_is_non_replayable(harness):
h = harness
original = (h.freeze_dir / "freeze.json").read_bytes()
with pytest.raises(h.controller.DeployError, match="never overwritten|already exists"):
h.utility.freeze_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert (h.freeze_dir / "freeze.json").read_bytes() == original
@pytest.mark.parametrize(
("mutation", "message"),
[
("revision", "freeze record.*source identity"),
("tree", "freeze record.*source identity"),
("calendar", "freeze record.*source identity"),
("epoch", "freeze record.*source epoch"),
("duplicate", "unavailable or invalid|duplicate"),
("partial", "unavailable or invalid|partial"),
],
)
def test_plan_rejects_invalid_freeze_records_without_candidate_writes(
harness, mutation, message
):
h = harness
_remove_candidate_and_freeze(h)
_prepare_freeze_dir(h)
record = dict(h.freeze_doc)
if mutation == "revision":
record["revision"] = "c" * 40
payload = json.dumps(record).encode()
elif mutation == "tree":
record["build"] = dict(record["build"])
record["build"]["tree"] = "d" * 40
payload = json.dumps(record).encode()
elif mutation == "calendar":
record["build"] = dict(record["build"])
record["build"]["builtAt"] = "2026-99-99T00:00:00.000Z"
payload = json.dumps(record).encode()
elif mutation == "epoch":
record["sourceDateEpoch"] = "1790888864"
payload = json.dumps(record).encode()
elif mutation == "duplicate":
payload = b'{"schema":"duplicate","schema":"duplicate"}'
else:
payload = b'{"schema":'
(h.freeze_dir / "freeze.json").write_bytes(payload)
(h.freeze_dir / "freeze.json").chmod(0o600)
with pytest.raises(h.controller.DeployError, match=message):
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert not h.candidate_dir.exists()
def test_plan_rejects_symlink_freeze_file_without_candidate_writes(harness, tmp_path):
h = harness
_remove_candidate_and_freeze(h)
_prepare_freeze_dir(h)
outside = tmp_path / "outside.json"
outside.write_text(json.dumps(h.freeze_doc))
try:
(h.freeze_dir / "freeze.json").symlink_to(outside)
except OSError:
pytest.skip("symlink creation is unavailable on this filesystem")
with pytest.raises(
h.controller.DeployError,
match="unavailable or invalid|unsafe|not a regular file",
):
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert not h.candidate_dir.exists()
def test_plan_rejects_fifo_freeze_record_without_blocking(harness):
if os.name == "nt" or not hasattr(os, "mkfifo"):
pytest.skip("FIFO files are a POSIX filesystem contract")
h = harness
_remove_candidate_and_freeze(h)
_prepare_freeze_dir(h)
os.mkfifo(h.freeze_dir / "freeze.json", 0o600)
with pytest.raises(h.controller.DeployError, match="not a regular file"):
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert not h.candidate_dir.exists()
def test_plan_rejects_unsafe_freeze_directory_mode_without_candidate_writes(harness):
h = harness
if os.name == "nt":
pytest.skip("POSIX private-mode enforcement is a Linux controller contract")
_remove_candidate_and_freeze(h)
_prepare_freeze_dir(h)
(h.freeze_dir / "freeze.json").write_text(json.dumps(h.freeze_doc))
(h.freeze_dir / "freeze.json").chmod(0o600)
h.freeze_dir.chmod(0o755)
with pytest.raises(h.controller.DeployError, match="private|root-owned|ACL"):
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert not h.candidate_dir.exists()
@pytest.mark.parametrize(
"fault", ["file_mode", "owner", "dir_owner", "acl", "dir_acl"]
)
def test_plan_rejects_unsafe_freeze_file_metadata_without_candidate_writes(
harness, monkeypatch, fault
):
h = harness
if fault == "file_mode" and os.name == "nt":
pytest.skip("POSIX private-mode enforcement is a Linux controller contract")
_remove_candidate_and_freeze(h)
_prepare_freeze_dir(h)
freeze_file = h.freeze_dir / "freeze.json"
freeze_file.write_text(json.dumps(h.freeze_doc))
freeze_file.chmod(0o600)
if fault == "file_mode":
freeze_file.chmod(0o644)
match = "unsafe"
elif fault in {"owner", "dir_owner"}:
original = h.controller._root_owned
denied_path = h.freeze_dir if fault == "dir_owner" else freeze_file
monkeypatch.setattr(
h.controller,
"_root_owned",
lambda path: False if Path(path) == denied_path else original(path),
)
match = "root-owned|unsafe|owner"
else:
original = h.controller._mcp_hub_require_no_posix_acl
def reject_freeze_acl(path):
if Path(path) == (h.freeze_dir if fault == "dir_acl" else freeze_file):
raise h.controller.DeployError("freeze ACL denied")
original(path)
monkeypatch.setattr(h.controller, "_mcp_hub_require_no_posix_acl", reject_freeze_acl)
match = "ACL"
with pytest.raises(h.controller.DeployError, match=match):
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert not h.candidate_dir.exists()
def test_freeze_rejects_nonexact_local_main_head_without_writes(harness, monkeypatch):
h = harness
_remove_candidate_and_freeze(h)
monkeypatch.setattr(h.controller, "output", lambda _args: "c" * 40)
with pytest.raises(h.controller.DeployError, match="exact protected main head"):
h.utility.freeze_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert not h.freeze_dir.parent.exists()
def test_freeze_rejects_staged_source_mismatch_without_freeze_record(harness, monkeypatch):
h = harness
_remove_candidate_and_freeze(h)
monkeypatch.setattr(h.controller, "exact_source_matches_revision", lambda *_args: False)
with pytest.raises(h.controller.DeployError, match="Staged MCP Hub source differs"):
h.utility.freeze_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert not h.freeze_dir.parent.exists()
def test_freeze_rejects_invalid_epoch_before_staging_or_freeze_record(harness, monkeypatch):
h = harness
_remove_candidate_and_freeze(h)
monkeypatch.setattr(h.controller, "mcp_hub_source_date_epoch", lambda *_args: "invalid")
monkeypatch.setattr(
h.controller,
"mcp_hub_stage_release_source",
lambda *_args: pytest.fail("invalid epoch must fail before source staging"),
)
with pytest.raises(h.controller.DeployError, match="source epoch"):
h.utility.freeze_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert not h.freeze_dir.parent.exists()
def test_plan_rejects_staged_source_drift_before_candidate_writes(harness, monkeypatch):
h = harness
shutil.rmtree(h.candidate_dir)
monkeypatch.setattr(h.controller, "exact_source_matches_revision", lambda *_args: False)
with pytest.raises(h.controller.DeployError, match="Staged MCP Hub source differs"):
h.utility.plan_candidate(h.controller.MCP_HUB_REPOSITORY, h.revision)
assert not h.candidate_dir.exists()
def test_freeze_wrong_repository_and_nonroot_fail_before_writes(modules, monkeypatch, tmp_path):
controller, utility = modules
monkeypatch.setattr(utility, "_is_host_root", lambda: True)
monkeypatch.setattr(controller, "STATE_ROOT", tmp_path)
with pytest.raises(controller.DeployError, match="fixed MCP Hub repository"):
utility.freeze_candidate("someone/else", "a" * 40)
assert list(tmp_path.iterdir()) == []
monkeypatch.setattr(utility, "_is_host_root", lambda: False)
with pytest.raises(controller.DeployError, match="host root"):
utility.freeze_candidate(controller.MCP_HUB_REPOSITORY, "a" * 40)
assert list(tmp_path.iterdir()) == []
def test_plan_candidate_freezes_the_selected_workflow_creation_timestamp(
harness, monkeypatch
):
@@ -334,7 +569,7 @@ def test_plan_candidate_freezes_the_selected_workflow_creation_timestamp(
plan = json.loads(
(staging / "build.json").read_text()
)
assert observed == [built_at]
assert observed == [built_at, built_at]
assert plan["build"]["builtAt"] == built_at
assert len(compose_calls) == 1
@@ -349,6 +584,7 @@ def test_candidate_preparation_rejects_staging_path_outside_private_import_ancho
h.utility.plan_candidate(
h.controller.MCP_HUB_REPOSITORY,
revision,
built_at="2026-10-03T12:34:56.789Z",
staging_directory=staging,
)
assert not staging.exists()
@@ -363,16 +599,22 @@ def test_candidate_preparation_rejects_symlinked_staging_destination(harness):
target = h.controller.STATE_ROOT / "untrusted-target"
target.mkdir(mode=0o700)
staging = token / revision
staging.symlink_to(target, target_is_directory=True)
try:
staging.symlink_to(target, target_is_directory=True)
except OSError:
pytest.skip("symlink creation is unavailable on this filesystem")
with pytest.raises(h.controller.DeployError):
h.utility.plan_candidate(
h.controller.MCP_HUB_REPOSITORY,
revision,
built_at="2026-10-03T12:34:56.789Z",
staging_directory=staging,
)
def test_candidate_preparation_rejects_nonprivate_staging_parent(harness):
if os.name == "nt":
pytest.skip("POSIX private-mode enforcement is a Linux controller contract")
h = harness
revision = h.revision
anchor = h.controller.STATE_ROOT / "mcp-hub-release-imports" / revision
@@ -384,6 +626,7 @@ def test_candidate_preparation_rejects_nonprivate_staging_parent(harness):
h.utility.plan_candidate(
h.controller.MCP_HUB_REPOSITORY,
revision,
built_at="2026-10-03T12:34:56.789Z",
staging_directory=staging,
)
@@ -447,6 +690,10 @@ def test_capture_emits_candidate_and_pending_only_envelope_without_apply_calls(
digests = json.loads((h.candidate_dir / "image-digests.json").read_text())
assert result["status"] == "pending_review"
assert set(candidate["images"]) == set(h.controller.MCP_HUB_SERVICES)
assert candidate["images"]["connector-ingestion"]["imageRef"].endswith(
"@sha256:" + h.state.ingestion_digest
)
assert candidate["build"]["builtAt"] == h.freeze_doc["build"]["builtAt"]
assert digests["images"]["web"]["tag"] == candidate["images"]["web"]["imageRef"]
assert digests["images"]["web"]["imageId"] == candidate["images"]["web"]["imageId"]
candidate_bytes = (h.candidate_dir / "candidate.json").read_bytes()
@@ -385,10 +385,25 @@ def test_external_candidate_pins_are_exact_and_declared_by_protected_main():
node for node in parsed.body
if isinstance(node, ast.FunctionDef) and node.name == "fixed_image_reference"
)
declared = {
"postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04",
"redis:8.10.2@sha256:7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48",
}
postgres = (
"gitea.example.com/jens/itworx-mcp-hub/postgres:"
"wp400-gosu-v2-20261002-a94e0b4d2029@sha256:"
"a60d81e06987671ae903757f1b71cdfbbff4d481b42610e4de2b7dcfa32aad73"
)
redis = (
"gitea.example.com/jens/itworx-mcp-hub/redis:"
"wp400-remediation-20261002-4e18f61c@sha256:"
"7d826b44f245ad32e8fe4bf40d42eae95855c3942c5f2e42784ee8fd3f9547a2"
)
previous_postgres = (
"postgres:18.6@sha256:"
"0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04"
)
previous_redis = (
"redis:8.10.2@sha256:"
"7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48"
)
declared = {previous_postgres, previous_redis, postgres, redis}
namespace = {
"re": re,
"main_declares_external_image": lambda image_ref: image_ref in declared,
@@ -399,13 +414,19 @@ def test_external_candidate_pins_are_exact_and_declared_by_protected_main():
fixed = namespace["fixed_image_reference"]
assert fixed("postgres", "postgres:18.4@sha256:3a82e1f56c8f0f5616a11103ac3d47e632c3938698946a7ad26da0df1334744a")
assert fixed(
"postgres",
"postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04",
)
assert fixed("postgres", "postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04")
assert fixed("redis", "redis:8.10.0@sha256:c29e49ab2f85760a3827b53882e6dd9f5c6c3f0bb7d724e07bb31cbf275a5236")
assert fixed("redis", "redis:8.10.2@sha256:7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48")
assert not fixed("postgres", "postgres:18.6@sha256:" + "a" * 64)
assert fixed("postgres", postgres)
assert fixed("redis", redis)
assert not fixed("redis", postgres)
assert not fixed("postgres", postgres[:-1] + "4")
assert not fixed("postgres", postgres.replace("wp400-gosu-v2-20261002-a94e0b4d2029", "wp400-gosu-v2-20261002-wrong"))
assert not fixed("postgres", postgres.replace("gitea.example.com/jens/itworx-mcp-hub", "gitea.example.com/other/itworx-mcp-hub"))
assert not fixed("redis", redis[:-1] + "3")
assert not fixed("redis", redis.replace("wp400-remediation-20261002-4e18f61c", "wp400-remediation-20261002-wrong"))
assert not fixed("redis", redis.replace("gitea.example.com/jens/itworx-mcp-hub", "gitea.example.com/other/itworx-mcp-hub"))
assert not fixed("postgres", "postgres:19.0@sha256:" + "a" * 64)
assert not fixed("redis", "redis:8.10.3@sha256:" + "b" * 64)
for service in ("web", "control-api", "dataplane-router", "connector-ingestion"):
assert fixed(service, "sha256:" + "a" * 64)
@@ -413,8 +434,58 @@ def test_external_candidate_pins_are_exact_and_declared_by_protected_main():
assert not fixed(service, "sha256:" + "a" * 64)
namespace["main_declares_external_image"] = lambda _image_ref: False
assert not fixed("postgres", "postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04")
assert not fixed("redis", "redis:8.10.2@sha256:7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48")
assert fixed("postgres", "postgres:18.4@sha256:3a82e1f56c8f0f5616a11103ac3d47e632c3938698946a7ad26da0df1334744a")
assert not fixed("postgres", previous_postgres)
assert fixed("redis", "redis:8.10.0@sha256:c29e49ab2f85760a3827b53882e6dd9f5c6c3f0bb7d724e07bb31cbf275a5236")
assert not fixed("redis", previous_redis)
assert not fixed("postgres", postgres)
assert not fixed("redis", redis)
@pytest.mark.parametrize("service, image_ref", [
(
"postgres",
"gitea.example.com/jens/itworx-mcp-hub/postgres:"
"wp400-gosu-v2-20261002-a94e0b4d2029@sha256:"
"a60d81e06987671ae903757f1b71cdfbbff4d481b42610e4de2b7dcfa32aad73",
),
(
"redis",
"gitea.example.com/jens/itworx-mcp-hub/redis:"
"wp400-remediation-20261002-4e18f61c@sha256:"
"7d826b44f245ad32e8fe4bf40d42eae95855c3942c5f2e42784ee8fd3f9547a2",
),
])
def test_new_db_pin_requires_exact_protected_main_compose_declaration(service, image_ref):
fake = FakePortainer()
McpHubDeploymentReconciliation(fake).run()
script = fake.calls[0][2]["json"]["Cmd"][2]
parsed = ast.parse(script)
function = next(
node for node in parsed.body
if isinstance(node, ast.FunctionDef) and node.name == "main_declares_external_image"
)
main_revision = "a" * 40
content = {"value": f"services:\n {service}:\n image: {image_ref}\n"}
calls = []
def git_run(*args):
calls.append(args)
return subprocess.CompletedProcess(args, 0, stdout=content["value"])
namespace = {"re": re, "main_revision": main_revision, "git_run": git_run}
exec(compile(ast.Module(body=[function], type_ignores=[]), "main-image-declaration-test", "exec"), namespace)
declares = namespace["main_declares_external_image"]
assert declares(image_ref)
assert calls == [("show", f"{main_revision}:infrastructure/compose/docker-compose.yml")]
calls.clear()
content["value"] = f"services:\n {service}:\n image: {image_ref[:-1]}0\n"
assert not declares(image_ref)
assert calls == [
("show", f"{main_revision}:infrastructure/compose/docker-compose.yml"),
("show", f"{main_revision}:infrastructure/compose/docker-compose.prod.yml"),
]
@pytest.fixture
@@ -268,15 +268,49 @@ def test_exact_legacy_target_derives_only_the_fixed_17_service_contract(controll
assert target["containers"] == list(controller.MCP_HUB_LEGACY_CONTAINERS)
assert controller.effective_mcp_hub_project("jens/other", target) is target
def test_postgres_candidate_pin_matches_the_reviewed_release_evidence(controller):
expected = (
def test_db_candidate_pins_and_observed_transition_refs_are_exact(controller):
postgres = (
"gitea.example.com/jens/itworx-mcp-hub/postgres:"
"wp400-gosu-v2-20261002-a94e0b4d2029@sha256:"
"a60d81e06987671ae903757f1b71cdfbbff4d481b42610e4de2b7dcfa32aad73"
)
redis = (
"gitea.example.com/jens/itworx-mcp-hub/redis:"
"wp400-remediation-20261002-4e18f61c@sha256:"
"7d826b44f245ad32e8fe4bf40d42eae95855c3942c5f2e42784ee8fd3f9547a2"
)
assert controller.MCP_HUB_POSTGRES_IMAGE == postgres
assert controller.MCP_HUB_REDIS_IMAGE == redis
assert controller.MCP_HUB_POSTGRES_PREVIOUS_IMAGE == (
"postgres:18.6@sha256:0377e72c5289ed2f98cf61b1a9c2db9eb9d300317fe14244492fbc94343b3d04"
)
assert controller.MCP_HUB_POSTGRES_IMAGE == expected
assert controller.MCP_HUB_OBSERVED_EXTERNAL_IMAGE_TRANSITIONS["postgres"] == (
controller.MCP_HUB_POSTGRES_LEGACY_IMAGE,
expected,
assert controller.MCP_HUB_REDIS_PREVIOUS_IMAGE == (
"redis:8.10.2@sha256:7ef5b5cec96495a04ca7feff88a9492efeab8053fb284d24bdd73344c9245a48"
)
assert controller.MCP_HUB_OBSERVED_EXTERNAL_IMAGE_TRANSITIONS == {
"postgres": (
(controller.MCP_HUB_POSTGRES_LEGACY_IMAGE, controller.MCP_HUB_POSTGRES_PREVIOUS_IMAGE),
postgres,
),
"redis": (
(controller.MCP_HUB_REDIS_LEGACY_IMAGE, controller.MCP_HUB_REDIS_PREVIOUS_IMAGE),
redis,
),
}
@pytest.mark.parametrize("service", ["postgres", "redis"])
def test_db_transition_accepts_only_new_forward_pin_against_exact_observed_refs(controller, service):
observed_refs, candidate_ref = controller.MCP_HUB_OBSERVED_EXTERNAL_IMAGE_TRANSITIONS[service]
assert controller._mcp_hub_observed_external_ref_allowed(service, candidate_ref, candidate_ref)
for old_ref in observed_refs:
assert controller._mcp_hub_observed_external_ref_allowed(service, candidate_ref, old_ref)
assert not controller._mcp_hub_observed_external_ref_allowed(service, old_ref, old_ref)
assert not controller._mcp_hub_observed_external_ref_allowed(service, candidate_ref, old_ref + "-unexpected")
assert not controller._mcp_hub_observed_external_ref_allowed(service, candidate_ref + "-unexpected", candidate_ref)
other_candidate = (
controller.MCP_HUB_REDIS_IMAGE if service == "postgres" else controller.MCP_HUB_POSTGRES_IMAGE
)
assert not controller._mcp_hub_observed_external_ref_allowed(service, other_candidate, candidate_ref)
def test_installed_legacy_registry_shape_is_adapted_only_in_memory(controller):
target = {
@@ -1402,7 +1436,7 @@ def test_private_backup_artifacts_publish_atomically_without_overwrite(
@pytest.mark.parametrize(
"image_ref_name",
["MCP_HUB_POSTGRES_IMAGE", "MCP_HUB_POSTGRES_LEGACY_IMAGE"],
["MCP_HUB_POSTGRES_IMAGE", "MCP_HUB_POSTGRES_PREVIOUS_IMAGE", "MCP_HUB_POSTGRES_LEGACY_IMAGE"],
)
def test_live_mount_inventory_includes_volume_identity_source_and_read_only_state(
controller, monkeypatch, image_ref_name
@@ -54,6 +54,7 @@ def prepared(controller, tmp_path, monkeypatch):
project = {
"_mcp_hub_contract": controller.MCP_HUB_TOPOLOGY_CONTRACT,
"_repository_path": str(tmp_path / "hub.git"),
"compose_project": controller.MCP_HUB_PROJECT,
"_deployment_metadata": metadata,
"_mcp_hub_source_date_epoch": "1790888863",
}
@@ -89,6 +90,10 @@ def prepared(controller, tmp_path, monkeypatch):
ref = f"fixture/{service}@sha256:" + f"{index:064x}"
if service == "contextforge":
ref = f"itworx-mcp-hub/contextforge-schema:{revision}"
elif service == "connector-ingestion":
repository = controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES[service]
archive_ref = f"{repository}:{revision}-run-21531-attempt-2"
ref = f"{archive_ref}@sha256:" + f"{index:064x}"
model["services"][service] = {"image": ref}
vuln = {
"SchemaVersion": 2,
@@ -142,6 +147,27 @@ def prepared(controller, tmp_path, monkeypatch):
}
candidate_sha = write("candidate.json", candidate)
exceptions_sha = write("exceptions.json", {"exceptions": []})
write(
"import-receipt.json",
{
"schema": "itworx.mcp-hub-import-receipt/v1",
"repository": controller.MCP_HUB_REPOSITORY,
"sourceRevision": revision,
"sourceTree": metadata["tree"],
"workflowRunId": 21531,
"workflowRunAttempt": 2,
"artifactId": 882,
"artifactName": "hub-release-candidate-" + revision,
"artifactZipSha256": "1" * 64,
"manifestSha256": "2" * 64,
"dockerTarSha256": "3" * 64,
"candidateSha256": candidate_sha,
"draftExceptionsSha256": exceptions_sha,
"imageIds": {
service: image["imageId"] for service, image in images.items()
},
},
)
write(
"approval.json",
{
@@ -162,8 +188,17 @@ def prepared(controller, tmp_path, monkeypatch):
v["imageRef"]: {
"Id": v["imageId"],
"Config": {"User": "10001:10001", "Labels": dict(labels)},
**(
{
"RepoDigests": [
f"{controller.MCP_HUB_OWNED_IMAGE_REPOSITORIES['connector-ingestion']}@{v['imageRef'].rsplit('@', 1)[1]}"
]
}
if service == "connector-ingestion"
else {}
),
}
for v in images.values()
for service, v in images.items()
}
monkeypatch.setattr(controller, "inspect_image", lambda ref: facts.get(ref))
return types.SimpleNamespace(
@@ -182,6 +217,9 @@ def prepared(controller, tmp_path, monkeypatch):
def test_fresh_complete_candidate_freezes_metadata_and_checks_explicit_gate(
controller, prepared
):
prepared.project["environment"] = {
"CONNECTOR_INGESTION_IMAGE": "attacker.invalid/connector:latest"
}
result = controller.mcp_hub_load_release_candidate(
prepared.project, prepared.revision
)
@@ -191,6 +229,35 @@ def test_fresh_complete_candidate_freezes_metadata_and_checks_explicit_gate(
controller.mcp_hub_require_candidate_images(
prepared.project, prepared.model, prepared.revision
)
binding = prepared.project["_mcp_hub_connector_ingestion_binding"]
environment = controller.deployment_environment(
prepared.project,
prepared.revision,
controller.MCP_HUB_REPOSITORY,
prepared.metadata,
)
assert environment["CONNECTOR_INGESTION_IMAGE"] == binding["imageRef"]
@pytest.mark.parametrize("mutation", ["missing", "attempt", "image_id"])
def test_import_receipt_is_required_and_binds_connector_ingestion_attempt(
controller, prepared, mutation
):
path = prepared.directory / "import-receipt.json"
if mutation == "missing":
path.unlink()
else:
receipt = json.loads(path.read_text(encoding="utf-8"))
if mutation == "attempt":
receipt["workflowRunAttempt"] = 1
else:
receipt["imageIds"]["connector-ingestion"] = "sha256:" + "f" * 64
prepared.write("import-receipt.json", receipt)
with pytest.raises(controller.DeployError):
controller.mcp_hub_load_release_candidate(
prepared.project, prepared.revision
)
assert prepared.checks == []
@pytest.mark.parametrize(
@@ -435,7 +502,8 @@ def test_compose_operations_pin_all_scanned_ids_without_changing_source_model(
def compose(project):
path = Path(project["_mcp_hub_image_override_file"])
assert path.stat().st_mode & 0o777 == 0o600
if controller.os.name != "nt":
assert path.stat().st_mode & 0o777 == 0o600
observed.append((path, json.loads(path.read_text())))
return prepared.directory, ["docker", "compose", "-f", str(path)]
@@ -775,6 +843,11 @@ def test_actual_high_findings_are_bound_to_current_service_tags(controller, prep
}
prepared.candidate["summarySha256"] = prepared.write("scan-summary.json", summary)
candidate_sha = prepared.write("candidate.json", prepared.candidate)
receipt = json.loads(
(prepared.directory / "import-receipt.json").read_text(encoding="utf-8")
)
receipt["candidateSha256"] = candidate_sha
prepared.write("import-receipt.json", receipt)
prepared.write(
"approval.json",
{
@@ -804,6 +877,11 @@ def test_fixable_high_findings_cannot_be_approved_around(controller, prepared):
"vulnerability-web.json", report
)
candidate_sha = prepared.write("candidate.json", prepared.candidate)
receipt = json.loads(
(prepared.directory / "import-receipt.json").read_text(encoding="utf-8")
)
receipt["candidateSha256"] = candidate_sha
prepared.write("import-receipt.json", receipt)
prepared.write(
"approval.json",
{
@@ -30,7 +30,7 @@ def _set_npm_config(
settings.apis["NPM_URL"] = url
settings.apis["NPM_IDENTITY"] = identity
settings.apis["NPM_SECRET"] = secret
settings.apis["NPM_VERIFY_TLS"] = False
settings.apis["NPM_VERIFY_TLS"] = True
settings.apis["NPM_TIMEOUT_SECONDS"] = "10"
return previous
+503
View File
@@ -0,0 +1,503 @@
from __future__ import annotations
import pytest
@pytest.fixture
def npm_settings(monkeypatch):
from backend.app.core.config import settings
previous = dict(settings.apis)
settings.apis.update(
{
"NPM_URL": "https://npm.example.test",
"NPM_IDENTITY": "operator@example.test",
"NPM_SECRET": "fixture password value",
"NPM_VERIFY_TLS": True,
"NPM_TIMEOUT_SECONDS": "3",
}
)
yield settings.apis
settings.apis.clear()
settings.apis.update(previous)
@pytest.mark.parametrize(
("url", "verify_tls", "reason"),
[
("http://npm.example.test", True, "npm_https_required"),
("https://user:fixture-password@npm.example.test", True, "npm_url_invalid"),
("https://npm.example.test/?token=fixture-password", True, "npm_url_invalid"),
("https://npm.example.test/#fixture-password", True, "npm_url_invalid"),
("https://npm.example.test:", True, "npm_url_invalid"),
("https://npm.example.test/base/../api", True, "npm_url_invalid"),
("https://[broken", True, "npm_url_invalid"),
("https://npm.example.test", False, "npm_tls_verification_required"),
],
)
def test_unsafe_npm_transport_is_rejected_before_client_construction(
npm_settings, monkeypatch, url, verify_tls, reason
):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
npm_settings["NPM_URL"] = url
npm_settings["NPM_VERIFY_TLS"] = verify_tls
client_calls = []
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client",
lambda **kwargs: client_calls.append(kwargs),
)
connector = NginxProxyManagerConnector()
with pytest.raises(RuntimeError, match=f"^{reason}$") as error:
connector._login()
assert str(error.value) == reason
assert client_calls == []
assert "fixture password value" not in str(error.value)
assert "npm.example.test" not in str(error.value)
def test_missing_verify_setting_defaults_to_verified_https(npm_settings, monkeypatch):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
npm_settings.pop("NPM_VERIFY_TLS")
clients = []
class Response:
status_code = 200
def json(self):
return {"token": "fixture bearer"}
class Client:
def __init__(self, **kwargs):
clients.append(kwargs)
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def post(self, *_args, **_kwargs):
return Response()
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client", Client
)
connector = NginxProxyManagerConnector()
assert connector._login() == "fixture bearer"
assert clients == [{"timeout": 3.0, "verify": True, "follow_redirects": False}]
def test_invalid_url_is_not_echoed_in_health_metadata(npm_settings, monkeypatch):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
npm_settings["NPM_URL"] = (
"https://operator:fixture-password@npm.example.test/?token=fixture-password"
)
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client",
lambda **_kwargs: pytest.fail("invalid URL must be rejected before HTTP"),
)
result = NginxProxyManagerConnector().health()
assert result["status"] == "unreachable"
assert result["message"] == "npm_url_invalid"
assert result["url"] == ""
assert "fixture-password" not in str(result)
assert "operator@npm.example.test" not in str(result)
def test_login_and_api_request_preserve_base_path_without_following_redirects(
npm_settings, monkeypatch
):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
npm_settings["NPM_URL"] = "https://npm.example.test/manager/"
calls = []
class Response:
def __init__(self, status_code, payload, headers=None):
self.status_code = status_code
self._payload = payload
self.headers = headers or {}
def json(self):
return self._payload
class Client:
def __init__(self, **kwargs):
assert kwargs == {
"timeout": 3.0,
"verify": True,
"follow_redirects": False,
}
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def post(self, url, **kwargs):
calls.append(("POST", url, kwargs))
return Response(200, {"token": "fixture bearer"})
def request(self, method, url, **kwargs):
calls.append((method, url, kwargs))
return Response(200, {"ok": True})
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client", Client
)
connector = NginxProxyManagerConnector()
assert connector._request("GET", "/nginx/proxy-hosts") == {"ok": True}
assert [call[:2] for call in calls] == [
("POST", "https://npm.example.test/manager/api/tokens"),
("GET", "https://npm.example.test/manager/api/nginx/proxy-hosts"),
]
assert "Authorization" not in calls[0][2].get("headers", {})
def test_redirect_responses_are_not_followed_or_reported_as_success(
npm_settings, monkeypatch
):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
calls = []
class Response:
def __init__(self):
self.status_code = 302
self.headers = {"location": "https://attacker.example.test/collect"}
class Client:
def __init__(self, **kwargs):
assert kwargs["follow_redirects"] is False
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def post(self, url, **kwargs):
calls.append((url, kwargs))
return Response()
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client", Client
)
connector = NginxProxyManagerConnector()
with pytest.raises(RuntimeError, match="^npm_redirect_denied$"):
connector._login()
assert len(calls) == 1
assert calls[0][0] == "https://npm.example.test/api/tokens"
assert calls[0][1]["json"]["secret"] == "fixture password value"
def test_api_redirect_does_not_forward_bearer_token(npm_settings, monkeypatch):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
calls = []
class Response:
def __init__(self, status_code, payload=None):
self.status_code = status_code
self._payload = payload
self.headers = {"location": "https://attacker.example.test/collect"}
def json(self):
return self._payload
class Client:
def __init__(self, **kwargs):
assert kwargs["follow_redirects"] is False
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def post(self, url, **kwargs):
calls.append(("POST", url, kwargs))
return Response(200, {"token": "fixture bearer"})
def request(self, method, url, **kwargs):
calls.append((method, url, kwargs))
return Response(302)
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client", Client
)
connector = NginxProxyManagerConnector()
with pytest.raises(RuntimeError, match="^npm_redirect_denied$"):
connector._request("GET", "/nginx/proxy-hosts", follow_redirects=True)
assert [call[:2] for call in calls] == [
("POST", "https://npm.example.test/api/tokens"),
("GET", "https://npm.example.test/api/nginx/proxy-hosts"),
]
assert calls[1][2]["headers"]["Authorization"] == "Bearer fixture bearer"
assert calls[1][2]["follow_redirects"] is False
def test_login_error_body_cannot_echo_configured_secret(npm_settings, monkeypatch):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
class Response:
status_code = 500
text = "login failed for fixture password value"
class Client:
def __init__(self, **_kwargs):
pass
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def post(self, *_args, **_kwargs):
return Response()
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client", Client
)
connector = NginxProxyManagerConnector()
with pytest.raises(RuntimeError, match="^npm_api_error$") as error:
connector._login()
assert "fixture password value" not in str(error.value)
assert connector.last_error == "npm_api_error"
def test_cached_token_is_cleared_before_unsafe_transport_reuse(npm_settings, monkeypatch):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
connector = NginxProxyManagerConnector()
connector._config()
connector._runtime_token = "stale fixture bearer"
connector._read_cache["proxy_hosts"] = {"success": True, "items": ["stale"]}
npm_settings["NPM_URL"] = "http://npm.example.test"
client_calls = []
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client",
lambda **kwargs: client_calls.append(kwargs),
)
with pytest.raises(RuntimeError, match="^npm_https_required$"):
connector._login()
assert connector._runtime_token is None
assert connector._read_cache == {}
assert client_calls == []
@pytest.mark.parametrize(
"changed_key,changed_value",
[
("NPM_URL", "https://other.example.test"),
("NPM_IDENTITY", "different@example.test"),
("NPM_SECRET", "different fixture password"),
],
)
def test_cached_token_and_read_cache_are_bound_to_transport_config(
npm_settings, monkeypatch, changed_key, changed_value
):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
connector = NginxProxyManagerConnector()
connector._config()
connector._runtime_token = "cached fixture bearer"
connector._read_cache["proxy_hosts"] = {"success": True, "items": ["old"]}
npm_settings[changed_key] = changed_value
requested = []
monkeypatch.setattr(
connector,
"_request",
lambda method, path, **_kwargs: requested.append((method, path)) or [],
)
result = connector.proxy_hosts()
assert result["items"] == []
assert requested == [("GET", "/nginx/proxy-hosts")]
assert connector._runtime_token is None
assert connector._read_cache["proxy_hosts"] is result
def test_disabled_tls_clears_cached_state_before_read_cache_return(
npm_settings, monkeypatch
):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
connector = NginxProxyManagerConnector()
connector._config()
connector._runtime_token = "cached fixture bearer"
connector._read_cache["proxy_hosts"] = {"success": True, "items": ["old"]}
npm_settings["NPM_VERIFY_TLS"] = False
monkeypatch.setattr(
connector,
"_request",
lambda *_args, **_kwargs: pytest.fail("unsafe transport must not request"),
)
result = connector.proxy_hosts()
assert result["success"] is False
assert result["message"] == "npm_tls_verification_required"
assert connector._runtime_token is None
assert connector._read_cache == {}
def test_malformed_tls_setting_is_rejected_and_url_metadata_is_hidden(
npm_settings, monkeypatch
):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
npm_settings["NPM_VERIFY_TLS"] = "maybe"
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client",
lambda **_kwargs: pytest.fail("invalid TLS setting must be rejected before HTTP"),
)
result = NginxProxyManagerConnector().health()
assert result["status"] == "unreachable"
assert result["message"] == "npm_config_invalid"
assert result["url"] == ""
@pytest.mark.parametrize(
"changed_key,changed_value",
[
("NPM_URL", "https://other.example.test"),
("NPM_IDENTITY", "different@example.test"),
("NPM_SECRET", "different fixture password"),
],
)
def test_changed_credentials_require_a_new_login(
npm_settings, monkeypatch, changed_key, changed_value
):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
token_calls = []
token_urls = []
class Response:
status_code = 200
def __init__(self, token):
self._token = token
def json(self):
return {"token": self._token}
class Client:
def __init__(self, **kwargs):
assert kwargs["verify"] is True
assert kwargs["follow_redirects"] is False
def __enter__(self):
return self
def __exit__(self, *_args):
return False
def post(self, url, **kwargs):
token_urls.append(url)
token_calls.append(kwargs["json"])
return Response(f"fixture-token-{len(token_calls)}")
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client", Client
)
connector = NginxProxyManagerConnector()
assert connector._login() == "fixture-token-1"
npm_settings[changed_key] = changed_value
assert connector._login() == "fixture-token-2"
assert len(token_calls) == 2
assert token_urls[0] == "https://npm.example.test/api/tokens"
expected_url = npm_settings["NPM_URL"].rstrip("/") + "/api/tokens"
assert token_urls[1] == expected_url
assert token_calls[1]["identity"] == npm_settings["NPM_IDENTITY"]
assert token_calls[1]["secret"] == npm_settings["NPM_SECRET"]
def test_missing_config_rejects_before_client_even_with_cached_token(
npm_settings, monkeypatch
):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
connector = NginxProxyManagerConnector()
connector._config()
connector._runtime_token = "stale fixture bearer"
npm_settings["NPM_URL"] = None
client_calls = []
monkeypatch.setattr(
"backend.app.connectors.nginx_proxy_manager.httpx.Client",
lambda **kwargs: client_calls.append(kwargs),
)
with pytest.raises(RuntimeError, match="^missing_config$"):
connector._login()
assert connector._runtime_token is None
assert client_calls == []
def test_missing_config_check_clears_cached_context_before_configuration_returns(
npm_settings,
):
from backend.app.connectors.nginx_proxy_manager import NginxProxyManagerConnector
connector = NginxProxyManagerConnector()
connector._config()
connector._runtime_token = "stale fixture bearer"
connector._read_cache["proxy_hosts"] = {"success": True, "items": []}
npm_settings["NPM_SECRET"] = None
assert connector.is_configured() is False
assert connector._runtime_token is None
assert connector._read_cache == {}
def test_missing_npm_tls_environment_default_does_not_inherit_global_disable(
monkeypatch,
):
import builtins
from backend.app.core import config
monkeypatch.setenv("APPOPS_VERIFY_TLS", "false")
monkeypatch.delenv("NPM_VERIFY_TLS", raising=False)
for name in (
"APPOPS_MCP_TOKEN_FILE",
"APPOPS_N8N_TOKEN_FILE",
"APPOPS_SECRET_KEY_FILE",
):
monkeypatch.delenv(name, raising=False)
monkeypatch.setattr(config, "_secret_value", lambda *_args: None)
monkeypatch.setattr(
builtins,
"open",
lambda *_args, **_kwargs: pytest.fail("config test must not read secret files"),
)
configured = config.Settings()
assert configured.verify_tls is False
assert configured.apis["NPM_VERIFY_TLS"] is True
@@ -0,0 +1,366 @@
from __future__ import annotations
import importlib.util
import os
import subprocess
from pathlib import Path
from types import SimpleNamespace
import pytest
MODULE = (
Path(__file__).resolve().parents[2]
/ "ops/gitea-deploy-broker/registered_build_context.py"
)
def load():
spec = importlib.util.spec_from_file_location(
"registered_build_context_test", MODULE
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def git(path, *args):
return (
subprocess.run(
["git", "-C", str(path), *args],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
.stdout.decode()
.strip()
)
@pytest.fixture
def inputs(tmp_path):
checkout = tmp_path / "working"
checkout.mkdir()
git(checkout, "init", "-q")
git(checkout, "config", "user.email", "test@example.invalid")
git(checkout, "config", "user.name", "Test")
(checkout / "server").mkdir()
(checkout / "server/Dockerfile").write_text("FROM scratch\nCOPY . /app\n")
(checkout / "server/main.py").write_text("print('protected')\n")
(checkout / "run.sh").write_text("#!/bin/sh\nexit 0\n")
(checkout / "run.sh").chmod(0o755)
git(checkout, "add", ".")
git(checkout, "update-index", "--chmod=+x", "run.sh")
git(checkout, "commit", "-qm", "protected source")
revision = git(checkout, "rev-parse", "HEAD")
bare = tmp_path / "repo.git"
subprocess.run(
["git", "clone", "--bare", "--quiet", str(checkout), str(bare)], check=True
)
live = tmp_path / "live"
live.mkdir()
(live / "server").mkdir()
(live / "server/stale.py").write_text("untracked")
(live / ".env").write_text("PRIVATE_VALUE=do-not-copy")
project = {"slug": "quiz-monitor", "source_dir": str(live), "compose_dir": ""}
model = {
"services": {
"quiz-server": {
"build": {"context": str(live), "dockerfile": "server/Dockerfile"}
}
}
}
return bare, revision, project, model, tmp_path / "state", live, checkout
def prepare(module, inputs):
return module.prepare_build_contexts(*inputs[:5])
def test_exact_archive_omits_live_untracked_files_and_preserves_live_tree(inputs):
result = prepare(load(), inputs)
tree = Path(result["quiz-server"])
assert tree.is_absolute()
assert (tree / "server/main.py").read_text() == "print('protected')\n"
assert not (tree / "server/stale.py").exists()
assert not (tree / ".env").exists()
assert (inputs[5] / "server/stale.py").read_text() == "untracked"
assert (inputs[5] / ".env").read_text() == "PRIVATE_VALUE=do-not-copy"
assert (tree.parent / "manifest.json").is_file()
assert not (tree / "manifest.json").exists()
assert prepare(load(), inputs) == result
if os.name != "nt":
assert tree.parent.stat().st_mode & 0o777 == 0o700
assert (tree / "run.sh").stat().st_mode & 0o777 == 0o755
@pytest.mark.parametrize("change", ["content", "extra", "missing", "mode", "manifest"])
def test_cached_context_drift_fails_without_replacing_source(inputs, change):
module = load()
tree = Path(prepare(module, inputs)["quiz-server"])
if change == "content":
(tree / "server/main.py").write_text("tampered")
elif change == "extra":
(tree / "untracked.py").write_text("tampered")
elif change == "missing":
(tree / "server/main.py").unlink()
elif change == "mode":
if os.name == "nt":
pytest.skip("POSIX permissions are verified on Linux")
(tree / "run.sh").chmod(0o644)
else:
(tree.parent / "manifest.json").write_text("{}")
with pytest.raises(module.RegisteredBuildContextError):
prepare(module, inputs)
assert (inputs[5] / "server/stale.py").is_file()
@pytest.mark.parametrize(
"build",
[
{"context": "../outside", "dockerfile": "Dockerfile"},
{"context": "https://example.invalid/source.git"},
{"context": ".", "dockerfile": "../outside/Dockerfile"},
{"context": ".", "dockerfile": "not-tracked"},
{"context": ".", "additional_contexts": {"other": "/elsewhere"}},
{"context": ".", "dockerfile_inline": "FROM scratch"},
],
)
def test_external_or_untracked_build_inputs_are_rejected(inputs, build):
module = load()
inputs[3]["services"]["quiz-server"]["build"] = build
with pytest.raises(module.RegisteredBuildContextError):
prepare(module, inputs)
def test_subdirectory_context_is_remapped_with_exact_revision(inputs):
inputs[3]["services"]["quiz-server"]["build"] = {
"context": "server",
"dockerfile": "Dockerfile",
}
result = prepare(load(), inputs)
assert Path(result["quiz-server"]).name == "server"
assert (Path(result["quiz-server"]) / "main.py").is_file()
def test_revision_is_not_a_branch_and_old_archive_survives_new_commit(inputs):
module = load()
result = prepare(module, inputs)
inputs[6].joinpath("server/main.py").write_text("changed main")
git(inputs[6], "commit", "-qam", "advance main")
git(inputs[0], "fetch", "--quiet", str(inputs[6]), "+HEAD:refs/heads/main")
assert prepare(module, inputs) == result
assert (
Path(result["quiz-server"]).joinpath("server/main.py").read_text()
== "print('protected')\n"
)
with pytest.raises(module.RegisteredBuildContextError):
module.prepare_build_contexts(inputs[0], "main", *inputs[2:5])
@pytest.mark.skipif(os.name == "nt", reason="POSIX symlink boundary")
def test_symlinked_live_context_or_archived_file_is_rejected(inputs):
module = load()
(inputs[5] / "link").symlink_to(inputs[5] / "server", target_is_directory=True)
inputs[3]["services"]["quiz-server"]["build"] = {
"context": "link",
"dockerfile": "Dockerfile",
}
with pytest.raises(module.RegisteredBuildContextError):
prepare(module, inputs)
inputs[6].joinpath("tracked-link").symlink_to("server/main.py")
git(inputs[6], "add", "tracked-link")
git(inputs[6], "commit", "-qm", "unsupported tracked symlink")
revision = git(inputs[6], "rev-parse", "HEAD")
git(inputs[0], "fetch", "--quiet", str(inputs[6]), "+HEAD:refs/heads/main")
inputs[3]["services"]["quiz-server"]["build"] = {
"context": ".",
"dockerfile": "server/Dockerfile",
}
with pytest.raises(module.RegisteredBuildContextError):
module.prepare_build_contexts(inputs[0], revision, *inputs[2:5])
@pytest.mark.parametrize(
"field", ["secrets", "ssh", "additional_contexts", "dockerfile_inline"]
)
def test_build_cannot_request_host_credentials(inputs, field):
module = load()
inputs[3]["services"]["quiz-server"]["build"][field] = ["default"]
with pytest.raises(module.RegisteredBuildContextError):
prepare(module, inputs)
def test_tracked_runtime_env_is_rejected_before_cache_creation(inputs):
module = load()
inputs[6].joinpath(".env").write_text("PRIVATE_CONFIG=value")
git(inputs[6], "add", ".env")
git(inputs[6], "commit", "-qm", "unsupported runtime env")
revision = git(inputs[6], "rev-parse", "HEAD")
git(inputs[0], "fetch", "--quiet", str(inputs[6]), "+HEAD:refs/heads/main")
with pytest.raises(module.RegisteredBuildContextError):
module.prepare_build_contexts(inputs[0], revision, *inputs[2:5])
assert not inputs[4].exists()
def test_git_export_attributes_cannot_change_exact_archive_bytes(inputs):
module = load()
inputs[6].joinpath(".gitattributes").write_text("server/main.py export-ignore\n")
git(inputs[6], "add", ".gitattributes")
git(inputs[6], "commit", "-qm", "unsupported export omission")
revision = git(inputs[6], "rev-parse", "HEAD")
git(inputs[0], "fetch", "--quiet", str(inputs[6]), "+HEAD:refs/heads/main")
with pytest.raises(module.RegisteredBuildContextError):
module.prepare_build_contexts(inputs[0], revision, *inputs[2:5])
assert not inputs[4].joinpath("registered-builds/quiz-monitor", revision).exists()
@pytest.mark.skipif(os.name == "nt", reason="POSIX symlink boundary")
def test_cached_directory_link_is_rejected_without_touching_link_target(inputs):
module = load()
tree = Path(prepare(module, inputs)["quiz-server"])
tree.rename(tree.parent / "original")
tree.symlink_to(inputs[5], target_is_directory=True)
with pytest.raises(module.RegisteredBuildContextError):
prepare(module, inputs)
assert (inputs[5] / ".env").is_file()
def test_tree_size_bound_prevents_archive_execution(inputs, monkeypatch):
module = load()
monkeypatch.setattr(module, "MAX_BYTES", 1)
with pytest.raises(module.RegisteredBuildContextError, match="bound_exceeded"):
prepare(module, inputs)
assert not inputs[4].exists()
@pytest.mark.skipif(
os.name == "nt" or os.getuid() != 0,
reason="observed Linux read-only Gitea mount ownership",
)
def test_gitea_owned_readonly_repository_uses_exact_safe_directory(inputs, monkeypatch):
module = load()
os.chown(inputs[0], 1000, 1000)
commands = []
original = module.subprocess.Popen
def spy(command, **kwargs):
commands.append(command)
return original(command, **kwargs)
monkeypatch.setattr(module.subprocess, "Popen", spy)
result = prepare(module, inputs)
assert Path(result["quiz-server"]).joinpath("server/main.py").is_file()
assert commands
assert all(
command[:4]
== ["git", "--no-replace-objects", "-c", f"safe.directory={inputs[0]}"]
for command in commands
)
@pytest.mark.skipif(
os.name == "nt" or os.getuid() != 0, reason="Linux repository UID boundary"
)
@pytest.mark.parametrize("owner,mode", [(1001, 0o755), (1000, 0o775), (0, 0o777)])
def test_unexpected_or_writable_repository_is_rejected(inputs, owner, mode):
module = load()
os.chown(inputs[0], owner, owner)
inputs[0].chmod(mode)
with pytest.raises(module.RegisteredBuildContextError):
prepare(module, inputs)
assert not inputs[4].exists()
def test_windows_path_identity_uses_creation_time_not_inconsistent_ctime(monkeypatch):
module = load()
monkeypatch.setattr(module, "os", SimpleNamespace(name="nt"))
descriptor = SimpleNamespace(
st_dev=1,
st_ino=2,
st_size=3,
st_mtime_ns=40,
st_ctime_ns=40,
st_birthtime_ns=30,
)
named = SimpleNamespace(**{**vars(descriptor), "st_ctime_ns": 30})
assert module._path_identity(descriptor) == module._path_identity(named)
named.st_ino = 9
assert module._path_identity(descriptor) != module._path_identity(named)
def test_posix_path_identity_preserves_change_time(monkeypatch):
module = load()
monkeypatch.setattr(module, "os", SimpleNamespace(name="posix"))
descriptor = SimpleNamespace(
st_dev=1,
st_ino=2,
st_size=3,
st_mtime_ns=40,
st_ctime_ns=40,
)
named = SimpleNamespace(**{**vars(descriptor), "st_ctime_ns": 41})
assert module._path_identity(descriptor) != module._path_identity(named)
def test_descriptor_metadata_changed_during_read_is_rejected(tmp_path, monkeypatch):
module = load()
path = tmp_path / "tracked"
raw = b"tracked bytes"
path.write_bytes(raw)
path.chmod(0o644)
original = module.os.fstat
calls = 0
def changed(descriptor):
nonlocal calls
calls += 1
info = original(descriptor)
if calls == 1:
return info
fields = {
name: getattr(info, name) for name in dir(info) if name.startswith("st_")
}
fields["st_ctime_ns"] += 1
return SimpleNamespace(**fields)
monkeypatch.setattr(module.os, "fstat", changed)
with pytest.raises(module.RegisteredBuildContextError, match="file_changed"):
module._file(
path, {"size": len(raw), "oid": module._blob_digest(raw), "mode": 0o644}
)
def test_git_replacement_refs_cannot_substitute_other_commit_contents(inputs):
checkout, bare, protected = inputs[6], inputs[0], inputs[1]
checkout.joinpath("server/main.py").write_text("replacement commit bytes\n")
git(checkout, "commit", "-qam", "replacement commit")
other = git(checkout, "rev-parse", "HEAD")
git(bare, "fetch", "--quiet", str(checkout), "HEAD")
git(bare, "replace", protected, other)
assert (
git(bare, "show", f"{protected}:server/main.py") == "replacement commit bytes"
)
tree = Path(prepare(load(), inputs)["quiz-server"])
assert tree.joinpath("server/main.py").read_text() == "print('protected')\n"
@pytest.mark.parametrize(
"name", ["C:/outside.txt", "D:outside.txt", "server/file:stream"]
)
def test_git_manifest_rejects_windows_drive_and_stream_paths_before_extraction(
tmp_path, monkeypatch, name
):
module = load()
revision, tree, blob = "a" * 40, "b" * 40, "c" * 40
responses = iter(
[
f"{revision}\n".encode(),
f"{tree}\n".encode(),
f"100644 blob {blob} 1\t{name}\0".encode(),
]
)
monkeypatch.setattr(module, "_git_output", lambda *_args: next(responses))
with pytest.raises(module.RegisteredBuildContextError, match="path_invalid"):
module._manifest(tmp_path, revision)
assert not list(tmp_path.iterdir())
@@ -0,0 +1,174 @@
from __future__ import annotations
import json
import subprocess
from types import SimpleNamespace
import pytest
from test_deploy_broker import BROKER_ROOT, _load_module
@pytest.mark.parametrize("repository", ["jens/quiz-monitor", "jens/another-registered-app"])
def test_verified_registered_build_uses_controller_metadata(repository, monkeypatch):
module = _load_module("registered_build_metadata", BROKER_ROOT / "deploy.py")
revision = "a" * 40
metadata = {"repository": repository, "revision": revision, "tree": "b" * 40,
"id": "protected-build-42", "builtAt": "2026-10-07T00:00:00Z"}
monkeypatch.setenv("APPOPS_SOURCE_TREE", "stale-inherited-tree")
project = {"compose_project": "existing-project", "verify_oci_revision": True,
"environment": {"APPOPS_SOURCE_TREE": "wrong-tree", "APPOPS_BUILD_ID": "wrong-id",
"APPOPS_BUILD_DATE": "wrong-date", "USER_SETTING": "preserved$literal"}}
actual = module.deployment_environment(project, revision, repository, metadata)
assert {key: actual[key] for key in (
"APPOPS_SOURCE_REVISION", "APPOPS_SOURCE_REPOSITORY", "APPOPS_SOURCE_TREE",
"APPOPS_BUILD_ID", "APPOPS_BUILD_DATE", "COMPOSE_PROJECT_NAME", "USER_SETTING",
)} == {"APPOPS_SOURCE_REVISION": revision, "APPOPS_SOURCE_REPOSITORY": repository,
"APPOPS_SOURCE_TREE": metadata["tree"], "APPOPS_BUILD_ID": metadata["id"],
"APPOPS_BUILD_DATE": metadata["builtAt"], "COMPOSE_PROJECT_NAME": "existing-project",
"USER_SETTING": "preserved$literal"}
def test_unverified_target_retains_existing_environment_overrides():
module = _load_module("unverified_build_metadata", BROKER_ROOT / "deploy.py")
project = {"compose_project": "existing-project", "environment": {
"APPOPS_SOURCE_TREE": "existing-tree", "APPOPS_BUILD_ID": "existing-id",
"APPOPS_BUILD_DATE": "existing-date"}}
actual = module.deployment_environment(project, "a" * 40, "jens/another-app")
assert actual["APPOPS_SOURCE_TREE"] == "existing-tree"
assert actual["APPOPS_BUILD_ID"] == "existing-id"
assert actual["APPOPS_BUILD_DATE"] == "existing-date"
def test_unbound_target_does_not_enter_legacy_build_adoption(monkeypatch):
module = _load_module("unbound_build_metadata", BROKER_ROOT / "deploy.py")
monkeypatch.setattr(module, "compose_command", lambda *_: pytest.fail("unbound target rendered"))
module.prepare_registered_legacy_build({}, "a" * 40, "jens/quiz-monitor", {})
def test_adopted_target_environment_excludes_controller_secrets(monkeypatch):
module = _load_module("isolated_legacy_environment", BROKER_ROOT / "deploy.py")
monkeypatch.setenv("UNRELATED_CONTROLLER_CREDENTIAL", "private-controller-value")
project = {"compose_project": "quiz-monitor", "_registered_legacy_request_id":
"6f9619ff-8b86-4011-b42d-00c04fc964ff", "environment": {
"OPENRGB_HOST": "preserved-value"}}
environment = module.deployment_environment(project, "a" * 40, "jens/quiz-monitor")
assert "UNRELATED_CONTROLLER_CREDENTIAL" not in environment
assert environment["OPENRGB_HOST"] == "preserved-value"
assert environment["APPOPS_SOURCE_REVISION"] == "a" * 40
def test_legacy_build_binds_archive_and_private_runtime_configuration(tmp_path, monkeypatch):
module = _load_module("legacy_build_metadata", BROKER_ROOT / "deploy.py")
revision = "a" * 40
repository = "jens/quiz-monitor"
project = {"_registered_legacy_request_id": "6f9619ff-8b86-4011-b42d-00c04fc964ff",
"_repository_path": str(tmp_path / "repository.git")}
model = {"services": {"quiz-server": {"build": {"context": str(tmp_path)}}}}
contexts = {"quiz-server": str(tmp_path / "verified-archive")}
environment = {"OPENRGB_HOST": "preserved-value", "APPOPS_SOURCE_REVISION": revision}
observed = {}
def archive(repo_path, target_revision, target_project, rendered_model, source_root):
observed["archive"] = (repo_path, target_revision, target_project, rendered_model, source_root)
return contexts
def configuration(repo, target_project, request_id, target_revision, rendered_model,
context_map, supplied_environment, **kwargs):
observed["configuration"] = (repo, request_id, target_revision, rendered_model,
context_map, supplied_environment, kwargs)
return {"overlayPath": str(tmp_path / "private-compose.json")}
monkeypatch.setattr(module, "compose_command", lambda *_: (tmp_path, ["docker", "compose"]))
monkeypatch.setattr(module.subprocess, "run", lambda *_, **__: SimpleNamespace(
returncode=0, stdout=json.dumps(model)))
monkeypatch.setattr(module, "_load_registered_build_context", lambda: SimpleNamespace(
prepare_build_contexts=archive))
monkeypatch.setattr(module, "_load_registered_legacy_adoption", lambda: SimpleNamespace(
prepare_build_configuration=configuration))
monkeypatch.setattr(module, "_registered_legacy_receipt_key", lambda: b"k" * 32)
module.prepare_registered_legacy_build(project, revision, repository, environment)
assert observed["archive"][1:] == (revision, project, model, module.EXACT_SOURCE_ROOT)
assert observed["configuration"][:6] == (
repository, project["_registered_legacy_request_id"], revision, model, contexts, environment)
assert project["_registered_legacy_compose_override_file"] == str(tmp_path / "private-compose.json")
assert project["_registered_legacy_compose_environment"] == environment
assert project["_registered_legacy_compose_environment"] is not environment
def test_failed_source_render_cannot_prepare_archive_or_runtime(tmp_path, monkeypatch):
module = _load_module("failed_render_build_metadata", BROKER_ROOT / "deploy.py")
project = {"_registered_legacy_request_id": "6f9619ff-8b86-4011-b42d-00c04fc964ff"}
monkeypatch.setattr(module, "compose_command", lambda *_: (tmp_path, ["docker", "compose"]))
monkeypatch.setattr(module.subprocess, "run", lambda *_, **__: SimpleNamespace(returncode=1, stdout=""))
monkeypatch.setattr(module, "_load_registered_build_context", lambda: pytest.fail("unsafe archive preparation"))
with pytest.raises(module.DeployError, match="could not be rendered"):
module.prepare_registered_legacy_build(project, "a" * 40, "jens/quiz-monitor", {})
def test_registered_metadata_and_source_ignore_git_replacement_refs(tmp_path, monkeypatch):
module = _load_module("registered_original_git_objects", BROKER_ROOT / "deploy.py")
monkeypatch.delenv("GIT_NO_REPLACE_OBJECTS", raising=False)
def git(*arguments):
return subprocess.check_output(["git", "-C", str(tmp_path), *arguments], text=True).strip()
git("init", "-b", "main")
git("config", "user.name", "Test")
git("config", "user.email", "test@example.invalid")
tracked = tmp_path / "source.txt"
tracked.write_text("protected original\n")
git("add", "source.txt")
git("commit", "-qm", "protected source")
protected, original_tree = git("rev-parse", "HEAD"), git("rev-parse", "HEAD^{tree}")
tracked.write_text("substituted source\n")
git("commit", "-qam", "replacement source")
replacement = git("rev-parse", "HEAD")
git("reset", "--hard", protected)
git("replace", protected, replacement)
assert git("show", f"{protected}:source.txt") == "substituted source"
monkeypatch.setenv("APPOPS_REGISTERED_LEGACY_REQUEST_ID", "6f9619ff-8b86-4011-b42d-00c04fc964ff")
# Restore the process variable after testing the production binding.
monkeypatch.setenv("GIT_NO_REPLACE_OBJECTS", "0")
project = {}
module.bind_registered_legacy_request(project, "deploy")
assert module.validate_commit(tmp_path / ".git", "main", protected) == protected
assert module.deployment_metadata(tmp_path / ".git", protected, "jens/quiz-monitor")["tree"] == original_tree
assert git("show", f"{protected}:source.txt") == "protected original"
@pytest.mark.parametrize("mode,request_id", [("rollback", "6f9619ff-8b86-4011-b42d-00c04fc964ff"),
("deploy", "invalid")])
def test_invalid_registered_binding_cannot_change_git_reads(monkeypatch, mode, request_id):
module = _load_module("invalid_registered_git_binding", BROKER_ROOT / "deploy.py")
monkeypatch.setenv("APPOPS_REGISTERED_LEGACY_REQUEST_ID", request_id)
monkeypatch.delenv("GIT_NO_REPLACE_OBJECTS", raising=False)
with pytest.raises(module.DeployError, match="binding is invalid"):
module.bind_registered_legacy_request({}, mode)
assert "GIT_NO_REPLACE_OBJECTS" not in module.os.environ
def test_registered_compose_promotion_keeps_sealed_local_image(tmp_path, monkeypatch):
module = _load_module("registered_no_mutable_pull", BROKER_ROOT / "deploy.py")
project = {"source_dir": str(tmp_path), "_registered_legacy_request_id":
"6f9619ff-8b86-4011-b42d-00c04fc964ff"}
commands, events = [], []
monkeypatch.setattr(module, "deployment_environment", lambda *_: {})
monkeypatch.setattr(module, "prepare_registered_legacy_build", lambda *_: None)
monkeypatch.setattr(module, "compose_command", lambda *_: (tmp_path, ["docker", "compose"]))
monkeypatch.setattr(module, "prepare_compose_migration", lambda *_: [])
monkeypatch.setattr(module, "workmesh_transition_enabled", lambda *_: False)
monkeypatch.setattr(module, "_registered_legacy_receipt_key", lambda: b"k" * 32)
monkeypatch.setattr(module, "_load_registered_legacy_adoption", lambda: SimpleNamespace(
prepare_candidate_deployment=lambda *_, **__: events.append("sealed"),
verify_candidate_pre_mutation=lambda *_, **__: events.append("checked"),
record_candidate_runtime=lambda *_, **__: events.append("observed")))
def run(command, **_):
commands.append(command)
if "up" in command:
assert events == ["sealed", "checked"]
monkeypatch.setattr(module, "run", run)
module.deploy_project(project, "a" * 40, "jens/quiz-monitor")
assert commands[-1] == ["docker", "compose", "up", "--detach", "--no-build", "--pull", "never"]
assert events[-1] == "observed"
@@ -0,0 +1,389 @@
from __future__ import annotations
import copy
import hashlib
import importlib.util
import json
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
MODULE_PATH = ROOT / "ops" / "gitea-deploy-broker" / "registered_legacy_adoption.py"
REPOSITORY = "jens/quiz-monitor"
REQUEST_ID = "6f9619ff-8b86-4011-b42d-00c04fc964ff"
TARGET_SHA = "a" * 40
KEY = b"k" * 32
def _load_module():
spec = importlib.util.spec_from_file_location("registered_legacy_adoption_test", MODULE_PATH)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.fixture
def module():
return _load_module()
@pytest.fixture
def project():
return {
"slug": "quiz-monitor",
"branch": "main",
"compose_project": "quiz-monitor",
"compose_files": ["docker-compose.yml"],
"containers": ["quiz-monitor-quiz-server-1"],
"source_dir": "/srv/example/appdata/quiz-monitor/source",
"verify_oci_revision": True,
}
@pytest.fixture
def baseline_inputs():
state_bytes = b'{"schema_version":3,"live_revision":"0c9c1747","live_status":"unknown"}\n'
image_id = "sha256:" + "a" * 64
observation = {
"sourceFingerprint": "1" * 64,
"runtimeFingerprint": "2" * 64,
"imageIDs": {"quiz-monitor-quiz-server-1": image_id},
"runtimeConfiguration": {
"environment": {"OPENRGB_HOST": "198.19.1.2", "OPENRGB_TOKEN": "private-test-value"},
"composeModel": {"services": {"quiz-server": {"ports": ["1205:8000"]}}},
},
"artifacts": {
"containers": {
"quiz-monitor-quiz-server-1": {
"image_id": image_id,
"container_id": "b" * 64,
"image_ref": "quiz-monitor:latest",
"service": "quiz-server",
"running": True,
}
}
},
}
return state_bytes, observation
class _Runtime:
def __init__(self, observation):
self.observation = observation
self.restored = []
self.snapshots = []
def observe(self, repository, target, *, receipt_key):
assert repository == REPOSITORY
assert target["slug"] == "quiz-monitor"
assert receipt_key == KEY
return copy.deepcopy(self.observation)
def snapshot(self, repository, target, observation, request_id, *, receipt_key):
value = {
"revision": "unknown",
"legacy_provenance": True,
"containers": [
{
"name": "quiz-monitor-quiz-server-1",
"image_id": observation["imageIDs"]["quiz-monitor-quiz-server-1"],
"image_ref": "quiz-monitor:latest",
"rollback_ref": "gitea-deploy-rollback/quiz-monitor-0:legacy-6f9619ff",
"was_running": True,
}
],
}
self.snapshots.append(value)
return copy.deepcopy(value)
def restore(self, repository, target, snapshot, current_revision, *, receipt_key):
self.restored.append((repository, target["slug"], snapshot, current_revision))
return copy.deepcopy(self.observation)
@pytest.fixture
def api():
def api_json(path, *, query):
if path == f"/repos/{REPOSITORY}/branches/main":
return {"protected": True, "commit": {"id": TARGET_SHA}}
if path == f"/repos/{REPOSITORY}/actions/runs":
assert query == {
"event": "push", "branch": "main", "head_sha": TARGET_SHA,
"page": "1", "limit": "100",
}
return {"total_count": 1, "workflow_runs": [{
"id": 7, "run_number": 19, "run_attempt": 1,
"event": "push", "head_sha": TARGET_SHA, "head_branch": "main",
"path": "managed-validation.yml@refs/heads/main",
"display_title": "Managed validation / full (push)",
"created_at": "2026-10-07T12:00:00Z", "status": "completed",
"conclusion": "success", "repository": {"full_name": REPOSITORY},
"head_repository": {"full_name": REPOSITORY}, "pull_requests": [],
}]}
if path == f"/repos/{REPOSITORY}/actions/runs/7/jobs":
assert query == {"page": "1", "limit": "100"}
return {"total_count": 1, "jobs": [{
"id": 9, "name": "full", "run_id": 7, "run_attempt": 1,
"head_sha": TARGET_SHA, "head_branch": "main",
"status": "completed", "conclusion": "success",
}]}
raise AssertionError(f"unexpected Gitea read: {path}")
return api_json
def _read_state(state_bytes):
state = json.loads(state_bytes.decode("utf-8")) if state_bytes else {}
return lambda _repository, _project: (state, state_bytes)
def _baseline(module, project, state_bytes, observation, tmp_path):
(tmp_path / "quiz-monitor.json").write_bytes(state_bytes)
return module.baseline(
{"repository": REPOSITORY, "requestId": REQUEST_ID},
registry={REPOSITORY: project},
read_state=_read_state(state_bytes),
runtime=_Runtime(observation), receipt_key=KEY, state_root=tmp_path,
)
def test_registered_target_policy_accepts_bounded_main_compose_and_rejects_special_targets(
module, project
):
assert module.validate_registered_legacy_target(REPOSITORY, project) == "quiz-monitor"
for repository, mutate in [
("jens/unraid-appops-gateway", lambda candidate: None),
("jens/workmesh", lambda candidate: None),
(REPOSITORY, lambda candidate: candidate.update(managed_plan_version="itworx.deployment/v1")),
(REPOSITORY, lambda candidate: candidate.update(deploy_script="scripts/deploy.sh")),
(REPOSITORY, lambda candidate: candidate.update(containers=[f"quiz-monitor-svc-{n}" for n in range(9)])),
(REPOSITORY, lambda candidate: candidate.update(compose_files=["../other/docker-compose.yml"])),
]:
candidate = copy.deepcopy(project)
mutate(candidate)
with pytest.raises(module.RegisteredLegacyAdoptionError):
module.validate_registered_legacy_target(repository, candidate)
def test_rendered_compose_must_match_observed_environment_mounts_and_published_ports(module):
model = {"services": {"quiz-server": {
"environment": {"OPENRGB_HOST": "198.19.1.2"},
"volumes": [{"type": "bind", "source": "/srv/example/appdata/quiz-monitor/config",
"target": "/config", "read_only": False}],
"ports": [{"target": 8000, "published": "1205", "protocol": "tcp",
"host_ip": "0.0.0.0"}],
}}, "networks": {"default": {"name": "quiz-monitor_default"}}}
container = {"quiz-monitor-quiz-server-1": {
"Config": {"Env": ["OPENRGB_HOST=198.19.1.2"]},
"Mounts": [{"Type": "bind", "Source": "/srv/example/appdata/quiz-monitor/config",
"Destination": "/config", "RW": True}],
"HostConfig": {"NetworkMode": "quiz-monitor_default"},
"NetworkSettings": {"Networks": {"quiz-monitor_default": {}},
"Ports": {"8000/tcp": [{"HostIp": "0.0.0.0", "HostPort": "1205"}]}},
}}
module.validate_compose_runtime(model, container)
wrong = copy.deepcopy(container)
wrong["quiz-monitor-quiz-server-1"]["NetworkSettings"]["Ports"]["8000/tcp"][0]["HostPort"] = "1206"
with pytest.raises(module.RegisteredLegacyAdoptionError):
module.validate_compose_runtime(model, wrong)
def test_rendered_compose_rejects_changed_command_security_and_named_volume_identity(module):
model = {"services": {"quiz-server": {
"image": "quiz-monitor:latest", "command": ["python", "server.py"],
"security_opt": ["no-new-privileges:true"],
"volumes": [{"type": "volume", "source": "quiz-config", "target": "/config"}],
}}, "volumes": {"quiz-config": {"name": "quiz-monitor-config"}},
"networks": {"default": {"name": "quiz-monitor_default"}}}
original = {"quiz-monitor-quiz-server-1": {
"Config": {"Cmd": ["python", "server.py"], "Labels": {"com.docker.compose.service": "quiz-server"}},
"HostConfig": {"SecurityOpt": ["no-new-privileges:true"], "NetworkMode": "quiz-monitor_default"},
"Mounts": [{"Type": "volume", "Name": "quiz-monitor-config", "Destination": "/config", "RW": True}],
"NetworkSettings": {"Networks": {"quiz-monitor_default": {}}, "Ports": {}},
}}
module.validate_compose_runtime(model, original, compose_project="quiz-monitor")
changed = copy.deepcopy(original)
changed["quiz-monitor-quiz-server-1"]["Config"]["Cmd"] = ["sh", "-c", "unexpected"]
with pytest.raises(module.RegisteredLegacyAdoptionError):
module.validate_compose_runtime(model, changed, compose_project="quiz-monitor")
changed = copy.deepcopy(original)
changed["quiz-monitor-quiz-server-1"]["HostConfig"]["SecurityOpt"] = []
with pytest.raises(module.RegisteredLegacyAdoptionError):
module.validate_compose_runtime(model, changed, compose_project="quiz-monitor")
changed = copy.deepcopy(original)
changed["quiz-monitor-quiz-server-1"]["Mounts"][0]["Name"] = "unexpected-volume"
with pytest.raises(module.RegisteredLegacyAdoptionError):
module.validate_compose_runtime(model, changed, compose_project="quiz-monitor")
def test_baseline_binds_exact_runtime_state_without_revealing_stale_sha_or_secrets(
module, project, baseline_inputs, tmp_path
):
state_bytes, observation = baseline_inputs
result = _baseline(module, project, state_bytes, observation, tmp_path)
assert result["previousRevision"] == "unknown"
assert result["expectedStateSha256"] == hashlib.sha256(state_bytes).hexdigest()
assert result["runtimeFingerprint"] == "2" * 64
assert result["imageIDs"] == observation["imageIDs"]
assert "0c9c1747" not in json.dumps(result)
assert "private-test-value" not in json.dumps(result)
root = tmp_path / "registered-legacy-adoption" / "quiz-monitor"
assert (root / f"{REQUEST_ID}.baseline.json").is_file()
private = root / f"{REQUEST_ID}.private.json"
assert private.is_file()
assert "private-test-value" not in (root / f"{REQUEST_ID}.baseline.json").read_text()
def test_preflight_requires_latest_exact_main_push_and_registered_full_job(
module, project, baseline_inputs, api, tmp_path
):
state_bytes, observation = baseline_inputs
baseline = _baseline(module, project, state_bytes, observation, tmp_path)
result = module.preflight(
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
"expectedStateSha256": baseline["expectedStateSha256"]},
registry={REPOSITORY: project}, read_state=_read_state(state_bytes),
runtime=_Runtime(observation), api_json=api, receipt_key=KEY, state_root=tmp_path,
)
assert result["status"] == "preflight-ok"
assert result["revision"] == TARGET_SHA
assert result["validationEvidenceSha256"]
assert result["preflightSha256"]
@pytest.mark.parametrize("invalid", ["pr_event", "wrong_sha", "failed_latest", "missing_full_job", "unprotected"])
def test_preflight_rejects_noncanonical_or_failed_latest_validation(
module, project, baseline_inputs, api, tmp_path, invalid
):
state_bytes, observation = baseline_inputs
baseline = _baseline(module, project, state_bytes, observation, tmp_path)
def bad_api(path, *, query):
value = api(path, query=query)
if path.endswith("/actions/runs"):
run = value["workflow_runs"][0]
if invalid == "pr_event":
run["event"] = "pull_request"
elif invalid == "wrong_sha":
run["head_sha"] = "b" * 40
elif invalid == "failed_latest":
run["run_attempt"] = 2
run["conclusion"] = "failure"
elif path.endswith("/branches/main") and invalid == "unprotected":
value["protected"] = False
elif path.endswith("/jobs") and invalid == "missing_full_job":
value["jobs"][0]["name"] = "backend"
return value
with pytest.raises(module.RegisteredLegacyAdoptionError):
module.preflight(
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
"expectedStateSha256": baseline["expectedStateSha256"]},
registry={REPOSITORY: project}, read_state=_read_state(state_bytes),
runtime=_Runtime(observation), api_json=bad_api, receipt_key=KEY, state_root=tmp_path,
)
def test_adoption_rechecks_gate_and_cas_then_saves_unknown_rollback_proof(
module, project, baseline_inputs, api, tmp_path
):
state_bytes, observation = baseline_inputs
(tmp_path / "quiz-monitor.json").write_bytes(state_bytes)
runtime = _Runtime(observation)
registry = {REPOSITORY: project}
read_state = _read_state(state_bytes)
baseline = module.baseline(
{"repository": REPOSITORY, "requestId": REQUEST_ID}, registry=registry,
read_state=read_state, runtime=runtime, receipt_key=KEY, state_root=tmp_path,
)
preflight = module.preflight(
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
"expectedStateSha256": baseline["expectedStateSha256"]},
registry=registry, read_state=read_state, runtime=runtime, api_json=api,
receipt_key=KEY, state_root=tmp_path,
)
# Adoption independently rereads main and validation rather than trusting the saved preflight.
calls = []
def rechecked_api(path, *, query):
calls.append(path)
return api(path, query=query)
result = module.adopt(
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
"expectedStateSha256": baseline["expectedStateSha256"],
"preflightSha256": preflight["preflightSha256"]},
registry=registry, read_state=read_state, runtime=runtime, api_json=rechecked_api,
receipt_key=KEY, state_root=tmp_path,
)
assert result["status"] == "adopted"
assert result["previousRevision"] == "unknown"
assert any(path.endswith("/actions/runs") for path in calls)
receipt = tmp_path / "registered-legacy-adoption" / "quiz-monitor" / f"{REQUEST_ID}.receipt.json"
assert receipt.is_file()
assert "private-test-value" not in receipt.read_text()
snapshot = module.capture_deployment_snapshot(
REPOSITORY, project, json.loads(state_bytes), request_id=REQUEST_ID,
target_revision=TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime,
)
assert snapshot["revision"] == "unknown"
assert snapshot["legacy_provenance"] is True
assert snapshot["containers"][0]["image_id"] == observation["imageIDs"]["quiz-monitor-quiz-server-1"]
assert snapshot["registered_legacy"]["snapshotSha256"]
assert module.status(
{"repository": REPOSITORY, "requestId": REQUEST_ID}, registry=registry,
read_state=read_state, receipt_key=KEY, state_root=tmp_path,
)["status"] == "adopted"
def test_adoption_rejects_a_changed_controller_state_before_snapshot(
module, project, baseline_inputs, api, tmp_path
):
state_bytes, observation = baseline_inputs
current_bytes = [state_bytes]
def read_state(_repository, _project):
return json.loads(current_bytes[0].decode("utf-8")), current_bytes[0]
runtime = _Runtime(observation)
registry = {REPOSITORY: project}
baseline = module.baseline(
{"repository": REPOSITORY, "requestId": REQUEST_ID}, registry=registry,
read_state=read_state, runtime=runtime, receipt_key=KEY, state_root=tmp_path,
)
preflight = module.preflight(
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
"expectedStateSha256": baseline["expectedStateSha256"]},
registry=registry, read_state=read_state, runtime=runtime, api_json=api,
receipt_key=KEY, state_root=tmp_path,
)
current_bytes[0] += b" "
with pytest.raises(module.RegisteredLegacyAdoptionError):
module.adopt(
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
"expectedStateSha256": baseline["expectedStateSha256"],
"preflightSha256": preflight["preflightSha256"]},
registry=registry, read_state=read_state, runtime=runtime, api_json=api,
receipt_key=KEY, state_root=tmp_path,
)
assert not (tmp_path / "registered-legacy-adoption" / "quiz-monitor" / f"{REQUEST_ID}.receipt.json").exists()
def test_status_is_nonblocking_and_worker_failure_is_durable_and_terminal_safe(
module, project, baseline_inputs, tmp_path
):
state_bytes, observation = baseline_inputs
_baseline(module, project, state_bytes, observation, tmp_path)
pending = module.record_worker_failure(
REPOSITORY, REQUEST_ID, "worker_start_failed", receipt_key=KEY, state_root=tmp_path,
)
assert pending["status"] == "recovery-required"
assert module.record_worker_failure(
REPOSITORY, REQUEST_ID, "second_failure", receipt_key=KEY, state_root=tmp_path,
)["status"] == "recovery-required"
assert module.status(
{"repository": REPOSITORY, "requestId": REQUEST_ID},
registry={REPOSITORY: project}, read_state=_read_state(state_bytes),
receipt_key=KEY, state_root=tmp_path,
)["status"] == "recovery-required"
@@ -0,0 +1,154 @@
from __future__ import annotations
import sys
from types import SimpleNamespace
import pytest
from test_hub_component_transport import call
from test_hub_component_transport import controller as _controller_fixture
REPO = "jens/quiz-monitor"
REQUEST = "11111111-1111-4111-8111-111111111111"
PREFIX = "/v1/appops/registered-legacy/"
@pytest.fixture
def controller(monkeypatch):
return _controller_fixture.__wrapped__(monkeypatch)
@pytest.fixture
def adoption(monkeypatch):
observations = []
def status(payload, **_kwargs):
return {"status": "adopted", "repository": payload["repository"], "requestId": payload["requestId"],
"revision": "a" * 40, "expectedStateSha256": "b" * 64}
fake = SimpleNamespace(RegisteredLegacyAdoptionError=ValueError, read_registered_state=lambda *_a: ({}, None),
status=status, baseline=lambda payload, **_k: {"status": "ready", **payload},
accept_rollback=lambda payload, **_k: {"status": "rollback_pending", **payload},
record_worker_failure=lambda *args, **_k: observations.append(args))
monkeypatch.setitem(sys.modules, "registered_legacy_adoption", fake)
return fake, observations
def invoke(module, stage, payload):
return call(module, PREFIX + stage, payload, "_handle_registered_legacy")
def rollback_payload():
return {"repository": REPO, "requestId": REQUEST, "revision": "a" * 40, "expectedStateSha256": "b" * 64}
def test_generic_authentication_rejects_before_helper_loading(controller, monkeypatch):
module, _ = controller
monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_a, **_k: False)
assert invoke(module, "adopt", {"image": "attacker/image"})[0] == 403
@pytest.mark.parametrize("change", [{"repository": "Jens/quiz-monitor"}, {"repository": "../escape"},
{"requestId": "bad"}, {"requestId": 123}])
def test_generic_envelope_rejects_invalid_identity_before_helpers(controller, adoption, change):
module, _ = controller
assert invoke(module, "baseline", {"repository": REPO, "requestId": REQUEST, **change})[0] == 400
def test_delivery_requires_adopted_exact_target_and_preserves_original_revalidation(controller, adoption, monkeypatch):
module, _ = controller
called = []
monkeypatch.setattr(module, "_request_terminal_revalidation", lambda *args, **kwargs: called.append((args, kwargs)) or {"status": "awaiting_validation"})
payload = {"repository": REPO, "requestId": REQUEST, "revision": "a" * 40}
assert invoke(module, "delivery", {**payload, "revision": "c" * 40})[1]["reason"] == "registered_delivery_adoption_required"
assert invoke(module, "delivery", {**payload, "image": "caller/image"})[1]["reason"] == "registered_delivery_contract_invalid"
assert not called
assert invoke(module, "delivery", payload)[1]["status"] == "awaiting_validation"
assert called == [((REPO, "a" * 40), {"registered_legacy_request_id": REQUEST})]
def test_only_internal_adopted_retry_binds_request_to_deployer_environment(controller, monkeypatch):
module, _ = controller
called = []
monkeypatch.setenv("APPOPS_REGISTERED_LEGACY_REQUEST_ID", "inherited-untrusted-value")
monkeypatch.setattr(module.subprocess, "run", lambda args, **kwargs: called.append((args, kwargs)) or SimpleNamespace(returncode=0))
module._run_registered_deploy(REPO, "a" * 40)
assert "APPOPS_REGISTERED_LEGACY_REQUEST_ID" not in called[-1][1]["env"]
module._run_registered_deploy(REPO, "a" * 40, registered_legacy_request_id=REQUEST)
assert called[-1][1]["env"]["APPOPS_REGISTERED_LEGACY_REQUEST_ID"] == REQUEST
assert REQUEST not in called[-1][0]
with pytest.raises(ValueError):
module._run_registered_deploy(REPO, "a" * 40, registered_legacy_request_id="bad")
with pytest.raises(ValueError):
module._run_registered_deploy(module.REGISTERED_AUTO_APPLY_REPOSITORY, "a" * 40, registered_legacy_request_id=REQUEST)
def test_rollback_queue_is_bounded_idempotent_and_rejects_changed_binding(controller, adoption, monkeypatch):
module, _ = controller
starts = []
monkeypatch.setattr(module.threading, "Thread", lambda **kwargs: SimpleNamespace(start=lambda: starts.append(kwargs)))
payload = rollback_payload()
assert invoke(module, "rollback", payload)[0] == 202
assert invoke(module, "rollback", payload)[0] == 200
assert len(starts) == 1
assert invoke(module, "rollback", {**payload, "expectedStateSha256": "c" * 64})[1]["reason"] == "registered_rollback_binding_changed"
assert invoke(module, "rollback", {**payload, "requestId": "22222222-2222-4222-8222-222222222222"})[1]["reason"] == "registered_rollback_capacity_busy"
def test_rollback_start_failure_is_recorded_and_capacity_released(controller, adoption, monkeypatch):
module, _ = controller
_, observations = adoption
def fail_start(**_kwargs):
raise RuntimeError("thread start unavailable")
monkeypatch.setattr(module.threading, "Thread", fail_start)
assert invoke(module, "rollback", rollback_payload())[1]["reason"] == "registered_rollback_worker_start_failed"
assert observations == [(REPO, REQUEST, "registered_rollback_worker_start_failed")]
assert not module._registered_legacy_worker_requests
def test_worker_never_discloses_raw_failure_and_releases_capacity(controller, adoption):
module, _ = controller
fake, observations = adoption
def fail(*_args, **_kwargs):
raise RuntimeError("private environment credential")
fake.rollback = fail
module._registered_legacy_worker_requests.add(REPO + "/" + REQUEST)
module._run_registered_legacy_rollback(rollback_payload(), b"x" * 64)
assert observations == [(REPO, REQUEST, "registered_rollback_worker_failed")]
assert not module._registered_legacy_worker_requests
def test_failed_delivery_retry_requires_explicit_adoption_and_keeps_original_record(controller, monkeypatch, tmp_path):
module, _ = controller
revision = "a" * 40
semantic = module._webhook_semantic_digest(6, "refs/heads/main", revision)
original = {"schema": module._WEBHOOK_SCHEMA, "semantic_sha256": semantic,
"gitea_repository_id": 6, "ref": "refs/heads/main", "repository": REPO,
"revision": revision, "body_sha256": "b" * 64, "validation": {},
"status": "failed", "reason": "deployment_failed"}
written = []
monkeypatch.setattr(module, "WEBHOOK_INBOX_ROOT", tmp_path)
monkeypatch.setattr(module, "_secure_inbox_root", lambda: None)
monkeypatch.setattr(module, "_webhook_policy", lambda *_a: (6, "main", "fixed-secret-file"))
monkeypatch.setattr(module, "_validation_policy", lambda *_a: {})
monkeypatch.setattr(module, "_validation_snapshot_matches", lambda *_a: True)
monkeypatch.setattr(module, "_validation_snapshot", lambda *_a: {"required": True})
monkeypatch.setattr(module, "_read_inbox_record", lambda _p: original)
monkeypatch.setattr(module, "_is_current_branch_head", lambda *_a: True)
monkeypatch.setattr(module, "_write_new_inbox_record", lambda path, record: written.append((path, record)))
assert module._enqueue_terminal_revalidation(REPO, revision, {})[0] == "not_revalidatable"
assert module._enqueue_terminal_revalidation(REPO, revision, {}, registered_legacy_request_id="bad")[0] == "invalid"
assert not written
assert module._enqueue_terminal_revalidation(REPO, revision, {}, registered_legacy_request_id=REQUEST)[0] == "awaiting_validation"
path, record = written[0]
assert path.name == semantic + "." + REQUEST + ".revalidation.json"
assert record["registered_legacy_request_id"] == REQUEST
assert record["validation"] == {"required": True}
assert original["status"] == "failed" and original["reason"] == "deployment_failed"
def test_webhook_cannot_inject_registered_adoption_binding(controller, monkeypatch, tmp_path):
module, _ = controller
monkeypatch.setattr(module, "_read_inbox_record", lambda _p: {
"schema": module._WEBHOOK_SCHEMA, "status": "validation_passed",
"registered_legacy_request_id": REQUEST,
})
monkeypatch.setattr(module, "_load_projects", lambda: pytest.fail("injected webhook must be rejected before processing"))
module._process_webhook_record(tmp_path / "a.json")
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,59 @@
from __future__ import annotations
import io
from types import SimpleNamespace
import pytest
from test_deploy_broker import BROKER_ROOT, _load_module
with pytest.MonkeyPatch.context() as patch:
patch.syspath_prepend(str(BROKER_ROOT))
transport = _load_module("registered_transport_test", BROKER_ROOT / "registered_legacy_transport.py")
REPO = "jens/quiz-monitor"
PREFIX = "/repos/" + REPO
def payload(path, query=None):
return {"repository": REPO, "path": PREFIX + path, "query": query or {},
"headers": {"Accept": "application/json", "Authorization": "token " + "x" * 40,
"User-Agent": transport.transport.USER_AGENT}}
@pytest.mark.parametrize("path,query", [
("/branches/main", {}),
("/actions/runs", {"event": "push", "branch": "main", "head_sha": "a" * 40, "page": "1", "limit": "100"}),
("/actions/runs/123/jobs", {"page": "10", "limit": "100"}),
])
def test_only_exact_main_metadata_is_admitted(path, query):
assert transport.allowed(REPO, PREFIX + path, query)
@pytest.mark.parametrize("path,query", [
("/branches/feature", {}), ("/branches/main", {"extra": "1"}),
("/actions/runs/123/jobs", {"page": "11", "limit": "100"}),
("/actions/runs", {"event": "pull_request", "branch": "main", "head_sha": "a" * 40, "page": "1", "limit": "100"}),
("/actions/artifacts/123/zip", {}), ("/branches/main/../../admin", {}),
])
def test_other_branches_runs_paths_and_artifacts_are_denied(path, query):
assert not transport.allowed(REPO, PREFIX + path, query)
def test_repository_and_headers_cannot_escape_read_scope():
assert not transport.allowed("jens/quiz-monitor/../other", PREFIX + "/branches/main", {})
assert not transport.allowed(REPO, "/repos/jens/other/branches/main", {})
request = payload("/branches/main")
request["headers"]["Authorization"] = "token invalid\nheader"
with pytest.raises(ValueError, match="headers_invalid"):
transport.execute(request)
def test_fixed_get_uses_bounded_reader_without_redirects_or_secret_output():
observed = []
class Response(io.BytesIO):
status = 200
def open_request(request, timeout):
observed.append((request.full_url, request.get_method(), timeout))
return Response(b'{"commit":{"id":"abc"}}')
result = transport.execute(payload("/branches/main"), opener=SimpleNamespace(open=open_request))
assert result == b'{"commit":{"id":"abc"}}'
assert observed == [("https://gitea.example.com/api/v1" + PREFIX + "/branches/main", "GET", 10)]