Publish curated Unraid-AppOps-Gateway source from 2e6cf771e11a9cdc8b2e2453586a16d518adf40b

This commit is contained in:
Public Source Publisher committed 2026-10-07 20:07:49 +00:00
1 parent 5d598db0df
commit 631b846397
10 files changed
+4709 -8

No files matched your search

@@ -0,0 +1,963 @@
from __future__ import annotations
import contextlib
import copy
import hashlib
import importlib.util
import json
import sys
import types
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[2]
BROKER = ROOT / "ops" / "gitea-deploy-broker"
REPOSITORY = "jens/unraid-appops-gateway"
REVISION = "c" * 40
PRIOR_REVISION = "d" * 40
TREE = "e" * 40
RECOVERY_ID = "12345678-1234-4234-8234-123456789abc"
TRANSACTION_ID = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
SECRET = b"transaction-reconciliation-test-key-32-bytes"
REGISTRY_BYTES = b'{"repository":"jens/unraid-appops-gateway"}\n'
IMAGE_ID = "sha256:" + "7" * 64
@pytest.fixture
def deploy(monkeypatch):
monkeypatch.syspath_prepend(str(BROKER))
if "fcntl" not in sys.modules:
fcntl = types.ModuleType("fcntl")
fcntl.LOCK_EX = 2
fcntl.LOCK_UN = 8
fcntl.LOCK_NB = 4
fcntl.flock = lambda *_args: None
monkeypatch.setitem(sys.modules, "fcntl", fcntl)
spec = importlib.util.spec_from_file_location(
"appops_effective_transaction_reconciliation_test",
BROKER / "deploy.py",
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def _digest(raw: bytes) -> str:
return hashlib.sha256(raw).hexdigest()
def _mac(secret: bytes, domain: str, raw: bytes) -> str:
return hashlib.sha256(secret + b"\0" + domain.encode() + b"\0" + raw).hexdigest()
def _metadata(
revision: str = REVISION,
tree: str = TREE,
built_at: str = "2026-10-07T12:00:00.000Z",
) -> dict[str, str]:
return {
"id": revision[:12],
"repository": REPOSITORY,
"revision": revision,
"tree": tree,
"treeDigest": hashlib.sha256(f"git-tree:{tree}".encode()).hexdigest(),
"builtAt": built_at,
}
def _pointer(revision: str) -> dict[str, str]:
return {
"recoveryId": RECOVERY_ID,
"revision": revision,
"modelSha256": "1" * 64,
"modelRecordSha256": "2" * 64,
}
def _state_bytes(state: dict[str, object]) -> bytes:
return json.dumps(state, sort_keys=True, separators=(",", ":")).encode() + b"\n"
def _case(deploy, monkeypatch, tmp_path, *, phase: str = "runtime_mutated"):
prior_pointer = _pointer(PRIOR_REVISION)
candidate_pointer = _pointer(REVISION)
backup = {"sha256": "3" * 64, "revision": REVISION}
prior_snapshot = {
"revision": PRIOR_REVISION,
"appops_runtime_model": prior_pointer,
"appops_data_backup": backup,
"containers": [{"name": "Unraid-AppOps-Gateway", "image_id": "sha256:" + "4" * 64}],
}
receipt = {
"schema": "gitea-deploy-receipt.v1",
"repository": REPOSITORY,
"revision": REVISION,
"tree": TREE,
"containers": {
"Unraid-AppOps-Gateway": {
"container_id": "5" * 64,
"image_id": IMAGE_ID,
},
},
}
live_reference = {
"recoveryId": RECOVERY_ID,
"revision": REVISION,
"liveRecordSha256": "6" * 64,
}
old_receipt = {
"schema": "gitea-deploy-receipt.v1",
"repository": REPOSITORY,
"revision": PRIOR_REVISION,
"tree": "9" * 40,
}
state: dict[str, object] = {
"repository": REPOSITORY,
"live_revision": PRIOR_REVISION,
"live_status": "healthy",
"live_legacy_provenance": False,
"live_receipt": old_receipt,
"rollback_snapshot": prior_snapshot,
"live_artifacts": {
"appops_runtime_model": {"recoveryId": RECOVERY_ID, "revision": PRIOR_REVISION},
"containers": {"Unraid-AppOps-Gateway": {"container_id": "8" * 64}},
},
}
original_raw = _state_bytes(state)
original_state_sha = _digest(original_raw)
candidate_model = {
"services": {
"appops-gateway": {
"image": "itworx/appops-gateway:protected",
"build": {"context": "/controller/registered-builds/unraid-appops-gateway/" + REVISION + "/tree"},
"environment": {"APPOPS_SOURCE_REVISION": REVISION},
},
},
}
payload: dict[str, object] = {
"schema": "itworx.appops-self-upgrade-model-transaction/v1",
"repository": REPOSITORY,
"transactionId": TRANSACTION_ID,
"recoveryId": RECOVERY_ID,
"kind": "followup",
"revision": REVISION,
"metadata": _metadata(),
"originSemanticSha256": "a" * 64,
"originRawSha256": "b" * 64,
"originJournalSha256": "c" * 64,
"expectedStateSha256": original_state_sha,
"registryFingerprint": _mac(SECRET, "registry", REGISTRY_BYTES),
"preMutationRuntimeSha256": "d" * 64,
"preMutationRuntimeFingerprint": "e" * 64,
"sourceFingerprint": "f" * 64,
"priorModelPointer": prior_pointer,
"priorSnapshot": prior_snapshot,
"candidateSourceFingerprint": "0" * 64,
"candidateModelPointer": candidate_pointer,
"candidateImageId": IMAGE_ID,
"buildContexts": {
"appops-gateway": "/controller/registered-builds/unraid-appops-gateway/" + REVISION + "/tree",
},
"phase": phase,
"createdAt": 1_791_378_000,
"updatedAt": 1_791_378_001,
}
if phase in {"verified", "committed"}:
payload["verifiedLiveReference"] = live_reference
payload["verifiedReceipt"] = receipt
if phase == "committed":
payload["committedStateSha256"] = "0" * 64
envelope = {"payload": payload, "sha256": "9" * 64}
registry = {"bytes": REGISTRY_BYTES}
calls: dict[str, object] = {
"active": [],
"active_result": None,
"docker": [],
"git": [],
"backup": [],
"verify": [],
"current": [],
"updates": [],
"writes": [],
"state_sha_override": None,
"metadata_override": None,
"receipt_override": None,
"fail_commit_once": False,
}
class StatusReader:
PROJECTS_PATH = tmp_path / "projects.json"
@staticmethod
def _self_upgrade_pending_model_transaction():
return copy.deepcopy(envelope)
@staticmethod
def _self_upgrade_read_fixed_file(_path, **_kwargs):
return registry["bytes"]
@staticmethod
def _self_upgrade_secret():
return SECRET
@staticmethod
def _self_upgrade_mac(secret, domain, raw):
return _mac(secret, domain, raw)
@staticmethod
def _self_upgrade_load_compose_model(recovery_id, revision):
if recovery_id == RECOVERY_ID and revision == REVISION and calls.get("model_valid", True):
return {"model": candidate_model, "pointer": candidate_pointer}
return None
@staticmethod
def _self_upgrade_candidate_model_binding(pointer, observed_receipt):
if (pointer == candidate_pointer and observed_receipt == receipt
and calls.get("model_valid", True)):
return {"modelPointer": pointer, "receipt": observed_receipt}
return None
@staticmethod
def _self_upgrade_store_live_model(pointer, binding):
if pointer == candidate_pointer and binding.get("receipt") == receipt:
return live_reference
return None
@staticmethod
def _self_upgrade_update_model_transaction(transaction_id, expected_sha, next_phase, bindings):
calls["updates"].append((next_phase, copy.deepcopy(bindings)))
if transaction_id != TRANSACTION_ID or expected_sha != envelope["sha256"]:
return None
if next_phase == "committed" and calls["fail_commit_once"]:
calls["fail_commit_once"] = False
return None
updated = dict(envelope["payload"])
updated.update(bindings)
updated["phase"] = next_phase
updated["updatedAt"] = int(updated["updatedAt"]) + 1
if next_phase == "committed":
updated["committedStateSha256"] = _digest(_state_bytes(state))
envelope["payload"] = updated
envelope["sha256"] = _digest(json.dumps(updated, sort_keys=True).encode())
return copy.deepcopy(envelope)
@staticmethod
def _self_upgrade_active_compose_model(observed_state, observed_container):
calls["active"].append((copy.deepcopy(observed_state), copy.deepcopy(observed_container)))
active = calls.get("active_result")
return copy.deepcopy(active) if isinstance(active, dict) else None
def read_preimage(_project):
raw = _state_bytes(state)
override = calls["state_sha_override"]
return copy.deepcopy(state), raw, override or _digest(raw)
def write_state(repository, project, revision, status, _message, **kwargs):
calls["writes"].append((repository, revision, status, copy.deepcopy(kwargs)))
reference = project.get("_appops_runtime_model_binding")
state.update({
"repository": repository,
"live_revision": revision,
"live_status": "healthy" if status == "success" else status,
"live_legacy_provenance": False,
"live_receipt": copy.deepcopy(kwargs["live_receipt"]),
"rollback_snapshot": copy.deepcopy(kwargs["rollback_snapshot"]),
"live_artifacts": {
"appops_runtime_model": copy.deepcopy(reference),
"containers": {"Unraid-AppOps-Gateway": {"container_id": receipt["containers"]["Unraid-AppOps-Gateway"]["container_id"]}},
},
})
def validate_commit(_repo, branch, candidate_revision):
calls["git"].append((branch, candidate_revision))
if branch != "main" or candidate_revision != REVISION:
raise deploy.DeployError("pending source is not the requested main commit")
return candidate_revision
def deployment_metadata(_repo, candidate_revision, repository, *, built_at=None):
if repository != REPOSITORY or candidate_revision != REVISION:
raise deploy.DeployError("pending target identity mismatch")
observed = _metadata(built_at=built_at or "2026-10-07T12:00:00.000Z")
override = calls["metadata_override"]
return copy.deepcopy(override or observed)
def verify_backup(actual_backup, candidate_revision, previous_revision):
calls["backup"].append((actual_backup, candidate_revision, previous_revision))
expected_previous = calls.get("backup_previous_revision", PRIOR_REVISION)
if actual_backup != backup or candidate_revision != REVISION or previous_revision != expected_previous:
raise deploy.DeployError("pending data backup proof changed")
def verify_deployment(_project, observed_metadata, *, expected_image_ids):
calls["verify"].append((copy.deepcopy(observed_metadata), dict(expected_image_ids)))
if expected_image_ids != {"Unraid-AppOps-Gateway": IMAGE_ID}:
raise deploy.DeployError("sealed candidate image differs from the live image")
if observed_metadata != _metadata():
raise deploy.DeployError("candidate receipt metadata changed")
return copy.deepcopy(calls["receipt_override"] or receipt)
def verified_current(repository, _project, candidate_revision, tree):
calls["current"].append((repository, candidate_revision, tree))
if (repository, candidate_revision, tree) == (REPOSITORY, REVISION, TREE):
return copy.deepcopy(receipt)
return None
project = {
"_repository_name": REPOSITORY,
"slug": "unraid-appops-gateway",
"branch": "main",
"containers": ["Unraid-AppOps-Gateway"],
}
monkeypatch.setattr(deploy, "_appops_status_reader", lambda: StatusReader())
monkeypatch.setattr(deploy, "read_appops_state_preimage", read_preimage)
monkeypatch.setattr(deploy, "write_state", write_state)
monkeypatch.setattr(deploy, "validate_commit", validate_commit)
monkeypatch.setattr(deploy, "deployment_metadata", deployment_metadata)
monkeypatch.setattr(deploy, "verify_appops_data_backup", verify_backup)
monkeypatch.setattr(deploy, "verify_deployment", verify_deployment)
monkeypatch.setattr(deploy, "verified_current_receipt", verified_current)
monkeypatch.setattr(deploy, "run", lambda *_args, **_kwargs: pytest.fail("reconciliation must not mutate Docker"))
monkeypatch.setattr(deploy, "inspect_container", lambda *_args, **_kwargs: pytest.fail("unexpected inspect path"))
monkeypatch.setattr(deploy, "restore_compose_snapshot", lambda *_args, **_kwargs: pytest.fail("unexpected rollback"))
return SimpleNamespace(
deploy=deploy,
project=project,
repo=tmp_path / "repository.git",
payload=payload,
envelope=envelope,
state=state,
state_sha=original_state_sha,
registry=registry,
metadata=_metadata(),
pointer=candidate_pointer,
reference=live_reference,
receipt=receipt,
backup=backup,
calls=calls,
)
def _reconcile(case):
return case.deploy._appops_reconcile_pending_model_transaction(
case.project, REPOSITORY, case.repo,
)
def test_runtime_mutated_transaction_finishes_state_publication_without_docker_mutation(
deploy, monkeypatch, tmp_path,
):
case = _case(deploy, monkeypatch, tmp_path)
result = _reconcile(case)
assert result == case.receipt
assert case.envelope["payload"]["phase"] == "committed"
assert [phase for phase, _bindings in case.calls["updates"]] == ["verified", "committed"]
assert len(case.calls["writes"]) == 1
repo, revision, status, kwargs = case.calls["writes"][0]
assert (repo, revision, status) == (REPOSITORY, REVISION, "success")
assert kwargs["expected_state_sha256"] == case.state_sha
assert kwargs["live_receipt"] == case.receipt
assert case.state["live_legacy_provenance"] is False
assert case.state["live_artifacts"]["appops_runtime_model"] == case.reference
assert case.calls["git"] == [("main", REVISION)]
assert len(case.calls["backup"]) == 1
assert case.calls["verify"] == [(case.metadata, {"Unraid-AppOps-Gateway": IMAGE_ID})]
assert case.calls["docker"] == []
def test_verified_state_replay_only_reattests_and_marks_transaction_committed(
deploy, monkeypatch, tmp_path,
):
case = _case(deploy, monkeypatch, tmp_path, phase="verified")
case.state.update({
"live_revision": REVISION,
"live_status": "healthy",
"live_legacy_provenance": False,
"live_receipt": copy.deepcopy(case.receipt),
"live_artifacts": {"appops_runtime_model": copy.deepcopy(case.reference)},
})
result = _reconcile(case)
assert result == case.receipt
assert case.envelope["payload"]["phase"] == "committed"
assert [phase for phase, _bindings in case.calls["updates"]] == ["committed"]
assert case.calls["current"] == [(REPOSITORY, REVISION, TREE)]
assert case.calls["writes"] == []
assert case.calls["git"] == []
assert case.calls["verify"] == []
assert case.calls["docker"] == []
def test_verified_state_replay_rejects_changed_registry_before_reattest(
deploy, monkeypatch, tmp_path,
):
case = _case(deploy, monkeypatch, tmp_path, phase="verified")
case.state.update({
"live_revision": REVISION,
"live_status": "healthy",
"live_legacy_provenance": False,
"live_receipt": copy.deepcopy(case.receipt),
"live_artifacts": {"appops_runtime_model": copy.deepcopy(case.reference)},
})
case.registry["bytes"] = b'{"repository":"changed"}\n'
with pytest.raises(deploy.DeployError, match="state or registry changed"):
_reconcile(case)
assert case.calls["current"] == []
assert case.calls["updates"] == []
assert case.calls["writes"] == []
assert case.calls["docker"] == []
def test_state_commit_failure_restarts_from_verified_proof_without_second_state_write(
deploy, monkeypatch, tmp_path,
):
case = _case(deploy, monkeypatch, tmp_path)
case.calls["fail_commit_once"] = True
with pytest.raises(deploy.DeployError, match="compare-and-swap failed"):
_reconcile(case)
assert case.envelope["payload"]["phase"] == "verified"
assert len(case.calls["writes"]) == 1
assert case.calls["docker"] == []
result = _reconcile(case)
assert result == case.receipt
assert case.envelope["payload"]["phase"] == "committed"
assert [phase for phase, _bindings in case.calls["updates"]] == [
"verified", "committed", "committed",
]
assert len(case.calls["writes"]) == 1
assert len(case.calls["current"]) == 1
assert case.calls["docker"] == []
@pytest.mark.parametrize(
"mismatch",
["state", "registry", "model", "target_metadata", "backup", "image", "receipt", "unknown_phase"],
)
def test_pending_transaction_mismatches_fail_without_state_or_docker_mutation(
deploy, monkeypatch, tmp_path, mismatch,
):
case = _case(deploy, monkeypatch, tmp_path)
if mismatch == "state":
case.calls["state_sha_override"] = "0" * 64
elif mismatch == "registry":
case.registry["bytes"] = b'{"repository":"different"}\n'
elif mismatch == "model":
case.calls["model_valid"] = False
elif mismatch == "target_metadata":
case.calls["metadata_override"] = _metadata(tree="9" * 40)
elif mismatch == "backup":
case.payload["priorSnapshot"]["appops_data_backup"] = {"sha256": "0" * 64}
elif mismatch == "image":
case.payload["candidateImageId"] = "sha256:" + "8" * 64
elif mismatch == "receipt":
case.calls["receipt_override"] = {**case.receipt, "revision": "b" * 40}
elif mismatch == "unknown_phase":
case.payload["phase"] = "prepared"
with pytest.raises(deploy.DeployError):
_reconcile(case)
assert case.calls["writes"] == []
assert case.calls["updates"] == []
assert case.calls["docker"] == []
def _prepare_rolling_back_case(deploy, monkeypatch, tmp_path):
case = _case(deploy, monkeypatch, tmp_path, phase="rolling_back")
prior_revision = deploy.APPOPS_LEGACY_OLD_REVISION
prior_pointer = {
**_pointer(prior_revision),
"schema": "itworx.appops-effective-compose-model.v1",
}
restored_reference = {**prior_pointer, "liveRecordSha256": "a" * 64}
prior_snapshot = {
"revision": prior_revision,
"legacy_provenance": True,
"receipt": {},
"appops_runtime_model": prior_pointer,
"appops_data_backup": case.backup,
"containers": [{"name": "Unraid-AppOps-Gateway", "image_id": IMAGE_ID}],
}
case.payload.update({
"phase": "rolling_back",
"priorModelPointer": prior_pointer,
"priorSnapshot": prior_snapshot,
})
case.state.update({
"repository": REPOSITORY,
"live_revision": prior_revision,
"live_status": "healthy",
"live_legacy_provenance": True,
"live_receipt": {},
"live_artifacts": {
"appops_runtime_model": restored_reference,
"containers": {"Unraid-AppOps-Gateway": {"container_id": "b" * 64}},
},
})
case.calls["active_result"] = {"pointer": copy.deepcopy(restored_reference)}
case.calls["backup_previous_revision"] = prior_revision
container = {"Id": "b" * 64, "Image": IMAGE_ID, "State": {"Running": True}}
monkeypatch.setattr(deploy, "inspect_container", lambda _name: copy.deepcopy(container))
return case, prior_pointer, restored_reference, prior_snapshot, container
def test_rolling_back_transaction_seals_exact_restored_legacy_binding_without_mutation(
deploy, monkeypatch, tmp_path,
):
case, prior_pointer, restored_reference, prior_snapshot, container = _prepare_rolling_back_case(
deploy, monkeypatch, tmp_path,
)
result = _reconcile(case)
assert result == {}
assert case.envelope["payload"]["phase"] == "rolled_back"
assert case.envelope["payload"]["restoredLiveReference"] == restored_reference
assert case.calls["updates"] == [("rolled_back", {"restoredLiveReference": restored_reference})]
assert case.calls["active"] == [(case.state, container)]
assert case.calls["backup"] == [(case.backup, REVISION, deploy.APPOPS_LEGACY_OLD_REVISION)]
assert case.calls["writes"] == []
assert case.calls["git"] == []
assert case.calls["docker"] == []
assert case.payload["priorModelPointer"] == prior_pointer
assert case.payload["priorSnapshot"] == prior_snapshot
@pytest.mark.parametrize("mismatch", ["state", "reference", "runtime", "registry", "backup"])
def test_rolling_back_transaction_rejects_unbound_prior_state_without_mutation(
deploy, monkeypatch, tmp_path, mismatch,
):
case, _prior_pointer, restored_reference, prior_snapshot, _container = _prepare_rolling_back_case(
deploy, monkeypatch, tmp_path,
)
if mismatch == "state":
case.state["live_revision"] = "9" * 40
elif mismatch == "reference":
case.state["live_artifacts"]["appops_runtime_model"]["liveRecordSha256"] = "c" * 64
elif mismatch == "runtime":
case.calls["active_result"] = {"pointer": {**restored_reference, "liveRecordSha256": "d" * 64}}
elif mismatch == "registry":
case.registry["bytes"] = b'{"repository":"different"}\n'
elif mismatch == "backup":
prior_snapshot["appops_data_backup"] = {"sha256": "0" * 64}
with pytest.raises(deploy.DeployError):
_reconcile(case)
assert case.calls["updates"] == []
assert case.calls["writes"] == []
assert case.calls["git"] == []
assert case.calls["docker"] == []
def test_explicit_rollback_cli_routes_through_pending_model_reconciliation(
deploy, monkeypatch, tmp_path, capsys,
):
revision = REVISION
project = {
"slug": "unraid-appops-gateway",
"branch": "main",
"source_dir": str(tmp_path / "source"),
"compose_project": "unraid-appops-gateway",
"compose_files": ["docker-compose.yml"],
"containers": ["Unraid-AppOps-Gateway"],
"verify_oci_revision": True,
"environment": {},
}
projects = {REPOSITORY: project}
events = []
real_exists = deploy.Path.exists
def path_exists(path):
if str(path) == "/controller-hub-releases/appops-self-upgrade-recoveries/transactions":
return True
return real_exists(path)
def reconcile(actual_project, repository, repo):
assert actual_project["_repository_name"] == REPOSITORY
assert repository == REPOSITORY
assert repo == tmp_path / "repository.git"
events.append("reconcile")
return {"revision": revision, "proof": "signed-prior-runtime"}
monkeypatch.setattr(deploy.Path, "exists", path_exists)
monkeypatch.setattr(deploy, "load_projects", lambda: projects)
monkeypatch.setattr(deploy, "effective_mcp_hub_project", lambda _repository, item: dict(item))
monkeypatch.setattr(deploy, "repository_path", lambda *_args: tmp_path / "repository.git")
monkeypatch.setattr(deploy, "validate_commit", lambda *_args: revision)
monkeypatch.setattr(deploy, "deployment_metadata", lambda *_args: _metadata())
monkeypatch.setattr(deploy, "validate_project_config", lambda *_args: None)
monkeypatch.setattr(deploy, "bind_appops_self_upgrade_recovery", lambda *_args: None)
monkeypatch.setattr(deploy, "bind_registered_legacy_request", lambda *_args: None)
monkeypatch.setattr(deploy, "exclusive_lock", lambda *_args, **_kwargs: contextlib.nullcontext())
@contextlib.contextmanager
def global_lock():
events.append("global-lock")
yield
monkeypatch.setattr(deploy, "exclusive_global_deploy_lock", global_lock)
monkeypatch.setattr(deploy, "_appops_reconcile_pending_model_transaction", reconcile)
monkeypatch.setattr(sys, "argv", ["deploy.py", "rollback", REPOSITORY, revision])
assert deploy.main() == 0
assert events == ["global-lock", "reconcile"]
assert '"status": "already-current"' in capsys.readouterr().out
def _recreated_receipt_case(deploy, monkeypatch):
pointer = {
"schema": "itworx.appops-effective-compose-model.v1",
"recoveryId": RECOVERY_ID,
"revision": PRIOR_REVISION,
"modelSha256": "1" * 64,
"modelRecordSha256": "2" * 64,
"liveRecordSha256": "3" * 64,
}
recorded = {
"schema": "gitea-deploy-receipt.v1",
"repository": REPOSITORY,
"revision": PRIOR_REVISION,
"tree": "4" * 40,
"treeDigest": "5" * 64,
"build": {
"id": PRIOR_REVISION[:12],
"repository": REPOSITORY,
"revision": PRIOR_REVISION,
"tree": "4" * 40,
"treeDigest": "5" * 64,
"builtAt": "2026-10-07T11:00:00.000Z",
},
"containers": {
"Unraid-AppOps-Gateway": {
"container_id": "a" * 64,
"image_id": IMAGE_ID,
"image_revision": PRIOR_REVISION,
"image_tree": "4" * 40,
"health": "healthy",
},
},
"live": {"status": "healthy"},
}
observed = copy.deepcopy(recorded)
observed["containers"]["Unraid-AppOps-Gateway"]["container_id"] = "b" * 64
snapshot = {
"revision": PRIOR_REVISION,
"legacy_provenance": False,
"receipt": recorded,
"appops_runtime_model": pointer,
}
project = {
"_repository_name": REPOSITORY,
"_appops_effective_model_pointer": pointer,
}
calls = {"attestation": [], "attestation_valid": True}
def verify_rollback_model(actual_pointer, actual_snapshot):
calls["attestation"].append((copy.deepcopy(actual_pointer), copy.deepcopy(actual_snapshot)))
return (
calls["attestation_valid"]
and actual_pointer == pointer
and actual_snapshot == snapshot
)
monkeypatch.setattr(
deploy,
"_appops_status_reader",
lambda: SimpleNamespace(_self_upgrade_verify_rollback_model=verify_rollback_model),
)
project["_appops_effective_rollback_snapshot"] = snapshot
return SimpleNamespace(
project=project,
pointer=pointer,
recorded=recorded,
observed=observed,
snapshot=snapshot,
calls=calls,
)
def test_recreated_receipt_matcher_allows_only_new_valid_container_id(
deploy, monkeypatch,
):
case = _recreated_receipt_case(deploy, monkeypatch)
recorded_id = case.recorded["containers"]["Unraid-AppOps-Gateway"]["container_id"]
observed_id = case.observed["containers"]["Unraid-AppOps-Gateway"]["container_id"]
assert len(recorded_id) == len(observed_id) == 64
assert recorded_id != observed_id
assert deploy._appops_recreated_receipt_matches(case.project, case.observed, case.recorded)
assert case.calls["attestation"] == [(case.pointer, case.snapshot)]
@pytest.mark.parametrize(
"mismatch",
[
"repository", "legacy_provenance", "snapshot_revision", "snapshot_receipt",
"project_model", "rollback_attestation", "observed_revision", "observed_tree",
"observed_image", "observed_container_id", "recorded_container_id",
"missing_container", "extra_container",
],
)
def test_recreated_receipt_matcher_rejects_every_other_changed_fact(
deploy, monkeypatch, mismatch,
):
case = _recreated_receipt_case(deploy, monkeypatch)
if mismatch == "repository":
case.project["_repository_name"] = "jens/other"
elif mismatch == "legacy_provenance":
case.snapshot["legacy_provenance"] = True
elif mismatch == "snapshot_revision":
case.snapshot["revision"] = "9" * 40
elif mismatch == "snapshot_receipt":
case.snapshot["receipt"] = {**case.recorded, "tree": "8" * 40}
elif mismatch == "project_model":
case.project["_appops_effective_model_pointer"] = {**case.pointer, "modelSha256": "9" * 64}
elif mismatch == "rollback_attestation":
case.calls["attestation_valid"] = False
elif mismatch == "observed_revision":
case.observed["revision"] = "9" * 40
elif mismatch == "observed_tree":
case.observed["build"]["tree"] = "8" * 40
elif mismatch == "observed_image":
case.observed["containers"]["Unraid-AppOps-Gateway"]["image_id"] = "sha256:" + "8" * 64
elif mismatch == "observed_container_id":
case.observed["containers"]["Unraid-AppOps-Gateway"]["container_id"] = "not-a-container-id"
elif mismatch == "recorded_container_id":
case.recorded["containers"]["Unraid-AppOps-Gateway"]["container_id"] = "not-a-container-id"
elif mismatch == "missing_container":
case.observed["containers"] = {}
elif mismatch == "extra_container":
case.observed["containers"]["unexpected"] = {"container_id": "c" * 64}
assert not deploy._appops_recreated_receipt_matches(case.project, case.observed, case.recorded)
def test_known_model_restore_runs_real_receipt_comparison_and_rebinds_recreated_container(
deploy, monkeypatch, tmp_path,
):
case = _recreated_receipt_case(deploy, monkeypatch)
container_name = "Unraid-AppOps-Gateway"
old_container_id = case.recorded["containers"][container_name]["container_id"]
new_container_id = case.observed["containers"][container_name]["container_id"]
case.snapshot["containers"] = [{
"name": container_name,
"container_id": old_container_id,
"image_id": IMAGE_ID,
"image_ref": "unraid-appops-gateway:production",
"was_running": True,
}]
case.project.update({
"slug": "unraid-appops-gateway",
"source_dir": str(tmp_path / "source"),
"compose_project": "unraid-appops-gateway",
"compose_files": ["docker-compose.yml"],
"containers": [container_name],
"environment": {},
})
case.project["_appops_effective_rollback_snapshot"] = case.snapshot
model = {"services": {"appops-gateway": {"environment": {"SYNTHETIC": "safe"}}}}
live_binding = {**case.pointer, "liveRecordSha256": "f" * 64}
calls = {"events": [], "recorded_receipt": None}
monkeypatch.setattr(
deploy,
"_appops_load_effective_model",
lambda _project, pointer: {"pointer": pointer, "model": model},
)
monkeypatch.setattr(deploy, "workmesh_transition_enabled", lambda _project: False)
monkeypatch.setattr(
deploy,
"run",
lambda command, **_kwargs: calls["events"].append(("docker-tag", list(command))),
)
monkeypatch.setattr(
deploy,
"inspect_image",
lambda image_ref: {"Id": IMAGE_ID} if image_ref == "unraid-appops-gateway:production" else None,
)
monkeypatch.setattr(
deploy,
"inspect_container",
lambda _name: {"Id": new_container_id, "Image": IMAGE_ID},
)
monkeypatch.setattr(deploy, "repository_path_from_project", lambda _project: tmp_path / "repository.git")
monkeypatch.setattr(
deploy,
"sync_source",
lambda *_args, **_kwargs: calls["events"].append(("sync-source", None)),
)
monkeypatch.setattr(
deploy,
"copy_environment",
lambda *_args, **_kwargs: calls["events"].append(("copy-environment", None)),
)
monkeypatch.setattr(
deploy,
"remove_attempt_only_compose_containers",
lambda *_args: calls["events"].append(("remove-attempt-only", None)),
)
monkeypatch.setattr(
deploy,
"deployment_environment",
lambda *_args: {"SYNTHETIC": "safe"},
)
monkeypatch.setattr(
deploy,
"restore_snapshot_runtime",
lambda *_args: calls["events"].append(("restore-runtime", None)),
)
monkeypatch.setattr(
deploy,
"validate_recorded_receipt",
lambda _project, receipt, repository, revision: (
{"validated": receipt == case.recorded, "repository": repository, "revision": revision}
),
)
def verify_deployment(_project, build, *, expected_image_ids):
calls["events"].append(("verify-deployment", copy.deepcopy(expected_image_ids)))
assert build == {"validated": True, "repository": REPOSITORY, "revision": PRIOR_REVISION}
assert expected_image_ids == {container_name: IMAGE_ID}
return copy.deepcopy(case.observed)
monkeypatch.setattr(deploy, "verify_deployment", verify_deployment)
monkeypatch.setattr(deploy, "cleanup_rollback_image_tags", lambda *_args: None)
def record_live_model(_project, receipt):
calls["recorded_receipt"] = copy.deepcopy(receipt)
return live_binding
monkeypatch.setattr(deploy, "_appops_record_live_effective_model", record_live_model)
deploy.restore_compose_snapshot(case.project, case.snapshot, REPOSITORY, REVISION)
assert calls["recorded_receipt"] == case.observed
assert case.project["_rollback_receipt"] == case.observed
assert case.project["_appops_restored_model_binding"] == live_binding
assert case.project["_rollback_restored_revision"] == PRIOR_REVISION
assert "_appops_effective_rollback_snapshot" not in case.project
assert old_container_id != new_container_id
assert [event[0] for event in calls["events"]] == [
"docker-tag", "sync-source", "copy-environment", "remove-attempt-only",
"restore-runtime", "verify-deployment",
]
assert len(case.calls["attestation"]) == 2
@pytest.mark.parametrize("legacy_provenance", [True, False])
def test_model_rollback_rebinds_the_restored_live_proof(
deploy, monkeypatch, tmp_path, legacy_provenance,
):
old_revision = deploy.APPOPS_LEGACY_OLD_REVISION if legacy_provenance else PRIOR_REVISION
pointer = {
"schema": "itworx.appops-effective-compose-model.v1",
"recoveryId": RECOVERY_ID,
"revision": old_revision,
"modelSha256": "a" * 64,
"modelRecordSha256": "b" * 64,
}
if not legacy_provenance:
pointer["liveRecordSha256"] = "c" * 64
model = {
"services": {
"appops-gateway": {
"image": "unraid-appops-gateway:production",
"environment": {"KNOWLEDGE_OPTIMIZER_MODEL": "synthetic-model"},
},
},
}
snapshot_receipt = {} if legacy_provenance else {
"schema": "gitea-deploy-receipt.v1",
"repository": REPOSITORY,
"revision": old_revision,
"containers": {"Unraid-AppOps-Gateway": {"image_id": IMAGE_ID}},
}
snapshot = {
"revision": old_revision,
"legacy_provenance": legacy_provenance,
"receipt": snapshot_receipt,
"containers": [{
"name": "Unraid-AppOps-Gateway",
"container_id": "5" * 64,
"image_id": IMAGE_ID,
"image_ref": "unraid-appops-gateway:production",
}],
"appops_runtime_model": pointer,
}
reference = {**pointer, "liveRecordSha256": "d" * 64}
calls = {"plain_snapshot": None, "binding": None, "recorded_receipt": None}
class StatusReader:
@staticmethod
def _self_upgrade_verify_rollback_model(observed_pointer, observed_snapshot):
return observed_pointer == pointer and observed_snapshot == snapshot
@staticmethod
def _self_upgrade_legacy_model_binding(observed_pointer):
calls["binding"] = copy.deepcopy(observed_pointer)
return {"modelPointer": observed_pointer, "runtimeFingerprint": "e" * 64}
@staticmethod
def _self_upgrade_store_live_model(observed_pointer, binding):
if binding.get("modelPointer") == observed_pointer:
return reference
return None
project = {"_repository_name": REPOSITORY, "containers": ["Unraid-AppOps-Gateway"]}
monkeypatch.setattr(deploy, "_appops_status_reader", lambda: StatusReader())
monkeypatch.setattr(
deploy,
"_appops_load_effective_model",
lambda _project, observed_pointer: {"pointer": observed_pointer, "model": model},
)
monkeypatch.setattr(deploy, "_appops_select_effective_model", lambda *_args: None)
monkeypatch.setattr(deploy, "verify_appops_legacy_runtime", lambda *_args, **_kwargs: None)
monkeypatch.setattr(
deploy, "_appops_captured_environment",
lambda _container: {"KNOWLEDGE_OPTIMIZER_MODEL": "synthetic-model"},
)
monkeypatch.setattr(deploy, "inspect_container", lambda _name: {"Id": "6" * 64})
monkeypatch.setattr(deploy, "inspect_image", lambda _image: {"Id": IMAGE_ID})
monkeypatch.setattr(
deploy,
"_appops_record_live_effective_model",
lambda _project, receipt: calls.__setitem__("recorded_receipt", receipt) or reference,
)
original_restore = deploy.restore_compose_snapshot
def restore_with_plain_snapshot(project_arg, observed_snapshot, repository, failed_revision):
if "appops_runtime_model" not in observed_snapshot:
calls["plain_snapshot"] = copy.deepcopy(observed_snapshot)
if not legacy_provenance:
project_arg["_rollback_receipt"] = snapshot_receipt
return None
return original_restore(project_arg, observed_snapshot, repository, failed_revision)
monkeypatch.setattr(deploy, "restore_compose_snapshot", restore_with_plain_snapshot)
restore_with_plain_snapshot(project, snapshot, REPOSITORY, REVISION)
plain_snapshot = calls["plain_snapshot"]
assert isinstance(plain_snapshot, dict)
assert "appops_runtime_model" not in plain_snapshot
if legacy_provenance:
assert plain_snapshot["containers"][0]["restore_image_ref"] == deploy.APPOPS_SOURCE_COMPOSE_IMAGE_REF
assert project["_appops_legacy_restored"] is True
assert calls["binding"] == pointer
else:
assert calls["recorded_receipt"] == snapshot_receipt
assert "_appops_legacy_restored" not in project
assert project["_appops_restored_model_binding"] == reference
File diff suppressed because it is too large. Load diff
@@ -168,6 +168,7 @@ def test_runtime_requires_exact_container_mount_port_and_health(controller, monk
"Config": {
"Image": "unraid-appops-gateway:platform-5553c04ce497",
"Hostname": "unraid-appops-gateway",
"Env": ["APPOPS_RUNTIME_FIXTURE=synthetic-value"],
"Labels": {
"com.docker.compose.project": "unraid-appops-gateway",
"com.docker.compose.service": "appops-gateway",
@@ -0,0 +1,558 @@
from __future__ import annotations
import http.server
import importlib.util
import json
import os
import stat
import subprocess
import sys
import threading
import urllib.error
import urllib.request
import uuid
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
CONTROLLER = ROOT / "ops" / "gitea-deploy-broker"
@pytest.fixture
def controller(monkeypatch, tmp_path):
monkeypatch.syspath_prepend(str(CONTROLLER))
spec = importlib.util.spec_from_file_location(
"appops_self_upgrade_recovery_boundaries_test",
CONTROLLER / "status_server.py",
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
monkeypatch.setattr(module, "WEBHOOK_INBOX_ROOT", tmp_path / "inbox")
module.WEBHOOK_INBOX_ROOT.mkdir(mode=0o700)
monkeypatch.setattr(module, "APPOPS_PROGRESS_ROOT", tmp_path / "progress")
monkeypatch.setattr(module, "SELF_UPGRADE_RECOVERY_ROOT", tmp_path / "recovery")
monkeypatch.setattr(
module,
"SELF_UPGRADE_RECOVERY_REQUEST_ROOT",
tmp_path / "recovery" / "requests",
)
return module
def _private_recovery_payload(module, recovery_id: str) -> dict[str, object]:
revision = "c" * 40
return {
"schema": module._SELF_UPGRADE_RECOVERY_SCHEMA,
"recoveryId": recovery_id,
"semanticSha256": module._webhook_semantic_digest(
9, "refs/heads/main", revision,
),
"repository": module.SELF_UPGRADE_REPOSITORY,
"revision": revision,
"originalRawSha256": "1" * 64,
"idempotencyKeySha256": "2" * 64,
"preflightSha256": "3" * 64,
"expectedCurrentRevision": "4" * 40,
"currentContainerId": "5" * 64,
"currentImageId": "sha256:" + "6" * 64,
"currentSourceTree": "7" * 40,
"runtimeFingerprint": "8" * 64,
"registryFingerprint": "9" * 64,
"sourceFingerprint": "a" * 64,
"stateSha256": "b" * 64,
"runtimeEnvironment": {"API_TOKEN": "recovery-secret-sentinel"},
"createdAt": 1,
}
def _recovery_delivery(module, *, secret: str) -> dict[str, object]:
return {
"schema": module._WEBHOOK_SELF_UPGRADE_RECOVERY_SCHEMA,
"semantic_sha256": "a" * 64,
"recovery_of_sha256": "b" * 64,
"recovery_id": str(uuid.UUID("12345678-1234-4234-8234-123456789abc")),
"idempotency_key_sha256": "c" * 64,
"preflight_sha256": "d" * 64,
"body_sha256": "e" * 64,
"delivery": "22345678-1234-4234-8234-123456789abc",
"gitea_repository_id": 9,
"ref": "refs/heads/main",
"repository": module.SELF_UPGRADE_REPOSITORY,
"revision": "f" * 40,
"source": "typed_self_upgrade_recovery",
"status": "awaiting_validation",
"validation": {
"last_result": "wait",
"required_contexts": ["Managed validation / full (push)"],
"observed": [],
"checked_at": 1,
"deadline_at": 2,
},
"audit": [
{"at": 1, "stage": "self_upgrade_recovery_requested"},
{"at": 2, "stage": "awaiting_validation"},
],
"received_at": 1,
"updated_at": 2,
# These fields must remain private even if a caller or record contains them.
"runtimeEnvironment": {"API_TOKEN": secret},
"privateCredential": secret,
}
def test_ordinary_deploy_strips_ambient_recovery_binding_and_rejects_bad_bindings(
controller, monkeypatch,
):
ambient_secret = "ambient-recovery-secret-sentinel"
monkeypatch.setenv("APPOPS_SELF_UPGRADE_RECOVERY_ID", ambient_secret)
monkeypatch.setenv("APPOPS_REGISTERED_LEGACY_REQUEST_ID", ambient_secret)
calls = []
def fake_run(command, **kwargs):
calls.append((command, kwargs))
return subprocess.CompletedProcess(command, 0)
monkeypatch.setattr(controller.subprocess, "run", fake_run)
ordinary = controller._run_registered_deploy("jens/quiz-monitor", "c" * 40)
assert ordinary.returncode == 0
command, kwargs = calls.pop()
assert command[-3:] == ["deploy", "jens/quiz-monitor", "c" * 40]
assert "APPOPS_SELF_UPGRADE_RECOVERY_ID" not in kwargs["env"]
assert "APPOPS_REGISTERED_LEGACY_REQUEST_ID" not in kwargs["env"]
assert ambient_secret not in json.dumps(kwargs["env"])
recovery_id = str(uuid.UUID("12345678-1234-4234-8234-123456789abc"))
authorized = controller._run_registered_deploy(
controller.SELF_UPGRADE_REPOSITORY,
"c" * 40,
self_upgrade_recovery_id=recovery_id,
)
assert authorized.returncode == 0
_, kwargs = calls.pop()
assert kwargs["env"]["APPOPS_SELF_UPGRADE_RECOVERY_ID"] == recovery_id
assert "APPOPS_REGISTERED_LEGACY_REQUEST_ID" not in kwargs["env"]
invalid_bindings = [
(controller.SELF_UPGRADE_REPOSITORY, "not-a-uuid", None),
("jens/quiz-monitor", recovery_id, None),
(controller.SELF_UPGRADE_REPOSITORY, recovery_id, recovery_id),
]
for repository, bad_recovery_id, registered_id in invalid_bindings:
with pytest.raises(ValueError):
controller._run_registered_deploy(
repository,
"c" * 40,
self_upgrade_recovery_id=bad_recovery_id,
registered_legacy_request_id=registered_id,
)
assert calls == []
def test_preflight_projects_runtime_identity_without_environment_values(
controller, monkeypatch,
):
project = {
"branch": "main",
"gitea_repository_id": 9,
"containers": ["Unraid-AppOps-Gateway"],
"compose_project": "unraid-appops-gateway",
"compose_files": ["docker-compose.yml"],
"environment": {
"APPOPS_DATA_PATH": controller.SELF_UPGRADE_DATA_PATH,
"APPOPS_PUBLISH_HOST": controller.SELF_UPGRADE_PUBLISH_HOST,
},
"verify_oci_revision": True,
"validation": {
"validation_gate_version": 1,
"required": True,
"contexts": ["Managed validation / full (push)"],
"timeout_seconds": 1800,
"poll_interval_seconds": 5,
"max_poll_interval_seconds": 30,
},
}
private_environment = {"API_TOKEN": "preflight-secret-sentinel"}
monkeypatch.setattr(
controller,
"_load_projects",
lambda: {controller.SELF_UPGRADE_REPOSITORY: project},
)
monkeypatch.setattr(controller, "_self_upgrade_protected_head", lambda _revision: True)
monkeypatch.setattr(controller, "_is_current_branch_head", lambda *_args: True)
monkeypatch.setattr(
controller, "_fetch_gitea_validation", lambda *_args: ("pass", []),
)
monkeypatch.setattr(
controller,
"_self_upgrade_runtime",
lambda: {
"revision": "4" * 40,
"imageId": "sha256:" + "5" * 64,
"containerId": "6" * 64,
"imageRef": controller.SELF_UPGRADE_PRODUCTION_IMAGE_REF,
"sourceTree": "7" * 40,
"runtimeFingerprint": "8" * 64,
"runtimeEnvironment": private_environment,
},
)
monkeypatch.setattr(
controller.shutil,
"disk_usage",
lambda _path: type("Usage", (), {"free": 50 * 1024**3})(),
)
monkeypatch.setattr(controller, "_self_upgrade_protected_head", lambda _revision: True)
monkeypatch.setattr(controller, "_is_current_branch_head", lambda *_args: True)
monkeypatch.setattr(
controller, "_fetch_gitea_validation", lambda *_args: ("pass", []),
)
monkeypatch.setattr(
controller, "_validation_policy",
lambda _repository, _project: {
"contexts": ["Managed validation / full (push)"],
},
)
monkeypatch.setattr(controller, "_self_upgrade_signature_is_valid", lambda *_a, **_k: True)
monkeypatch.setattr(controller.Handler, "log_message", lambda *args: None)
result = controller._self_upgrade_preflight("c" * 40)
serialized = json.dumps(result)
assert result["status"] == "preflight_ok"
assert "runtimeEnvironment" not in result
assert "API_TOKEN" not in serialized
assert "preflight-secret-sentinel" not in serialized
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), controller.Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
body = json.dumps({
"repository": controller.SELF_UPGRADE_REPOSITORY,
"revision": "c" * 40,
"reason": "preflight boundary test",
"expectedCurrentRevision": None,
}).encode("utf-8")
request = urllib.request.Request(
f"http://127.0.0.1:{server.server_port}{controller.SELF_UPGRADE_PREFLIGHT_PATH}",
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=5) as response:
response_body = response.read()
assert response.status == 200
response_payload = json.loads(response_body)
serialized_response = response_body.decode("utf-8")
assert response_payload["status"] == "preflight_ok"
assert "runtimeEnvironment" not in response_payload
assert "API_TOKEN" not in serialized_response
assert "preflight-secret-sentinel" not in serialized_response
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
def test_public_recovery_status_projects_away_private_environment(controller, monkeypatch):
secret = "public-status-secret-sentinel"
record = _recovery_delivery(controller, secret=secret)
monkeypatch.setattr(controller, "_self_upgrade_delivery_record", lambda _request_id: record)
monkeypatch.setattr(controller, "_self_upgrade_signature_is_valid", lambda *_a, **_k: True)
monkeypatch.setattr(controller, "_read_inbox_record", lambda _path: None)
monkeypatch.setattr(controller.Handler, "log_message", lambda *args: None)
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), controller.Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
request = urllib.request.Request(
f"http://127.0.0.1:{server.server_port}"
f"{controller.SELF_UPGRADE_STATUS_PREFIX}{'a' * 64}",
method="GET",
)
try:
with urllib.request.urlopen(request, timeout=5) as response:
status = response.status
body = response.read()
except urllib.error.HTTPError as error:
status = error.code
body = error.read()
assert status == 200, body.decode("utf-8", errors="replace")
view = json.loads(body)
assert view["repository"] == controller.SELF_UPGRADE_REPOSITORY
assert view["revision"] == "f" * 40
assert view["attemptType"] == "self-upgrade-recovery"
assert view["recoveryOf"] == "b" * 64
assert not {
"recovery_id", "recovery_of_sha256", "idempotency_key_sha256",
"preflight_sha256", "body_sha256", "recovery_hmac",
} & view.keys()
assert "runtimeEnvironment" not in view
assert "privateCredential" not in view
assert "recovery_hmac" not in view
assert "idempotency_key_sha256" not in view
assert secret.encode() not in body
assert "API_TOKEN" not in body.decode("utf-8")
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
def test_private_recovery_journal_is_0600_and_hmac_tampering_fails_closed(
controller, monkeypatch, tmp_path,
):
if os.name == "nt" or os.geteuid() != 0:
pytest.skip("The controller recovery store requires root-owned POSIX files")
recovery_root = tmp_path / "recovery-store"
recovery_root.mkdir(mode=0o700)
recovery_root.chmod(0o700)
monkeypatch.setattr(controller, "SELF_UPGRADE_RECOVERY_ROOT", recovery_root)
secret = b"fixed-self-upgrade-recovery-test-key-0123456789"
monkeypatch.setattr(controller, "_self_upgrade_secret", lambda: secret)
recovery_id = str(uuid.UUID("12345678-1234-4234-8234-123456789abc"))
payload = _private_recovery_payload(controller, recovery_id)
controller._self_upgrade_write_private_journal(recovery_id, payload, secret)
marker = recovery_root / f"{recovery_id}.json"
assert stat.S_IMODE(marker.stat().st_mode) == 0o600
assert controller._self_upgrade_recovery_journal(recovery_id) == payload
envelope = json.loads(marker.read_text(encoding="utf-8"))
envelope["payload"]["runtimeEnvironment"]["API_TOKEN"] = "changed-secret"
marker.write_text(json.dumps(envelope, sort_keys=True, separators=(",", ":")) + "\n")
marker.chmod(0o600)
assert controller._self_upgrade_recovery_journal(recovery_id) is None
envelope = controller._self_upgrade_recovery_envelope(payload, secret)
marker.write_text(json.dumps(envelope, sort_keys=True, separators=(",", ":")) + "\n")
marker.chmod(0o644)
assert controller._self_upgrade_recovery_journal(recovery_id) is None
def test_recovery_request_is_immutable_and_idempotent_for_one_key(
controller, monkeypatch, tmp_path,
):
if os.name == "nt" or os.geteuid() != 0:
pytest.skip("The controller recovery store requires root-owned POSIX files")
recovery_root = tmp_path / "recovery-store"
recovery_root.mkdir(mode=0o700)
recovery_root.chmod(0o700)
request_root = recovery_root / "requests"
monkeypatch.setattr(controller, "SELF_UPGRADE_RECOVERY_ROOT", recovery_root)
monkeypatch.setattr(controller, "SELF_UPGRADE_RECOVERY_REQUEST_ROOT", request_root)
secret = b"fixed-self-upgrade-recovery-test-key-0123456789"
monkeypatch.setattr(controller, "_self_upgrade_secret", lambda: secret)
revision = "c" * 40
semantic = controller._webhook_semantic_digest(9, "refs/heads/main", revision)
original_path = tmp_path / "original-failed-delivery.json"
original_raw = b'{"immutable":"original failed delivery"}\n'
original_path.write_bytes(original_raw)
original = {"revision": revision}
original_info = (original_path, original_raw, original)
monkeypatch.setattr(
controller,
"_self_upgrade_original_failed_delivery",
lambda *_args: original_info,
)
monkeypatch.setattr(
controller, "_self_upgrade_delivery_revision", lambda _semantic: revision,
)
expected_preflight = "3" * 64
expected_current_revision = controller.SELF_UPGRADE_OLD_REVISION
expected_container_id = "5" * 64
expected_image_id = "sha256:" + "6" * 64
expected_source_tree = "7" * 40
expected_runtime_fingerprint = "8" * 64
private_environment = {"API_TOKEN": "journal-secret-sentinel"}
fresh = {
"status": "preflight_ok",
"preflightSha256": expected_preflight,
"currentRevision": expected_current_revision,
"currentContainerId": expected_container_id,
"currentImageId": expected_image_id,
"currentSourceTree": expected_source_tree,
"currentRuntimeFingerprint": expected_runtime_fingerprint,
}
bindings = {
"currentRevision": expected_current_revision,
"currentContainerId": expected_container_id,
"currentImageId": expected_image_id,
"currentSourceTree": expected_source_tree,
"runtimeFingerprint": expected_runtime_fingerprint,
"registryFingerprint": "9" * 64,
"sourceFingerprint": "a" * 64,
"stateSha256": "b" * 64,
"runtimeEnvironment": private_environment,
}
monkeypatch.setattr(controller, "_self_upgrade_preflight", lambda _revision: fresh)
monkeypatch.setattr(
controller, "_self_upgrade_recovery_current_bindings", lambda: bindings,
)
policy = {
"repository": controller.SELF_UPGRADE_REPOSITORY,
"validation_gate_version": 1,
"contexts": ["Managed validation / full (push)"],
"timeout_seconds": 1800,
"poll_interval_seconds": 5,
"max_poll_interval_seconds": 30,
}
project = {"slug": "unraid-appops-gateway"}
monkeypatch.setattr(
controller, "_load_projects",
lambda: {controller.SELF_UPGRADE_REPOSITORY: project},
)
monkeypatch.setattr(
controller,
"_validation_policy",
lambda _repository, _project: policy,
)
original_sha = controller.hashlib.sha256(original_raw).hexdigest()
idempotency_key_sha = "2" * 64
reason_sha = "4" * 64
first = controller._self_upgrade_recovery_request_status(
semantic,
original_sha,
idempotency_key_sha,
expected_preflight,
expected_current_revision,
reason_sha,
policy,
)
assert first == ("awaiting_validation", semantic)
assert original_path.read_bytes() == original_raw
def store_snapshot() -> dict[str, bytes]:
return {
path.relative_to(recovery_root).as_posix(): path.read_bytes()
for path in sorted(recovery_root.rglob("*.json"))
}
accepted_bytes = store_snapshot()
assert accepted_bytes
assert stat.S_IMODE(recovery_root.stat().st_mode) == 0o700
assert stat.S_IMODE(request_root.stat().st_mode) == 0o700
assert all(
stat.S_IMODE(path.stat().st_mode) == 0o600
for path in recovery_root.rglob("*.json")
)
# The secret is intentionally present only in the root-owned HMAC journal.
recovery_id = str(uuid.UUID(bytes=bytes.fromhex(idempotency_key_sha)[:16], version=4))
journal_bytes = accepted_bytes[f"{recovery_id}.json"]
assert b"journal-secret-sentinel" in journal_bytes
assert b"journal-secret-sentinel" not in accepted_bytes[
f"{semantic}.delivery.json"
]
assert b"journal-secret-sentinel" not in accepted_bytes[
f"requests/{idempotency_key_sha}.json"
]
same_key = controller._self_upgrade_recovery_request_status(
semantic,
original_sha,
idempotency_key_sha,
expected_preflight,
expected_current_revision,
reason_sha,
policy,
)
assert same_key == ("awaiting_validation", semantic)
assert store_snapshot() == accepted_bytes
assert original_path.read_bytes() == original_raw
different_key = controller._self_upgrade_recovery_request_status(
semantic,
original_sha,
"d" * 64,
expected_preflight,
expected_current_revision,
reason_sha,
policy,
)
assert different_key == ("conflict", semantic)
assert store_snapshot() == accepted_bytes
assert original_path.read_bytes() == original_raw
@pytest.mark.parametrize(
"malformed",
[b"{", b"[]", b'{"duplicate":1,"duplicate":2}'],
)
def test_legacy_json_parser_rejects_malformed_or_duplicate_records(
controller, monkeypatch, malformed,
):
monkeypatch.setattr(
controller,
"_self_upgrade_read_legacy_cache_file",
lambda *_args, **_kwargs: malformed,
)
with pytest.raises((OSError, ValueError)):
controller._self_upgrade_read_legacy_cache_json(
Path("/mnt/cache/appdata/gitea-deploy-runners/state/webhook-inbox")
/ ("a" * 64 + ".json"),
maximum_bytes=65536,
allowed_modes=frozenset({0o600}),
)
def test_original_failed_delivery_rejects_modified_legacy_record(controller, monkeypatch):
revision = "c" * 40
semantic = controller._webhook_semantic_digest(9, "refs/heads/main", revision)
policy = {
"repository": controller.SELF_UPGRADE_REPOSITORY,
"validation_gate_version": 1,
"contexts": ["Managed validation / full (push)"],
"timeout_seconds": 1800,
"poll_interval_seconds": 5,
"max_poll_interval_seconds": 30,
}
validation = controller._validation_snapshot(policy, 1)
validation["last_result"] = "pass"
validation["observed"] = [{
"context": "Managed validation / full (push)",
"status": "success",
}]
record = {
"schema": controller._WEBHOOK_SCHEMA,
"semantic_sha256": semantic,
"gitea_repository_id": 9,
"ref": "refs/heads/main",
"repository": controller.SELF_UPGRADE_REPOSITORY,
"revision": revision,
"status": "failed",
"reason": "deployment_failed",
"source": "gitea_webhook",
"body_sha256": "1" * 64,
"validation": validation,
"audit": [
{"stage": "validation_passed"},
{"stage": "deployment_started"},
{"stage": "deployment_failed", "reason": "deployment_failed"},
],
}
raw = json.dumps(record, sort_keys=True, separators=(",", ":")).encode()
monkeypatch.setattr(
controller,
"_self_upgrade_read_legacy_cache_json",
lambda *_args, **_kwargs: (raw, record),
)
assert controller._self_upgrade_original_failed_delivery(
semantic, revision, policy,
) == (controller.WEBHOOK_INBOX_ROOT / f"{semantic}.json", raw, record)
tampered = {**record, "revision": "d" * 40}
monkeypatch.setattr(
controller,
"_self_upgrade_read_legacy_cache_json",
lambda *_args, **_kwargs: (raw, tampered),
)
assert controller._self_upgrade_original_failed_delivery(
semantic, revision, policy,
) is None
@@ -0,0 +1,815 @@
from __future__ import annotations
import hashlib
import importlib.util
import json
import os
import stat
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[2]
CONTROLLER = ROOT / "ops" / "gitea-deploy-broker"
RECOVERY_ID = "12345678-1234-4234-8234-123456789abc"
REVISION = "c" * 40
TREE = "d" * 40
IMAGE_ID = "sha256:" + "e" * 64
CONTAINER_ID = "f" * 64
BUILT_AT = "2026-10-07T00:00:00.000Z"
SECRET_SENTINEL = "model-private-secret$literal$$value"
@pytest.fixture
def controller(monkeypatch, tmp_path):
monkeypatch.syspath_prepend(str(CONTROLLER))
spec = importlib.util.spec_from_file_location(
"appops_self_upgrade_runtime_model_test",
CONTROLLER / "status_server.py",
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
recovery_root = tmp_path / "recovery"
monkeypatch.setattr(module, "SELF_UPGRADE_RECOVERY_ROOT", recovery_root)
monkeypatch.setattr(module, "SELF_UPGRADE_RECOVERY_REQUEST_ROOT", recovery_root / "requests")
monkeypatch.setattr(module, "SELF_UPGRADE_RECOVERY_MODEL_ROOT", recovery_root / "models")
monkeypatch.setattr(module, "SELF_UPGRADE_RECOVERY_TRANSACTION_ROOT", recovery_root / "transactions")
secret = b"runtime-model-test-secret-key-0123456789"
monkeypatch.setattr(module, "_self_upgrade_secret", lambda: secret)
return module, recovery_root, secret
def _environment(revision: str = REVISION, tree: str = TREE) -> dict[str, str]:
return {
"APPOPS_SOURCE_REPOSITORY": "jens/unraid-appops-gateway",
"APPOPS_SOURCE_REVISION": revision,
"APPOPS_SOURCE_TREE": tree,
"APPOPS_BUILD_ID": revision[:12],
"APPOPS_BUILD_DATE": BUILT_AT,
"KNOWLEDGE_OPTIMIZER_MODEL": "gpt-6-luna",
"OPERATOR_TOKEN": SECRET_SENTINEL,
}
def _model(revision: str = REVISION, tree: str = TREE) -> dict[str, object]:
return {
"name": "unraid-appops-gateway",
"services": {
"appops-gateway": {
"image": "unraid-appops-gateway:production",
"container_name": "Unraid-AppOps-Gateway",
"environment": _environment(revision, tree),
"ports": [{"target": 1216, "published": "1216", "host_ip": "198.18.10.150"}],
},
},
}
def _legacy_environment() -> dict[str, str]:
return {
**{f"LEGACY_SETTING_{index:02d}": f"fixture-value-{index:02d}" for index in range(43)},
"KNOWLEDGE_OPTIMIZER_MODEL": "gpt-6-luna",
"APPOPS_FRONTEND_ORIGIN": "https://apps.example.test",
"OPERATOR_TOKEN": SECRET_SENTINEL,
}
def _legacy_model(module, image_ref: str | None = None) -> dict[str, object]:
return {
"name": "unraid-appops-gateway",
"services": {
"appops-gateway": {
"image": image_ref or module.SELF_UPGRADE_OLD_IMAGE_REF,
"container_name": "Unraid-AppOps-Gateway",
"environment": _legacy_environment(),
"ports": [{
"target": 1216, "published": "1216", "host_ip": "198.18.10.150",
}],
},
},
}
def _mock_legacy_docker(monkeypatch, module):
environment = _legacy_environment()
raw_env = [f"{key}={value}" for key, value in environment.items()]
raw_env.append(f"APPOPS_FRONTEND_ORIGIN={environment['APPOPS_FRONTEND_ORIGIN']}")
image_labels = {
"org.opencontainers.image.source": module.SELF_UPGRADE_REPOSITORY,
"org.opencontainers.image.revision": module.SELF_UPGRADE_OLD_REVISION,
"org.opencontainers.image.source-tree": module.SELF_UPGRADE_OLD_TREE,
"org.opencontainers.image.version": module.SELF_UPGRADE_OLD_REVISION[:12],
"org.opencontainers.image.created": "2026-09-01T00:00:00.000Z",
}
container = {
"Id": CONTAINER_ID,
"Image": module.SELF_UPGRADE_IMAGE_ID,
"State": {"Running": True, "Health": {"Status": "healthy"}},
"Mounts": [
{"Type": "bind", "Source": module.SELF_UPGRADE_DATA_PATH,
"Destination": "/data", "RW": True},
{"Type": "bind", "Source": module.SELF_UPGRADE_MODELS_PATH,
"Destination": module.SELF_UPGRADE_MODELS_TARGET, "RW": True},
],
"Config": {
"Image": module.SELF_UPGRADE_OLD_IMAGE_REF,
"Hostname": "unraid-appops-gateway",
"Env": raw_env,
"Labels": {
**image_labels,
"com.docker.compose.project": "unraid-appops-gateway",
"com.docker.compose.service": "appops-gateway",
"com.docker.compose.project.working_dir": "/srv/example/appdata/unraid-appops-gateway/source",
"com.docker.compose.project.config_files": "/srv/example/appdata/unraid-appops-gateway/source/docker-compose.yml",
},
},
"HostConfig": {"RestartPolicy": {"Name": "unless-stopped"}},
"NetworkSettings": {
"Ports": {"1216/tcp": [{"HostIp": module.SELF_UPGRADE_PUBLISH_HOST, "HostPort": "1216"}]},
"Networks": {
"itworx-mcp-hub-npm": {},
"unraid-appops-gateway_default": {},
},
},
}
image = {"Id": module.SELF_UPGRADE_IMAGE_ID, "Config": {"Labels": image_labels}}
def fake_run(args, **_kwargs):
if args[:3] == ["docker", "ps", "-a"]:
return SimpleNamespace(stdout="Unraid-AppOps-Gateway\n")
if args[:2] == ["docker", "inspect"]:
return SimpleNamespace(stdout=json.dumps([container]))
if args[:3] == ["docker", "image", "inspect"]:
return SimpleNamespace(stdout=json.dumps([image]))
raise AssertionError("unexpected Docker command category")
monkeypatch.setattr(module.subprocess, "run", fake_run)
monkeypatch.setattr(module, "_load_projects", lambda: {
module.SELF_UPGRADE_REPOSITORY: {
"containers": ["Unraid-AppOps-Gateway"], "verify_oci_revision": True,
},
})
monkeypatch.setattr(
module, "_self_upgrade_read_legacy_cache_json",
lambda *_args, **_kwargs: (b"{}", {"live_artifacts": {"containers": {}}}),
)
monkeypatch.setattr(module, "_self_upgrade_source_is_valid", lambda _source: True)
monkeypatch.setattr(module, "_self_upgrade_source_fingerprint", lambda *_args: "2" * 64)
return container
def _receipt(module, revision: str = REVISION, tree: str = TREE) -> dict[str, object]:
digest = hashlib.sha256(f"git-tree:{tree}".encode()).hexdigest()
return {
"schema": "gitea-deploy-receipt.v1",
"repository": module.SELF_UPGRADE_REPOSITORY,
"revision": revision,
"tree": tree,
"treeDigest": digest,
"build": {
"id": revision[:12],
"repository": module.SELF_UPGRADE_REPOSITORY,
"revision": revision,
"tree": tree,
"treeDigest": digest,
"builtAt": BUILT_AT,
},
"containers": {
"Unraid-AppOps-Gateway": {
"image_id": IMAGE_ID,
"image_revision": revision,
"image_source": module.SELF_UPGRADE_REPOSITORY,
"image_tree": tree,
"image_version": revision[:12],
"image_created": BUILT_AT,
},
},
"live": None,
}
def _binding(module, receipt, *, container_id: str = CONTAINER_ID) -> dict[str, object]:
return {
"containerId": container_id,
"imageId": IMAGE_ID,
"runtimeFingerprint": "1" * 64,
"sourceFingerprint": "2" * 64,
"receiptSha256": hashlib.sha256(module._self_upgrade_canonical_json(receipt)).hexdigest(),
}
def test_plain_compose_file_preserves_literal_dollars_under_private_attestation(
controller,
):
module, recovery_root, _secret = controller
if os.name == "nt" or os.geteuid() != 0:
pytest.skip("The controller model store requires root-owned POSIX files")
pointer = module._self_upgrade_store_compose_model(RECOVERY_ID, REVISION, _model())
assert isinstance(pointer, dict)
loaded = module._self_upgrade_load_compose_model(RECOVERY_ID, REVISION)
assert loaded is not None
assert loaded["model"] == _model()
assert loaded["pointer"] == pointer
assert Path(loaded["path"]).name == f"{RECOVERY_ID}-{REVISION}.compose.json"
compose_path = Path(loaded["path"])
attestation_path = compose_path.with_name(
f"{RECOVERY_ID}-{REVISION}.model.json"
)
compose_raw = compose_path.read_bytes()
attestation_raw = attestation_path.read_bytes()
assert b"model-private-secret$$literal$$$$value" in compose_raw
assert SECRET_SENTINEL.encode() not in compose_raw
assert SECRET_SENTINEL.encode() not in attestation_raw
assert b"OPERATOR_TOKEN" not in attestation_raw
assert stat.S_IMODE(compose_path.stat().st_mode) == 0o600
assert stat.S_IMODE(attestation_path.stat().st_mode) == 0o600
assert stat.S_IMODE(recovery_root.stat().st_mode) == 0o700
assert stat.S_IMODE((recovery_root / "models").stat().st_mode) == 0o700
assert module._self_upgrade_store_compose_model(RECOVERY_ID, REVISION, _model()) == pointer
assert module._self_upgrade_store_compose_model(
RECOVERY_ID, REVISION, _model(tree="a" * 40),
) is None
compose_path.write_text(compose_path.read_text(encoding="utf-8").replace("gpt-6-luna", "gpt-5.5"))
compose_path.chmod(0o600)
assert module._self_upgrade_load_compose_model(RECOVERY_ID, REVISION) is None
def test_live_model_records_are_content_addressed_and_activate_only_from_state(
controller, monkeypatch,
):
module, _recovery_root, _secret = controller
if os.name == "nt" or os.geteuid() != 0:
pytest.skip("The controller model store requires root-owned POSIX files")
pointer = module._self_upgrade_store_compose_model(RECOVERY_ID, REVISION, _model())
assert isinstance(pointer, dict)
receipt = _receipt(module)
live_pointer = module._self_upgrade_store_live_model(pointer, _binding(module, receipt))
assert isinstance(live_pointer, dict)
first_live = module._self_upgrade_read_live_model_reference(live_pointer)
assert first_live is not None
assert first_live["model"] == _model()
fresh_container_id = "a" * 64
restored_pointer = module._self_upgrade_store_live_model(
pointer, _binding(module, receipt, container_id=fresh_container_id),
)
assert isinstance(restored_pointer, dict)
assert restored_pointer["liveRecordSha256"] != live_pointer["liveRecordSha256"]
assert module._self_upgrade_read_live_model_reference(live_pointer) == first_live
restored = module._self_upgrade_read_live_model_reference(restored_pointer)
assert restored is not None
assert restored["live"]["containerId"] == fresh_container_id
state = {
"schema_version": 3,
"repository": module.SELF_UPGRADE_REPOSITORY,
"status": "failed",
"live_revision": REVISION,
"live_status": "healthy",
"live_legacy_provenance": False,
"live_receipt": receipt,
"live_artifacts": {
"containers": {
"Unraid-AppOps-Gateway": {
"container_id": fresh_container_id,
"image_id": IMAGE_ID,
},
},
"appops_runtime_model": restored_pointer,
},
}
project = {
"containers": ["Unraid-AppOps-Gateway"],
"verify_oci_revision": True,
}
active = module._self_upgrade_active_compose_model(state, project=project)
assert active is not None
assert active["model"] == _model()
runtime = {
"revision": REVISION,
"containerId": fresh_container_id,
"imageId": IMAGE_ID,
"runtimeFingerprint": "1" * 64,
"sourceFingerprint": "2" * 64,
"runtimeEnvironment": _environment(),
}
assert module._self_upgrade_active_compose_model(state, runtime, project=project) is not None
runtime["runtimeEnvironment"] = {**_environment(), "KNOWLEDGE_OPTIMIZER_MODEL": "gpt-5.5"}
assert module._self_upgrade_active_compose_model(state, runtime, project=project) is None
state["live_status"] = "unhealthy"
assert module._self_upgrade_active_compose_model(state, project=project) is None
def test_candidate_binding_rechecks_exact_model_receipt_and_runtime(
controller, monkeypatch,
):
module, _recovery_root, _secret = controller
if os.name == "nt" or os.geteuid() != 0:
pytest.skip("The controller model store requires root-owned POSIX files")
pointer = module._self_upgrade_store_compose_model(RECOVERY_ID, REVISION, _model())
assert isinstance(pointer, dict)
receipt = _receipt(module)
project = {
"containers": ["Unraid-AppOps-Gateway"],
"verify_oci_revision": True,
}
monkeypatch.setattr(module, "_load_projects", lambda: {module.SELF_UPGRADE_REPOSITORY: project})
monkeypatch.setattr(module, "_self_upgrade_runtime", lambda **_kwargs: {
"revision": REVISION,
"sourceRepository": module.SELF_UPGRADE_REPOSITORY,
"sourceTree": TREE,
"imageId": IMAGE_ID,
"containerId": CONTAINER_ID,
"runtimeFingerprint": "1" * 64,
"sourceFingerprint": "2" * 64,
"runtimeEnvironment": _environment(),
})
binding = module._self_upgrade_candidate_model_binding(pointer, receipt)
assert binding == _binding(module, receipt)
wrong_receipt = _receipt(module, tree="b" * 40)
assert module._self_upgrade_candidate_model_binding(pointer, wrong_receipt) is None
bad_pointer = {**pointer, "modelSha256": "0" * 64}
assert module._self_upgrade_candidate_model_binding(bad_pointer, receipt) is None
def test_fixed_555_model_preserves_exact_environment_and_duplicate_origin(
controller, monkeypatch,
):
module, _recovery_root, _secret = controller
if os.name == "nt" or os.geteuid() != 0:
pytest.skip("The controller model store requires root-owned POSIX files")
journal = {
"expectedCurrentRevision": module.SELF_UPGRADE_OLD_REVISION,
"currentImageId": module.SELF_UPGRADE_IMAGE_ID,
"runtimeEnvironment": _legacy_environment(),
}
monkeypatch.setattr(module, "_self_upgrade_recovery_journal", lambda _rid: journal)
pointer = module._self_upgrade_store_compose_model(
RECOVERY_ID, module.SELF_UPGRADE_OLD_REVISION, _legacy_model(module),
)
assert isinstance(pointer, dict)
loaded = module._self_upgrade_load_compose_model(
RECOVERY_ID, module.SELF_UPGRADE_OLD_REVISION,
)
assert loaded is not None
assert loaded["model"] == _legacy_model(module)
changed = _legacy_model(module)
changed["services"]["appops-gateway"]["environment"]["APPOPS_BUILD_ID"] = "untrusted"
assert not module._self_upgrade_compose_model_valid(
changed, module.SELF_UPGRADE_OLD_REVISION,
)
container = _mock_legacy_docker(monkeypatch, module)
initial_runtime = module._self_upgrade_runtime()
assert initial_runtime is not None
assert initial_runtime["runtimeEnvironment"] == _legacy_environment()
assert len(container["Config"]["Env"]) == 47
assert len(initial_runtime["runtimeEnvironment"]) == 46
runtime = module._self_upgrade_runtime(
authorized_model_pointer=pointer,
allow_legacy_canonical_config=True,
)
assert runtime is not None
assert runtime["runtimeEnvironment"] == _legacy_environment()
assert len(runtime["runtimeEnvironment"]) == 46
assert len(container["Config"]["Env"]) == 47
assert runtime["imageId"] == module.SELF_UPGRADE_IMAGE_ID
assert runtime["containerId"] == container["Id"]
binding = module._self_upgrade_legacy_model_binding(pointer)
assert binding == {
"containerId": CONTAINER_ID,
"imageId": module.SELF_UPGRADE_IMAGE_ID,
"runtimeFingerprint": runtime["runtimeFingerprint"],
"sourceFingerprint": "2" * 64,
"receiptSha256": hashlib.sha256(module._self_upgrade_canonical_json({})).hexdigest(),
}
live_pointer = module._self_upgrade_store_live_model(pointer, binding)
assert isinstance(live_pointer, dict)
state = {
"schema_version": 3,
"repository": module.SELF_UPGRADE_REPOSITORY,
"status": "rolled-back",
"live_revision": module.SELF_UPGRADE_OLD_REVISION,
"live_status": "healthy",
"live_legacy_provenance": True,
"live_receipt": {},
"live_artifacts": {
"containers": {
"Unraid-AppOps-Gateway": {
"container_id": CONTAINER_ID,
"image_id": module.SELF_UPGRADE_IMAGE_ID,
},
},
"appops_runtime_model": live_pointer,
},
}
project = {
"containers": ["Unraid-AppOps-Gateway"],
"verify_oci_revision": True,
}
active = module._self_upgrade_active_compose_model(state, project=project)
assert active is not None
assert active["legacy"] is True
assert active["model"] == _legacy_model(module)
assert module._self_upgrade_active_compose_model(state, runtime, project=project) is not None
container["State"]["Health"]["Status"] = "unhealthy"
assert module._self_upgrade_runtime() is None
assert module._self_upgrade_model_runtime_identity(pointer, module.SELF_UPGRADE_IMAGE_ID) is None
recovery_identity = module._self_upgrade_model_runtime_identity(
pointer, module.SELF_UPGRADE_IMAGE_ID, require_healthy=False,
)
assert isinstance(recovery_identity, dict)
assert set(recovery_identity) == {
"containerId", "imageId", "revision", "sourceRepository", "sourceTree",
"runtimeFingerprint", "sourceFingerprint", "runtimeCasSha256",
}
assert module._self_upgrade_model_runtime_identity(
pointer, module.SELF_UPGRADE_IMAGE_ID, require_healthy="false",
) is None
container["State"]["Health"]["Status"] = "healthy"
# The identical duplicate is accepted only for the exact immutable old
# image; conflicting duplicates remain fail-closed.
raw_env = container["Config"]["Env"]
raw_env[-1] = "APPOPS_FRONTEND_ORIGIN=https://attacker.invalid"
assert module._self_upgrade_runtime(
authorized_model_pointer=pointer,
allow_legacy_canonical_config=True,
) is None
def test_fixed_555_production_reference_is_valid_only_under_signed_legacy_model(
controller, monkeypatch,
):
module, _recovery_root, _secret = controller
if os.name == "nt" or os.geteuid() != 0:
pytest.skip("The controller model store requires root-owned POSIX files")
journal = {
"expectedCurrentRevision": module.SELF_UPGRADE_OLD_REVISION,
"currentImageId": module.SELF_UPGRADE_IMAGE_ID,
"runtimeEnvironment": _legacy_environment(),
}
monkeypatch.setattr(module, "_self_upgrade_recovery_journal", lambda _rid: journal)
model = _legacy_model(module, module.SELF_UPGRADE_PRODUCTION_IMAGE_REF)
pointer = module._self_upgrade_store_compose_model(
"42345678-1234-4234-8234-123456789abc",
module.SELF_UPGRADE_OLD_REVISION,
model,
)
assert isinstance(pointer, dict)
container = _mock_legacy_docker(monkeypatch, module)
# The pre-capture container can retain the old platform tag while the
# signed effective rollback model uses the closed production tag.
container["Config"]["Image"] = module.SELF_UPGRADE_OLD_IMAGE_REF
initial_runtime = module._self_upgrade_runtime()
assert initial_runtime is not None
assert initial_runtime["imageRef"] == module.SELF_UPGRADE_OLD_IMAGE_REF
runtime = module._self_upgrade_runtime(
authorized_model_pointer=pointer,
allow_legacy_canonical_config=True,
)
assert isinstance(runtime, dict)
assert runtime["imageId"] == module.SELF_UPGRADE_IMAGE_ID
assert runtime["imageRef"] == module.SELF_UPGRADE_OLD_IMAGE_REF
binding = module._self_upgrade_legacy_model_binding(pointer)
assert isinstance(binding, dict)
reference = module._self_upgrade_store_live_model(pointer, binding)
assert isinstance(reference, dict)
project = {
"containers": ["Unraid-AppOps-Gateway"],
"verify_oci_revision": True,
}
monkeypatch.setattr(module, "_load_projects", lambda: {
module.SELF_UPGRADE_REPOSITORY: project,
})
snapshot = {
"revision": module.SELF_UPGRADE_OLD_REVISION,
"legacy_provenance": True,
"receipt": {},
"appops_runtime_model": reference,
"containers": [{
"name": "Unraid-AppOps-Gateway",
"container_id": CONTAINER_ID,
"image_id": module.SELF_UPGRADE_IMAGE_ID,
"image_ref": module.SELF_UPGRADE_OLD_IMAGE_REF,
"rollback_ref": "gitea-deploy-rollback/unraid-appops-gateway-0:5553c04ce497",
"restore_image_ref": module.SELF_UPGRADE_PRODUCTION_IMAGE_REF,
"was_running": True,
}],
}
active_state = {
"schema_version": 3,
"repository": module.SELF_UPGRADE_REPOSITORY,
"status": "rolled-back",
"live_revision": module.SELF_UPGRADE_OLD_REVISION,
"live_status": "healthy",
"live_legacy_provenance": True,
"live_receipt": {},
"live_artifacts": {
"appops_runtime_model": reference,
"containers": {
"Unraid-AppOps-Gateway": {
"container_id": CONTAINER_ID,
"image_id": module.SELF_UPGRADE_IMAGE_ID,
},
},
},
}
monkeypatch.setattr(
module,
"_self_upgrade_read_legacy_cache_json",
lambda *_args, **_kwargs: (b"{}", active_state),
)
assert module._self_upgrade_runtime() is not None
verified = module._self_upgrade_verify_rollback_model(reference, snapshot)
assert verified is not None
assert verified["model"] == model
assert verified["legacy"] is True
container["Config"]["Image"] = module.SELF_UPGRADE_PRODUCTION_IMAGE_REF
production_model = module._self_upgrade_load_compose_model(
pointer["recoveryId"], pointer["revision"],
)
assert production_model is not None
container["Config"]["Labels"]["com.docker.compose.project.config_files"] = str(
production_model["path"],
)
restored_runtime = module._self_upgrade_runtime(
authorized_model_pointer=pointer,
allow_legacy_canonical_config=True,
)
assert isinstance(restored_runtime, dict)
assert restored_runtime["imageRef"] == module.SELF_UPGRADE_PRODUCTION_IMAGE_REF
def test_recovery_transaction_binds_authorized_555_preimage_and_holds_pending_state(
controller, monkeypatch,
):
module, recovery_root, secret = controller
if os.name == "nt" or os.geteuid() != 0:
pytest.skip("The controller transaction store requires root-owned POSIX files")
recovery_root.mkdir(mode=0o700, parents=True, exist_ok=True)
semantic = module._webhook_semantic_digest(9, "refs/heads/main", REVISION)
original_record = {
"schema": module._WEBHOOK_SCHEMA,
"semantic_sha256": semantic,
"revision": REVISION,
"status": "failed",
"reason": "deployment_failed",
}
original_raw = module._self_upgrade_canonical_json(original_record) + b"\n"
original_sha = hashlib.sha256(original_raw).hexdigest()
journal = {
"schema": module._SELF_UPGRADE_RECOVERY_SCHEMA,
"recoveryId": RECOVERY_ID,
"semanticSha256": semantic,
"repository": module.SELF_UPGRADE_REPOSITORY,
"revision": REVISION,
"originalRawSha256": original_sha,
"idempotencyKeySha256": "3" * 64,
"preflightSha256": "7" * 64,
"expectedCurrentRevision": module.SELF_UPGRADE_OLD_REVISION,
"runtimeEnvironment": _legacy_environment(),
"sourceFingerprint": "4" * 64,
"runtimeFingerprint": "5" * 64,
"registryFingerprint": "b" * 64,
"currentContainerId": CONTAINER_ID,
"currentImageId": module.SELF_UPGRADE_IMAGE_ID,
"stateSha256": "a" * 64,
"currentSourceTree": module.SELF_UPGRADE_OLD_TREE,
"createdAt": 1,
}
monkeypatch.setattr(module, "_self_upgrade_model_transaction_origin_valid", lambda _payload: True)
module._self_upgrade_write_private_journal(RECOVERY_ID, journal, secret)
request_root = recovery_root / "requests"
request_root.mkdir(mode=0o700)
request_payload = {
"schema": module._SELF_UPGRADE_RECOVERY_REQUEST_SCHEMA,
"revision": REVISION,
"preflightSha256": "7" * 64,
"operationId": semantic,
"reasonSha256": "8" * 64,
"idempotencyKeySha256": "3" * 64,
"recoveryId": RECOVERY_ID,
"recoveryOfSha256": original_sha,
"createdAt": 1,
}
request_record = {
**request_payload,
"hmac": module._self_upgrade_mac(
secret, "recovery-request", module._self_upgrade_canonical_json(request_payload),
),
}
module._self_upgrade_write_private_record(
request_root / f"{'3' * 64}.json",
module._self_upgrade_canonical_json(request_record) + b"\n",
)
inbox_root = recovery_root / "inbox"
inbox_root.mkdir(mode=0o700)
monkeypatch.setattr(module, "WEBHOOK_INBOX_ROOT", inbox_root)
original_path = inbox_root / f"{semantic}.json"
original_path.write_bytes(original_raw)
original_path.chmod(0o600)
recovery_delivery = {
"status": "deploying",
"recovery_id": RECOVERY_ID,
"validation": {"last_result": "pass"},
"audit": [
{"stage": "validation_passed"},
{"stage": "deployment_started"},
],
}
module._self_upgrade_write_private_record(
recovery_root / f"{semantic}.delivery.json",
module._self_upgrade_canonical_json(recovery_delivery) + b"\n",
)
project = {"branch": "main", "gitea_repository_id": 9}
monkeypatch.setattr(module, "_load_projects", lambda: {module.SELF_UPGRADE_REPOSITORY: project})
monkeypatch.setattr(module, "_validation_policy", lambda *_args: {"repository": module.SELF_UPGRADE_REPOSITORY})
monkeypatch.setattr(module, "_self_upgrade_original_failed_delivery", lambda *_args: (
original_path, original_raw, original_record,
))
monkeypatch.setattr(module, "_self_upgrade_recovery_record_valid", lambda *_args, **_kwargs: True)
monkeypatch.setattr(module, "_self_upgrade_recovery_preimage_matches", lambda _record: True)
pointer = module._self_upgrade_store_compose_model(
RECOVERY_ID, module.SELF_UPGRADE_OLD_REVISION, _legacy_model(module),
)
assert isinstance(pointer, dict)
monkeypatch.setattr(module, "_self_upgrade_runtime", lambda **_kwargs: {
"runtimeCasSha256": "6" * 64,
"runtimeFingerprint": "5" * 64,
"sourceFingerprint": "4" * 64,
"containerId": CONTAINER_ID,
"imageId": module.SELF_UPGRADE_IMAGE_ID,
"revision": module.SELF_UPGRADE_OLD_REVISION,
"runtimeEnvironment": _legacy_environment(),
})
metadata = {
"id": REVISION[:12], "repository": module.SELF_UPGRADE_REPOSITORY,
"revision": REVISION, "tree": TREE,
"treeDigest": hashlib.sha256(f"git-tree:{TREE}".encode()).hexdigest(),
"builtAt": BUILT_AT,
}
snapshot = {
"revision": module.SELF_UPGRADE_OLD_REVISION,
"legacy_provenance": True,
"receipt": {},
"compose_containers": ["Unraid-AppOps-Gateway"],
"containers": [{
"name": "Unraid-AppOps-Gateway", "container_id": CONTAINER_ID,
"image_id": module.SELF_UPGRADE_IMAGE_ID,
"image_ref": module.SELF_UPGRADE_OLD_IMAGE_REF,
"rollback_ref": "gitea-deploy-rollback/unraid-appops-gateway-0:5553c04ce497",
"was_running": True,
}],
"captured_at_epoch": 1,
"appops_runtime_model": pointer,
}
now = 100
transaction_id = "22345678-1234-4234-8234-123456789abc"
request = {
"schema": module._SELF_UPGRADE_MODEL_TRANSACTION_SCHEMA,
"transactionId": transaction_id,
"recoveryId": RECOVERY_ID,
"repository": module.SELF_UPGRADE_REPOSITORY,
"kind": "recovery",
"revision": REVISION,
"metadata": metadata,
"expectedStateSha256": "a" * 64,
"registryFingerprint": "b" * 64,
"preMutationRuntimeSha256": "6" * 64,
"sourceFingerprint": "4" * 64,
"priorModelPointer": pointer,
"priorSnapshot": snapshot,
"candidateModelPointer": None,
"buildContexts": {},
"candidateImageId": None,
"verifiedLiveReference": None,
"verifiedReceipt": None,
"phase": "prepared",
"createdAt": now,
"updatedAt": now,
}
assert module._self_upgrade_recovery_journal(RECOVERY_ID) == journal
assert module._self_upgrade_recovery_request_payload(request_record) == request_payload
assert module._self_upgrade_original_failed_delivery(semantic, REVISION, {"repository": module.SELF_UPGRADE_REPOSITORY}) == (
original_path, original_raw, original_record,
)
assert module._self_upgrade_recovery_preimage_matches(recovery_delivery) is True
read_legacy = module._self_upgrade_read_legacy_cache_json
state_root = recovery_root / "state"
state_root.mkdir(mode=0o700)
state_path = state_root / "unraid-appops-gateway.json"
restored_state_raw: bytes | None = None
restored_state: dict[str, object] | None = None
def read_test_inbox(path, **kwargs):
if path == original_path:
assert path.read_bytes() == original_raw
return original_raw, original_record
if path == state_path and restored_state_raw is not None and restored_state is not None:
assert path.read_bytes() == restored_state_raw
return restored_state_raw, restored_state
return read_legacy(path, **kwargs)
# The production reader pins the real legacy cache directory profile. For
# this temp-root test, stub only that fixed-path adapter while preserving
# the actual HMAC request, journal, authorization, and transaction checks.
monkeypatch.setattr(module, "_self_upgrade_read_legacy_cache_json", read_test_inbox)
actual_authorization = module._self_upgrade_recovery_authorization(RECOVERY_ID)
assert actual_authorization is not None
preimage = module._self_upgrade_transaction_live_preimage(RECOVERY_ID, "recovery")
assert preimage is not None
assert preimage["currentRevision"] == module.SELF_UPGRADE_OLD_REVISION
assert preimage["runtimeEnvironment"] == _legacy_environment()
assert module._self_upgrade_load_compose_model(
RECOVERY_ID, module.SELF_UPGRADE_OLD_REVISION,
)["model"]["services"]["appops-gateway"]["environment"] == journal["runtimeEnvironment"]
created = module._self_upgrade_begin_model_transaction(request)
assert isinstance(created, dict)
assert created["payload"]["priorSnapshot"]["appops_runtime_model"] == pointer
assert stat.S_IMODE((recovery_root / "transactions" / f"{transaction_id}.json").stat().st_mode) == 0o600
assert module._self_upgrade_load_model_transaction(transaction_id) == created
assert module._self_upgrade_update_model_transaction(
transaction_id, "0" * 64, "recovery_required",
) is None
held = module._self_upgrade_update_model_transaction(
transaction_id, created["sha256"], "recovery_required",
)
assert isinstance(held, dict)
assert held["payload"]["phase"] == "recovery_required"
assert module._self_upgrade_pending_model_transaction() == held
rollback_binding = {
"containerId": CONTAINER_ID,
"imageId": module.SELF_UPGRADE_IMAGE_ID,
"runtimeFingerprint": "5" * 64,
"sourceFingerprint": "4" * 64,
"receiptSha256": hashlib.sha256(module._self_upgrade_canonical_json({})).hexdigest(),
}
restored_reference = module._self_upgrade_store_live_model(pointer, rollback_binding)
assert isinstance(restored_reference, dict)
restored_state = {
"repository": module.SELF_UPGRADE_REPOSITORY,
"status": "failed",
"live_revision": module.SELF_UPGRADE_OLD_REVISION,
"live_status": "healthy",
"live_legacy_provenance": True,
"live_receipt": {},
"live_artifacts": {
"appops_runtime_model": restored_reference,
"containers": {
"Unraid-AppOps-Gateway": {
"container_id": CONTAINER_ID,
"image_id": module.SELF_UPGRADE_IMAGE_ID,
},
},
},
}
restored_state_raw = module._self_upgrade_canonical_json(restored_state) + b"\n"
state_path.write_bytes(restored_state_raw)
state_path.chmod(0o600)
monkeypatch.setattr(module, "STATE_ROOT", state_root)
rollback_transaction_id = "32345678-1234-4234-8234-123456789abc"
rollback_payload = dict(created["payload"])
rollback_payload.update({
"transactionId": rollback_transaction_id,
"phase": "rolling_back",
"failureCode": "rollback_started",
"updatedAt": now + 1,
})
transaction_path = module._self_upgrade_transaction_path(rollback_transaction_id)
module._self_upgrade_write_private_record(
transaction_path,
module._self_upgrade_canonical_json(
module._self_upgrade_model_transaction_envelope(rollback_payload, secret),
) + b"\n",
)
rollback_before = module._self_upgrade_load_model_transaction(rollback_transaction_id)
assert isinstance(rollback_before, dict)
rolled_back = module._self_upgrade_update_model_transaction(
rollback_transaction_id,
rollback_before["sha256"],
"rolled_back",
{"restoredLiveReference": restored_reference},
)
assert isinstance(rolled_back, dict)
assert rolled_back["payload"]["phase"] == "rolled_back"
assert rolled_back["payload"]["restoredLiveReference"] == restored_reference
assert rolled_back["payload"]["restoredStateSha256"] == hashlib.sha256(restored_state_raw).hexdigest()
assert module._self_upgrade_pending_model_transaction() == held
@@ -1,6 +1,7 @@
from __future__ import annotations
import copy
import os
import subprocess
import pytest
@@ -110,6 +111,235 @@ def run(docker, git):
)
_MOUNT_ORDERS = {
("name", 1): (3, 0, 4, 1, 2),
("name", 2): (4, 2, 0, 3, 1),
("name", 3): (1, 3, 0, 4, 2),
("id", 1): (2, 4, 1, 0, 3),
("id", 2): (4, 1, 3, 2, 0),
}
def configure_reinspection(
docker,
*,
drift=None,
drift_reference="id",
drift_pair=1,
identifier=FAILED_ID,
):
"""Model Docker returning the same inspect with independent mount ordering."""
original_inspect = docker.inspect_container
counts = {"name": 0, "id": 0}
reads = []
def inspect(reference):
if reference == bootstrap.MODERN_HELPER_NAME:
scope = "name"
elif reference == identifier:
scope = "id"
else:
return original_inspect(reference)
counts[scope] += 1
reads.append((scope, counts[scope]))
if docker.existing_child is None:
return None
snapshot = copy.deepcopy(docker.existing_child)
order = _MOUNT_ORDERS.get((scope, counts[scope]))
if order is not None:
snapshot["Mounts"] = [snapshot["Mounts"][index] for index in order]
pair_number = (
1
if (scope == "name" and counts[scope] == 2)
or (scope == "id" and counts[scope] == 1)
else 2
if (scope == "name" and counts[scope] == 3)
or (scope == "id" and counts[scope] == 2)
else None
)
is_drift_read = pair_number == drift_pair and (
drift_reference == "both" or scope == drift_reference
)
if drift is not None and is_drift_read:
mount = snapshot["Mounts"][0]
if drift == "mount_type":
mount["Type"] = "volume"
elif drift == "mount_source":
mount["Source"] = "/tmp/changed-source"
elif drift == "mount_destination":
mount["Destination"] = "/tmp/changed-destination"
elif drift == "mount_rw":
mount["RW"] = not mount["RW"]
elif drift == "mount_propagation":
mount["Propagation"] = "shared"
elif drift == "mount_name":
mount["Name"] = "unexpected-volume"
elif drift == "missing_mount":
snapshot["Mounts"].pop()
elif drift == "extra_mount":
extra = copy.deepcopy(mount)
extra["Destination"] = "/unexpected"
snapshot["Mounts"].append(extra)
elif drift == "duplicate_destination":
snapshot["Mounts"].append(copy.deepcopy(mount))
elif drift == "metadata_field":
cache_mount = next(
item
for item in snapshot["Mounts"]
if item["Destination"] == "/mnt/cache"
)
cache_mount["Mode"] = "ro"
elif drift == "environment_order":
snapshot["Config"]["Env"] = list(reversed(snapshot["Config"]["Env"]))
elif drift == "binds_order":
snapshot["HostConfig"]["Binds"] = list(
reversed(snapshot["HostConfig"]["Binds"])
)
elif drift == "state_drift":
snapshot["State"]["ExitCode"] = 2
else:
raise AssertionError(f"unhandled test drift: {drift}")
return snapshot
docker.inspect_container = inspect
return counts, reads
def historical_inputs():
helper_source = (
original.ROOT / "backend/app/services/controller_artifact_helper_spec.py"
).read_text(encoding="utf-8")
child = original._retired_failed_child(helper_source)
docker = original.FakeDocker(
original._legacy_helper_spec(), existing_child=child
)
docker.containers["gitea-deploy-control"] = original._modern_controller_source_inspect(
bootstrap.MODERN_CONTROLLER_REVISION
)
git = original.FakeProtectedGit(helper_source)
return docker, git
def run_historical(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_failed_child_inspect_ignores_mount_order_across_name_and_id_reads():
docker, git, _child = inputs()
counts, reads = configure_reinspection(docker)
result = run(docker, git)
assert result["status"] == "promoted"
assert counts == {"name": 4, "id": 2}
assert reads[:5] == [
("name", 1),
("name", 2),
("id", 1),
("name", 3),
("id", 2),
]
assert docker.removed == [FAILED_ID]
assert len(docker.created) == len(docker.started) == 1
def test_historical_failed_child_mount_order_is_ignored_across_name_and_id_reads():
docker, git = historical_inputs()
counts, reads = configure_reinspection(
docker, identifier=original.RETIRED_HELPER_ID
)
result = run_historical(docker, git)
assert result["status"] == "promoted"
assert counts == {"name": 4, "id": 2}
assert reads[:5] == [
("name", 1),
("name", 2),
("id", 1),
("name", 3),
("id", 2),
]
assert docker.removed == [original.RETIRED_HELPER_ID]
assert len(docker.created) == len(docker.started) == 1
@pytest.mark.parametrize("drift_pair", [1, 2])
def test_historical_failed_child_rejects_mount_metadata_change_from_initial_snapshot(
drift_pair,
):
docker, git = historical_inputs()
configure_reinspection(
docker,
drift="metadata_field",
drift_reference="both",
drift_pair=drift_pair,
identifier=original.RETIRED_HELPER_ID,
)
with pytest.raises(bootstrap.BootstrapError):
run_historical(docker, git)
assert docker.removed == docker.created == docker.started == []
@pytest.mark.parametrize(
"drift",
[
"mount_type",
"mount_source",
"mount_destination",
"mount_rw",
"mount_propagation",
"mount_name",
"missing_mount",
"extra_mount",
"duplicate_destination",
"metadata_field",
"environment_order",
"binds_order",
"state_drift",
],
)
@pytest.mark.parametrize("drift_reference", ["name", "id"])
def test_failed_child_reinspection_rejects_any_non_mount_order_drift(
drift, drift_reference
):
docker, git, _child = inputs()
# Make the environment order check meaningful while keeping the initial
# image and child Config.Env identical.
docker.existing_child["Config"]["Env"] = ["PATH=/usr/bin", "LANG=C"]
docker.images[bootstrap.LEGACY_CONTROLLER_IMAGE_ID]["Config"]["Env"] = [
"PATH=/usr/bin",
"LANG=C",
]
_counts, _reads = configure_reinspection(
docker, drift=drift, drift_reference=drift_reference
)
if drift == "binds_order":
assert len(docker.existing_child["HostConfig"]["Binds"]) > 1
with pytest.raises(bootstrap.BootstrapError):
run(docker, git)
assert docker.removed == docker.created == docker.started == []
def test_noncurrent_protected_ancestor_failure_releases_only_its_verified_name():
docker, git, child = inputs()
result = run(docker, git)
@@ -274,13 +504,20 @@ def test_delete_adapter_rejects_noncanonical_full_container_identity(
def test_git_ancestor_proof_reads_original_protected_objects(
tmp_path, monkeypatch, scenario
):
def git(*args):
def git(*args, allow_replace_objects=False):
environment = None
if allow_replace_objects:
# Only the attack demonstration opts out of the inherited guard.
# The production proof still reads original protected Git objects.
environment = dict(os.environ)
environment.pop("GIT_NO_REPLACE_OBJECTS", None)
return (
subprocess.run(
["git", "-C", str(tmp_path), *args],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=environment,
)
.stdout.decode()
.strip()
@@ -302,7 +539,7 @@ def test_git_ancestor_proof_reads_original_protected_objects(
proof = bootstrap.ProtectedGitRepository()
if scenario == "replace_ref":
git("replace", old, current)
assert git("rev-parse", f"{old}^{{tree}}") != tree
assert git("rev-parse", f"{old}^{{tree}}", allow_replace_objects=True) != tree
if scenario == "unrelated":
git("checkout", "-qb", "side", old)
path.write_text("unrelated child")
@@ -0,0 +1,633 @@
from __future__ import annotations
import io
import json
from datetime import datetime, timezone
import pytest
from sqlmodel import Session, SQLModel, create_engine, select
from backend.app.db.models import AuditEvent, Secret
from backend.app.services import workmesh_secret_import as importer
from backend.app.services.secret_store import get_secret, set_secret
REVISION = "a" * 40
CREDENTIAL = "W" * 48
PREFLIGHT_SHA = "b" * 64
BACKUP_SHA = "c" * 64
SECRET_REF = "WORKMESH_DEPLOYMENT_BROKER"
TARGET = "jens/workmesh"
CHECKS = {
"exactMain": "passed",
"pushValidation": "passed",
"controllerPreflight": "passed",
"dataBackup": "passed",
}
def _valid_preflight(revision: str = REVISION) -> dict[str, object]:
return {
"apiVersion": "itworx.workmesh-promotion/v2",
"status": "preflight_ok",
"repository": TARGET,
"revision": revision,
"preflightSha256": PREFLIGHT_SHA,
"backupInputsSha256": BACKUP_SHA,
"checks": dict(CHECKS),
"diagnostic": CREDENTIAL,
}
class FakeBroker:
def __init__(
self,
*,
response: object | None = None,
error: Exception | None = None,
base_url: str = "https://controller.example",
verify_tls: bool = True,
config_error: str | None = None,
before_response=None,
):
self.response = _valid_preflight() if response is None else response
self.error = error
self.base_url = base_url
self.verify_tls = verify_tls
self._config_error = config_error
self.before_response = before_response
self.preflight_calls: list[tuple[str, str]] = []
self.apply_calls = 0
def configuration_error(self):
return self._config_error
def workmesh_promotion_preflight(self, revision: str, *, secret: str):
self.preflight_calls.append((revision, secret))
if self.before_response is not None:
self.before_response()
if self.error is not None:
raise self.error
return self.response
def workmesh_promotion_apply(self, *args, **kwargs):
self.apply_calls += 1
raise AssertionError("the credential importer must not invoke promotion apply")
@pytest.fixture
def database(tmp_path, monkeypatch):
engine = create_engine(f"sqlite:///{tmp_path / 'workmesh-secret-import.db'}")
SQLModel.metadata.create_all(engine)
monkeypatch.setattr(
importer, "secret_storage_status", lambda: {"productionReady": True}
)
with Session(engine) as session:
yield engine, session
engine.dispose()
def _rows(session: Session) -> tuple[Secret | None, list[AuditEvent]]:
return (
session.get(Secret, SECRET_REF),
list(session.exec(select(AuditEvent)).all()),
)
def _database_dump(session: Session) -> str:
secret, audits = _rows(session)
return repr(
{
"secret": secret.model_dump() if secret is not None else None,
"audits": [event.model_dump() for event in audits],
}
)
def _import(
session: Session,
*,
credential: str = CREDENTIAL,
revision: str = REVISION,
broker: FakeBroker | None = None,
):
return importer.import_workmesh_deployment_secret(
session,
credential,
revision=revision,
broker=broker or FakeBroker(),
)
def test_import_creates_encrypted_secret_after_verified_fixed_preflight_and_audits(
database,
):
_, session = database
broker = FakeBroker()
result = _import(session, broker=broker)
row = session.get(Secret, SECRET_REF)
assert result["status"] == "imported"
assert result["secretRef"] == SECRET_REF
assert result["repository"] == TARGET
assert result["revision"] == REVISION
assert result["preflightSha256"] == PREFLIGHT_SHA
assert result["readback"] == "verified"
assert row is not None
assert row.encrypted_value != CREDENTIAL
assert get_secret(session, SECRET_REF, touch=False) == CREDENTIAL
assert broker.preflight_calls == [(REVISION, CREDENTIAL)]
assert broker.apply_calls == 0
audits = session.exec(select(AuditEvent)).all()
assert len(audits) == 1
audit = audits[0]
assert audit.target == TARGET
assert audit.result == "success"
assert audit.redacted_metadata["storeReference"] == SECRET_REF
assert audit.redacted_metadata["revision"] == REVISION
assert audit.redacted_metadata["preflightSha256"] == PREFLIGHT_SHA
assert CREDENTIAL not in _database_dump(session)
def test_identical_import_verifies_and_audits_without_mutating_secret(database):
_, session = database
assert _import(session)["status"] == "imported"
row = session.get(Secret, SECRET_REF)
assert row is not None
before = {
"encrypted_value": row.encrypted_value,
"created_at": row.created_at,
"updated_at": row.updated_at,
"last_used_at": row.last_used_at,
"used_by_json": list(row.used_by_json),
"metadata_json": dict(row.metadata_json),
}
result = _import(session)
session.refresh(row)
assert result["status"] == "already_present"
assert {
"encrypted_value": row.encrypted_value,
"created_at": row.created_at,
"updated_at": row.updated_at,
"last_used_at": row.last_used_at,
"used_by_json": list(row.used_by_json),
"metadata_json": dict(row.metadata_json),
} == before
assert len(session.exec(select(AuditEvent)).all()) == 2
@pytest.mark.parametrize("credential", ["W" * 32, "W" * 256])
def test_credential_length_contract_accepts_exact_boundaries(database, credential):
_, session = database
result = _import(session, credential=credential)
assert result["status"] == "imported"
assert get_secret(session, SECRET_REF, touch=False) == credential
def test_different_existing_value_is_rejected_before_controller_preflight(database):
_, session = database
set_secret(session, SECRET_REF, "D" * 48, metadata={"owner": "keep"})
original = session.get(Secret, SECRET_REF)
assert original is not None
before = original.model_dump()
broker = FakeBroker()
with pytest.raises(importer.WorkMeshSecretImportError):
_import(session, broker=broker)
session.rollback()
current = session.get(Secret, SECRET_REF)
assert current is not None and current.model_dump() == before
assert broker.preflight_calls == []
assert session.exec(select(AuditEvent)).all() == []
def test_undecryptable_existing_value_is_rejected_without_replacement(database):
_, session = database
set_secret(session, SECRET_REF, "D" * 48)
row = session.get(Secret, SECRET_REF)
assert row is not None
row.encrypted_value = "unreadable-ciphertext"
session.add(row)
session.commit()
broker = FakeBroker()
with pytest.raises(importer.WorkMeshSecretImportError):
_import(session, broker=broker)
assert session.get(Secret, SECRET_REF).encrypted_value == "unreadable-ciphertext"
assert broker.preflight_calls == []
def test_concurrent_identical_insert_winner_is_retained_and_audited(database):
engine, session = database
winner_timestamp = datetime(2026, 10, 4, 12, 0, 0, tzinfo=timezone.utc)
def concurrent_winner():
with Session(engine) as winner_session:
set_secret(
winner_session,
SECRET_REF,
CREDENTIAL,
used_by=["concurrent-owner"],
metadata={"owner": "concurrent-winner"},
)
winner = winner_session.get(Secret, SECRET_REF)
assert winner is not None
winner.created_at = winner_timestamp
winner.updated_at = winner_timestamp
winner_session.add(winner)
winner_session.commit()
broker = FakeBroker(before_response=concurrent_winner)
result = _import(session, broker=broker)
winner = session.get(Secret, SECRET_REF)
assert result["status"] == "already_present"
assert winner is not None
assert winner.created_at.replace(tzinfo=timezone.utc) == winner_timestamp
assert winner.updated_at.replace(tzinfo=timezone.utc) == winner_timestamp
assert winner.used_by_json == ["concurrent-owner"]
assert winner.metadata_json == {"owner": "concurrent-winner"}
assert get_secret(session, SECRET_REF, touch=False) == CREDENTIAL
assert len(session.exec(select(AuditEvent)).all()) == 1
def test_concurrent_different_insert_winner_is_never_overwritten(database):
engine, session = database
def concurrent_winner():
with Session(engine) as winner_session:
set_secret(
winner_session,
SECRET_REF,
"D" * 48,
metadata={"owner": "concurrent-winner"},
)
broker = FakeBroker(before_response=concurrent_winner)
with pytest.raises(importer.WorkMeshSecretImportError):
_import(session, broker=broker)
session.rollback()
assert get_secret(session, SECRET_REF, touch=False) == "D" * 48
assert session.get(Secret, SECRET_REF).metadata_json == {
"owner": "concurrent-winner"
}
assert session.exec(select(AuditEvent)).all() == []
def test_production_secret_storage_must_be_ready(database, monkeypatch):
_, session = database
monkeypatch.setattr(
importer, "secret_storage_status", lambda: {"productionReady": False}
)
broker = FakeBroker()
with pytest.raises(importer.WorkMeshSecretImportError):
_import(session, broker=broker)
assert broker.preflight_calls == []
assert session.get(Secret, SECRET_REF) is None
@pytest.mark.parametrize(
"credential",
[
"W" * 31,
"W" * 257,
"W" * 31 + " ",
"W" * 31 + "\n",
"W" * 31 + "\u00e9",
"W" * 31 + "\x00",
],
)
def test_credential_must_be_bounded_printable_ascii_without_whitespace(
database, credential
):
_, session = database
broker = FakeBroker()
with pytest.raises(importer.WorkMeshSecretImportError):
_import(session, credential=credential, broker=broker)
assert broker.preflight_calls == []
assert session.get(Secret, SECRET_REF) is None
@pytest.mark.parametrize("revision", ["main", "A" * 40, "a" * 39, "a" * 41])
def test_revision_must_be_an_exact_full_lowercase_commit(database, revision):
_, session = database
broker = FakeBroker()
with pytest.raises(importer.WorkMeshSecretImportError):
_import(session, revision=revision, broker=broker)
assert broker.preflight_calls == []
assert session.get(Secret, SECRET_REF) is None
@pytest.mark.parametrize(
("base_url", "verify_tls", "config_error"),
[
("http://controller.example", True, None),
("https://controller.example", False, None),
("https://controller.example", True, "deployment_broker_url_invalid"),
],
)
def test_preflight_requires_verified_https_configuration(
database, base_url, verify_tls, config_error
):
_, session = database
broker = FakeBroker(
base_url=base_url,
verify_tls=verify_tls,
config_error=config_error,
)
with pytest.raises(importer.WorkMeshSecretImportError):
_import(session, broker=broker)
assert broker.preflight_calls == []
assert session.get(Secret, SECRET_REF) is None
@pytest.mark.parametrize(
"mutate",
[
lambda value: value.update(status="preflight_failed"),
lambda value: value.update(apiVersion="wrong/v1"),
lambda value: value.update(repository="jens/other"),
lambda value: value.update(revision="f" * 40),
lambda value: value.update(preflightSha256="invalid"),
lambda value: value.update(backupInputsSha256="invalid"),
lambda value: value.update(checks={"exactMain": "passed"}),
lambda value: value["checks"].update(pushValidation="blocked"),
],
)
def test_invalid_fixed_preflight_never_writes_secret_or_audit(database, mutate):
_, session = database
preflight = _valid_preflight()
mutate(preflight)
broker = FakeBroker(response=preflight)
with pytest.raises(importer.WorkMeshSecretImportError):
_import(session, broker=broker)
session.rollback()
assert session.get(Secret, SECRET_REF) is None
assert session.exec(select(AuditEvent)).all() == []
assert broker.apply_calls == 0
def test_non_mapping_preflight_is_rejected_without_storage_write(database):
_, session = database
broker = FakeBroker(response=["not", "a", "mapping"])
with pytest.raises(importer.WorkMeshSecretImportError):
_import(session, broker=broker)
session.rollback()
assert session.get(Secret, SECRET_REF) is None
def test_broker_exception_is_sanitized_and_never_persisted(database):
_, session = database
broker = FakeBroker(error=RuntimeError(f"upstream leaked {CREDENTIAL}"))
with pytest.raises(importer.WorkMeshSecretImportError) as error:
_import(session, broker=broker)
assert CREDENTIAL not in str(error.value)
assert broker.preflight_calls == [(REVISION, CREDENTIAL)]
session.rollback()
assert session.get(Secret, SECRET_REF) is None
assert session.exec(select(AuditEvent)).all() == []
def test_audit_insert_failure_rolls_back_secret_and_audit_together(database):
engine, session = database
def fail_audit_insert(conn, cursor, statement, parameters, context, executemany):
if statement.lstrip().lower().startswith("insert into auditevent"):
raise RuntimeError(f"database detail contained {CREDENTIAL}")
from sqlalchemy import event
event.listen(engine, "before_cursor_execute", fail_audit_insert)
try:
with pytest.raises(importer.WorkMeshSecretImportError) as error:
_import(session)
finally:
event.remove(engine, "before_cursor_execute", fail_audit_insert)
assert CREDENTIAL not in str(error.value)
session.rollback()
with Session(engine) as verify:
assert verify.get(Secret, SECRET_REF) is None
assert verify.exec(select(AuditEvent)).all() == []
def test_uncertain_readback_returns_blocked_without_replacing_committed_secret(
database, monkeypatch
):
engine, session = database
actual_get_secret = importer.get_secret
calls = 0
def fail_readback_once(db_session, name, *, touch=False):
nonlocal calls
calls += 1
if calls == 2:
return None
return actual_get_secret(db_session, name, touch=touch)
monkeypatch.setattr(importer, "get_secret", fail_readback_once)
broker = FakeBroker()
with pytest.raises(importer.WorkMeshSecretImportError) as error:
_import(session, broker=broker)
assert CREDENTIAL not in str(error.value)
monkeypatch.setattr(importer, "get_secret", actual_get_secret)
session.rollback()
with Session(engine) as verify:
row = verify.get(Secret, SECRET_REF)
assert row is not None
before = row.model_dump()
assert get_secret(verify, SECRET_REF, touch=False) == CREDENTIAL
assert len(verify.exec(select(AuditEvent)).all()) == 1
outcome = _import(session, broker=FakeBroker())
assert outcome["status"] == "already_present"
session.expire_all()
after = session.get(Secret, SECRET_REF)
assert after is not None and after.model_dump() == before
def test_uncertain_commit_returns_blocked_and_same_value_replay_does_not_replace(
database, monkeypatch
):
engine, session = database
actual_commit = session.commit
def commit_then_lose_acknowledgement():
if any(isinstance(item, AuditEvent) for item in session.new):
actual_commit()
raise RuntimeError(f"commit acknowledgement lost for {CREDENTIAL}")
actual_commit()
monkeypatch.setattr(session, "commit", commit_then_lose_acknowledgement)
with pytest.raises(importer.WorkMeshSecretImportError) as error:
_import(session)
assert CREDENTIAL not in str(error.value)
monkeypatch.setattr(session, "commit", actual_commit)
with Session(engine) as verify:
row = verify.get(Secret, SECRET_REF)
assert row is not None
before = row.model_dump()
assert get_secret(verify, SECRET_REF, touch=False) == CREDENTIAL
assert len(verify.exec(select(AuditEvent)).all()) == 1
result = _import(session, broker=FakeBroker())
assert result["status"] == "already_present"
session.expire_all()
after = session.get(Secret, SECRET_REF)
assert after is not None and after.model_dump() == before
assert len(session.exec(select(AuditEvent)).all()) == 2
def test_cli_imports_stdin_only_and_emits_sanitized_verification(database, monkeypatch):
engine, _ = database
broker = FakeBroker()
monkeypatch.setattr(importer, "engine", engine)
monkeypatch.setattr(importer, "DeploymentBrokerClient", lambda: broker)
output = io.StringIO()
exit_code = importer.main(
[REVISION],
stdin=io.BytesIO(CREDENTIAL.encode("ascii")),
stdout=output,
)
result = json.loads(output.getvalue())
assert exit_code == 0
assert result["status"] == "imported"
assert result["secretRef"] == SECRET_REF
assert result["revision"] == REVISION
assert CREDENTIAL not in output.getvalue()
assert broker.preflight_calls == [(REVISION, CREDENTIAL)]
def test_cli_rejects_extra_arguments_before_reading_secret(database, monkeypatch):
engine, _ = database
broker = FakeBroker()
monkeypatch.setattr(importer, "engine", engine)
monkeypatch.setattr(importer, "DeploymentBrokerClient", lambda: broker)
output = io.StringIO()
class UnreadableInput:
def read(self, _size):
pytest.fail("invalid command line must be rejected before reading stdin")
exit_code = importer.main(
[REVISION, CREDENTIAL],
stdin=UnreadableInput(),
stdout=output,
)
assert exit_code != 0
assert json.loads(output.getvalue()) == {
"reason": "invalid_arguments",
"status": "blocked",
}
assert broker.preflight_calls == []
def test_cli_rejects_non_ascii_stdin_without_calling_controller(database, monkeypatch):
engine, _ = database
broker = FakeBroker()
monkeypatch.setattr(importer, "engine", engine)
monkeypatch.setattr(importer, "DeploymentBrokerClient", lambda: broker)
output = io.StringIO()
exit_code = importer.main(
[REVISION],
stdin=io.BytesIO(b"W" * 47 + b"\xff"),
stdout=output,
)
assert exit_code != 0
assert json.loads(output.getvalue()) == {
"reason": "invalid_input",
"status": "blocked",
}
assert broker.preflight_calls == []
def test_cli_rejects_oversized_stdin_without_calling_controller(database, monkeypatch):
engine, _ = database
broker = FakeBroker()
monkeypatch.setattr(importer, "engine", engine)
monkeypatch.setattr(importer, "DeploymentBrokerClient", lambda: broker)
output = io.StringIO()
exit_code = importer.main(
[REVISION],
stdin=io.BytesIO(b"W" * 4097),
stdout=output,
)
assert exit_code != 0
assert json.loads(output.getvalue())["status"] == "blocked"
assert "W" * 48 not in output.getvalue()
assert broker.preflight_calls == []
def test_cli_unexpected_error_never_echoes_secret(database, monkeypatch):
engine, _ = database
broker = FakeBroker(error=RuntimeError(f"unexpected {CREDENTIAL}"))
monkeypatch.setattr(importer, "engine", engine)
monkeypatch.setattr(importer, "DeploymentBrokerClient", lambda: broker)
output = io.StringIO()
exit_code = importer.main(
[REVISION],
stdin=io.BytesIO(CREDENTIAL.encode("ascii")),
stdout=output,
)
result = json.loads(output.getvalue())
assert exit_code != 0
assert result["status"] == "blocked"
assert CREDENTIAL not in output.getvalue()
assert "unexpected" not in output.getvalue()
assert "traceback" not in output.getvalue().lower()
@pytest.mark.parametrize("tampered", [False, True])
def test_cli_unknown_typed_error_code_is_normalized(database, monkeypatch, tampered):
engine, _ = database
monkeypatch.setattr(importer, "engine", engine)
monkeypatch.setattr(importer, "DeploymentBrokerClient", FakeBroker)
def fail_import(*args, **kwargs):
error = importer.WorkMeshSecretImportError(CREDENTIAL)
if tampered:
error.code = CREDENTIAL
raise error
monkeypatch.setattr(importer, "import_workmesh_deployment_secret", fail_import)
output = io.StringIO()
assert importer.main(
[REVISION], stdin=io.BytesIO(CREDENTIAL.encode("ascii")), stdout=output
) == 2
assert json.loads(output.getvalue()) == {
"status": "blocked", "reason": "import_failed"
}
assert CREDENTIAL not in output.getvalue()