Publish curated Unraid-AppOps-Gateway source from 21729207a161d87393cfb73e79582bc373aa19cf
This commit is contained in:
1 parent
ab3fa553f0
commit
76d7249b5e
6 files changed
+1497
-37
No files matched your search
@@ -2445,6 +2445,30 @@ def _workmesh_product_project(tmp_path: Path) -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
def _agenttestlab_project(tmp_path: Path, containers: list[str] | None = None) -> dict[str, object]:
|
||||
return {
|
||||
"slug": "agenttestlab",
|
||||
"branch": "main",
|
||||
"source_dir": "/mnt/cache/appdata/gitea-deploy-runners/sources/agenttestlab",
|
||||
"compose_project": "agenttestlab",
|
||||
"compose_dir": "",
|
||||
"compose_files": ["compose.inspect.yaml"],
|
||||
"containers": list(containers or [
|
||||
"agenttestlab-postgres-1",
|
||||
"agenttestlab-redis-1",
|
||||
"agenttestlab-api-1",
|
||||
"agenttestlab-worker-1",
|
||||
"agenttestlab-agenttestlab-1",
|
||||
]),
|
||||
"exact_source": True,
|
||||
"env_source": "/srv/example/appdata/agenttestlab/deploy.env",
|
||||
"health_timeout": 300,
|
||||
"verify_oci_revision": False,
|
||||
"_repository_name": "jens/agenttestlab",
|
||||
"_repository_path": str(tmp_path),
|
||||
}
|
||||
|
||||
|
||||
def _workmesh_legacy_reattest_setup(tmp_path: Path):
|
||||
revision = "a" * 40
|
||||
project = _workmesh_product_project(tmp_path)
|
||||
@@ -3100,6 +3124,17 @@ def test_workmesh_topology_transition_requires_exact_registry_and_recorded_recei
|
||||
assert deploy_module.workmesh_rollback_topology(wrong, previous) is wrong
|
||||
|
||||
|
||||
def test_snapshot_topology_preserves_workmesh_legacy_container_name_contract(
|
||||
deploy_module, tmp_path
|
||||
):
|
||||
project = _workmesh_product_project(tmp_path)
|
||||
snapshot = {"containers": [{"name": name} for name in deploy_module.WORKMESH_LEGACY_CONTAINERS]}
|
||||
|
||||
restored = deploy_module.snapshot_rollback_topology(project, snapshot)
|
||||
|
||||
assert restored["containers"] == deploy_module.WORKMESH_LEGACY_CONTAINERS
|
||||
|
||||
|
||||
def test_workmesh_product_deploy_removes_only_reviewed_compose_orphan(
|
||||
deploy_module, tmp_path, monkeypatch
|
||||
):
|
||||
@@ -3272,6 +3307,482 @@ def test_workmesh_product_snapshot_retains_authorized_five_container_rollback(
|
||||
assert deploy_module.durable_rollback_snapshot(project, "jens/workmesh", new_revision) == snapshot
|
||||
|
||||
|
||||
def test_agenttestlab_deploy_accepts_only_the_reviewed_five_or_six_container_topology(
|
||||
deploy_module, tmp_path, monkeypatch
|
||||
):
|
||||
project = _agenttestlab_project(tmp_path, deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS)
|
||||
commands = []
|
||||
monkeypatch.setattr(deploy_module, "deployment_environment", lambda *_: {})
|
||||
monkeypatch.setattr(deploy_module, "compose_command", lambda *_: (tmp_path, ["docker", "compose"]))
|
||||
monkeypatch.setattr(deploy_module, "prepare_compose_migration", lambda *_: [])
|
||||
monkeypatch.setattr(deploy_module, "run", lambda command, **_: commands.append(command))
|
||||
observed = [
|
||||
set(deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS),
|
||||
set(deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS),
|
||||
set(deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS) | {"agenttestlab-unreviewed-1"},
|
||||
]
|
||||
monkeypatch.setattr(
|
||||
deploy_module, "compose_project_container_names", lambda *_: observed.pop(0)
|
||||
)
|
||||
|
||||
deploy_module.deploy_project(project, "a" * 40, "jens/agenttestlab")
|
||||
deploy_module.deploy_project(project, "b" * 40, "jens/agenttestlab")
|
||||
with pytest.raises(deploy_module.DeployError, match="topology changed"):
|
||||
deploy_module.deploy_project(project, "c" * 40, "jens/agenttestlab")
|
||||
|
||||
assert commands == [
|
||||
["docker", "compose", "config", "--quiet"],
|
||||
["docker", "compose", "build"],
|
||||
["docker", "compose", "up", "--detach", "--no-build"],
|
||||
["docker", "compose", "config", "--quiet"],
|
||||
["docker", "compose", "build"],
|
||||
["docker", "compose", "up", "--detach", "--no-build"],
|
||||
["docker", "compose", "config", "--quiet"],
|
||||
["docker", "compose", "build"],
|
||||
]
|
||||
|
||||
|
||||
def test_agenttestlab_transition_preserves_registry_and_accepts_verified_post_deploy_retry(
|
||||
deploy_module, tmp_path, monkeypatch
|
||||
):
|
||||
entry = _agenttestlab_project(tmp_path)
|
||||
entry.pop("_repository_name")
|
||||
entry.pop("_repository_path")
|
||||
original = {"jens/agenttestlab": entry, "jens/other": {"slug": "other", "policy": ["unchanged"]}}
|
||||
original_bytes = json.dumps(original).encode()
|
||||
registry = tmp_path / "projects.json"
|
||||
registry.write_bytes(original_bytes)
|
||||
registry.chmod(0o600)
|
||||
revision = "b" * 40
|
||||
protected_head = revision
|
||||
live_revision = "a" * 40
|
||||
tree = "c" * 40
|
||||
state = {
|
||||
"schema_version": 3,
|
||||
"repository": "jens/agenttestlab",
|
||||
"live_revision": live_revision,
|
||||
"live_status": "healthy",
|
||||
"live_receipt": {
|
||||
"build": {"tree": tree},
|
||||
"containers": {name: {"image_id": "sha256:" + "1" * 64}
|
||||
for name in deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS},
|
||||
},
|
||||
"live_artifacts": {
|
||||
"containers": {name: {"image_id": "sha256:" + "1" * 64}
|
||||
for name in deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS},
|
||||
},
|
||||
}
|
||||
project = {**entry, "_repository_name": "jens/agenttestlab", "_repository_path": str(tmp_path)}
|
||||
monkeypatch.setattr(deploy_module, "CONFIG_PATH", registry)
|
||||
monkeypatch.setattr(deploy_module, "STATE_ROOT", tmp_path)
|
||||
monkeypatch.setattr(deploy_module, "_root_owned", lambda _path: True)
|
||||
monkeypatch.setattr(deploy_module, "_fsync_directory", lambda _path: None)
|
||||
monkeypatch.setattr(deploy_module, "load_projects", lambda: json.loads(registry.read_text()))
|
||||
monkeypatch.setattr(deploy_module, "validate_commit", lambda _repo, _branch, value: value)
|
||||
|
||||
def git_or_compose_output(command, **_kwargs):
|
||||
if command[-2:] == ["rev-parse", "refs/heads/main"]:
|
||||
return protected_head
|
||||
return "\n".join(deploy_module.AGENTTESTLAB_SERVICES)
|
||||
|
||||
monkeypatch.setattr(deploy_module, "output", git_or_compose_output)
|
||||
monkeypatch.setattr(deploy_module, "read_state", lambda *_: state)
|
||||
monkeypatch.setattr(deploy_module, "state_live_artifacts", lambda _state: state["live_artifacts"])
|
||||
monkeypatch.setattr(
|
||||
deploy_module, "compose_project_container_names",
|
||||
lambda *_: set(deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS),
|
||||
)
|
||||
monkeypatch.setattr(deploy_module, "exact_source_matches_revision", lambda *_: True)
|
||||
monkeypatch.setattr(deploy_module, "compose_command", lambda *_: (tmp_path, ["docker", "compose"]))
|
||||
monkeypatch.setattr(deploy_module, "deployment_environment", lambda *_: {})
|
||||
verified = []
|
||||
monkeypatch.setattr(
|
||||
deploy_module,
|
||||
"verified_current_receipt",
|
||||
lambda _repo, target, _revision, _tree: verified.append(list(target["containers"])) or {"verified": True},
|
||||
)
|
||||
|
||||
result = deploy_module.transition_agenttestlab_registry(
|
||||
original, project, tmp_path, revision
|
||||
)
|
||||
|
||||
expected = copy.deepcopy(original)
|
||||
expected["jens/agenttestlab"]["containers"] = deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS.copy()
|
||||
assert result == "product-topology-registered"
|
||||
assert json.loads(registry.read_bytes()) == expected
|
||||
assert expected["jens/agenttestlab"]["env_source"] == entry["env_source"]
|
||||
assert expected["jens/other"] == original["jens/other"]
|
||||
backup = deploy_module._agenttestlab_registry_backup_path()
|
||||
assert backup.read_bytes() == original_bytes
|
||||
monkeypatch.setattr(
|
||||
deploy_module,
|
||||
"_root_only_backup",
|
||||
lambda path, payload: None
|
||||
if path.read_bytes() == payload
|
||||
else pytest.fail("idempotent transition changed the saved registry backup"),
|
||||
)
|
||||
|
||||
product_project = {**expected["jens/agenttestlab"], "_repository_name": "jens/agenttestlab"}
|
||||
result = deploy_module.transition_agenttestlab_registry(
|
||||
expected, product_project, tmp_path, revision
|
||||
)
|
||||
assert result == "already-registered"
|
||||
|
||||
restored_artifacts = []
|
||||
monkeypatch.setattr(
|
||||
deploy_module,
|
||||
"deployment_artifacts",
|
||||
lambda target: restored_artifacts.append(list(target["containers"])) or {
|
||||
"containers": {name: {"image_id": "sha256:" + "1" * 64}
|
||||
for name in target["containers"]}
|
||||
},
|
||||
)
|
||||
failed_state = deploy_module.resolve_failed_live_state(
|
||||
state,
|
||||
{"revision": live_revision, "receipt": state["live_receipt"]},
|
||||
product_project,
|
||||
production_started=True,
|
||||
failure_stage="deployment",
|
||||
restore_error=None,
|
||||
)
|
||||
assert restored_artifacts == [deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS]
|
||||
assert set(failed_state["live_artifacts"]["containers"]) == set(deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS)
|
||||
assert json.loads(registry.read_bytes()) == expected
|
||||
|
||||
later_revision = "d" * 40
|
||||
protected_head = later_revision
|
||||
state["live_revision"] = later_revision
|
||||
state["live_receipt"]["containers"] = {
|
||||
name: {"image_id": "sha256:" + "2" * 64}
|
||||
for name in deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS
|
||||
}
|
||||
state["live_artifacts"]["containers"] = copy.deepcopy(state["live_receipt"]["containers"])
|
||||
monkeypatch.setattr(
|
||||
deploy_module, "compose_project_container_names",
|
||||
lambda *_: set(deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS),
|
||||
)
|
||||
result = deploy_module.transition_agenttestlab_registry(
|
||||
expected, product_project, tmp_path, later_revision
|
||||
)
|
||||
|
||||
assert result == "already-registered"
|
||||
assert registry.read_bytes() != original_bytes
|
||||
assert json.loads(registry.read_bytes()) == expected
|
||||
assert verified == [
|
||||
deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS,
|
||||
deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS,
|
||||
deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS,
|
||||
]
|
||||
|
||||
saved = deploy_module.agenttestlab_registry_rollback_backup(expected)
|
||||
deploy_module.restore_agenttestlab_registry(expected, saved)
|
||||
assert saved == original_bytes
|
||||
assert registry.read_bytes() != original_bytes
|
||||
assert json.loads(registry.read_bytes()) == original
|
||||
|
||||
legacy_project = {
|
||||
**original["jens/agenttestlab"],
|
||||
"_repository_name": "jens/agenttestlab",
|
||||
"_repository_path": str(tmp_path),
|
||||
}
|
||||
state["live_revision"] = live_revision
|
||||
state["live_receipt"]["containers"] = {
|
||||
name: {"image_id": "sha256:" + "1" * 64}
|
||||
for name in deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS
|
||||
}
|
||||
state["live_artifacts"]["containers"] = copy.deepcopy(state["live_receipt"]["containers"])
|
||||
monkeypatch.setattr(
|
||||
deploy_module, "compose_project_container_names",
|
||||
lambda *_: set(deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS),
|
||||
)
|
||||
result = deploy_module.transition_agenttestlab_registry(
|
||||
original, legacy_project, tmp_path, later_revision
|
||||
)
|
||||
assert result == "product-topology-registered"
|
||||
assert json.loads(registry.read_bytes()) == expected
|
||||
assert backup.read_bytes() == original_bytes
|
||||
|
||||
|
||||
def test_agenttestlab_transition_rejects_wrong_target_source_and_rendered_services(
|
||||
deploy_module, tmp_path, monkeypatch
|
||||
):
|
||||
entry = _agenttestlab_project(tmp_path)
|
||||
entry.pop("_repository_name")
|
||||
entry.pop("_repository_path")
|
||||
projects = {"jens/agenttestlab": entry}
|
||||
original_bytes = (json.dumps(projects, indent=2) + "\n").encode()
|
||||
registry = tmp_path / "projects.json"
|
||||
registry.write_bytes(original_bytes)
|
||||
project = {**entry, "_repository_name": "jens/agenttestlab"}
|
||||
revision = "b" * 40
|
||||
tree = "c" * 40
|
||||
state = {
|
||||
"live_revision": "a" * 40,
|
||||
"live_status": "healthy",
|
||||
"live_receipt": {
|
||||
"build": {"tree": tree},
|
||||
"containers": {name: {"image_id": "sha256:" + "1" * 64}
|
||||
for name in deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS},
|
||||
},
|
||||
"live_artifacts": {
|
||||
"containers": {name: {"image_id": "sha256:" + "1" * 64}
|
||||
for name in deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS},
|
||||
},
|
||||
}
|
||||
compose_services = [*deploy_module.AGENTTESTLAB_SERVICES]
|
||||
source_matches = True
|
||||
monkeypatch.setattr(deploy_module, "CONFIG_PATH", registry)
|
||||
monkeypatch.setattr(deploy_module, "STATE_ROOT", tmp_path)
|
||||
monkeypatch.setattr(deploy_module, "_fsync_directory", lambda _path: None)
|
||||
monkeypatch.setattr(deploy_module, "load_projects", lambda: json.loads(registry.read_text()))
|
||||
monkeypatch.setattr(deploy_module, "validate_commit", lambda _repo, _branch, value: value)
|
||||
|
||||
def output_stub(command, **_kwargs):
|
||||
if command[-2:] == ["rev-parse", "refs/heads/main"]:
|
||||
return revision
|
||||
return "\n".join(compose_services)
|
||||
|
||||
monkeypatch.setattr(deploy_module, "output", output_stub)
|
||||
monkeypatch.setattr(deploy_module, "read_state", lambda *_: state)
|
||||
monkeypatch.setattr(deploy_module, "state_live_artifacts", lambda _state: state["live_artifacts"])
|
||||
monkeypatch.setattr(
|
||||
deploy_module, "compose_project_container_names",
|
||||
lambda *_: set(deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
deploy_module, "exact_source_matches_revision",
|
||||
lambda *_: source_matches,
|
||||
)
|
||||
monkeypatch.setattr(deploy_module, "compose_command", lambda *_: (tmp_path, ["docker", "compose"]))
|
||||
monkeypatch.setattr(deploy_module, "deployment_environment", lambda *_: {})
|
||||
monkeypatch.setattr(deploy_module, "verified_current_receipt", lambda *_: {"verified": True})
|
||||
|
||||
with pytest.raises(deploy_module.DeployError, match="fixed main target"):
|
||||
deploy_module.transition_agenttestlab_registry(
|
||||
projects,
|
||||
{**project, "_repository_name": "other/agenttestlab"},
|
||||
tmp_path,
|
||||
revision,
|
||||
)
|
||||
with pytest.raises(deploy_module.DeployError, match="fixed main target"):
|
||||
deploy_module.transition_agenttestlab_registry(
|
||||
projects,
|
||||
{**project, "source_dir": str(tmp_path)},
|
||||
tmp_path,
|
||||
revision,
|
||||
)
|
||||
|
||||
source_matches = False
|
||||
with pytest.raises(deploy_module.DeployError, match="prepared source"):
|
||||
deploy_module.transition_agenttestlab_registry(projects, project, tmp_path, revision)
|
||||
source_matches = True
|
||||
compose_services.append("unreviewed")
|
||||
with pytest.raises(deploy_module.DeployError, match="six-service topology"):
|
||||
deploy_module.transition_agenttestlab_registry(projects, project, tmp_path, revision)
|
||||
compose_services.pop()
|
||||
state["live_artifacts"]["containers"][deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS[0]]["image_id"] = "sha256:" + "2" * 64
|
||||
with pytest.raises(deploy_module.DeployError, match="artifact identity"):
|
||||
deploy_module.transition_agenttestlab_registry(projects, project, tmp_path, revision)
|
||||
|
||||
assert registry.read_bytes() == original_bytes
|
||||
assert not deploy_module._agenttestlab_registry_backup_path().exists()
|
||||
|
||||
|
||||
def test_agenttestlab_automatic_failure_and_explicit_rollback_keep_distinct_registry_semantics(
|
||||
deploy_module, tmp_path, monkeypatch
|
||||
):
|
||||
legacy = _agenttestlab_project(tmp_path)
|
||||
product = {**legacy, "containers": deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS.copy()}
|
||||
previous_receipt = {"containers": {
|
||||
name: {"image_id": "sha256:" + "1" * 64}
|
||||
for name in deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS
|
||||
}}
|
||||
assert deploy_module.rollback_topology(product, {"live_receipt": previous_receipt})["containers"] == deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS
|
||||
assert deploy_module.rollback_topology(
|
||||
product,
|
||||
{"live_receipt": {"containers": {
|
||||
name: {"image_id": "sha256:" + "2" * 64}
|
||||
for name in deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS
|
||||
}}},
|
||||
) is product
|
||||
|
||||
observed = []
|
||||
monkeypatch.setattr(
|
||||
deploy_module,
|
||||
"deployment_artifacts",
|
||||
lambda target: observed.append(list(target["containers"])) or {
|
||||
"containers": {name: {"image_id": "sha256:" + "1" * 64}
|
||||
for name in target["containers"]}
|
||||
},
|
||||
)
|
||||
restored = deploy_module.resolve_failed_live_state(
|
||||
{"live_revision": "a" * 40, "live_status": "healthy", "live_receipt": previous_receipt},
|
||||
{"revision": "a" * 40, "receipt": previous_receipt},
|
||||
product,
|
||||
production_started=True,
|
||||
failure_stage="deployment",
|
||||
restore_error=None,
|
||||
)
|
||||
assert observed == [deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS]
|
||||
assert set(restored["live_artifacts"]["containers"]) == set(deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("drift", [None, "image", "build", "image_ref"])
|
||||
def test_agenttestlab_snapshot_restore_allows_only_container_id_recreation(
|
||||
deploy_module, tmp_path, monkeypatch, drift
|
||||
):
|
||||
previous_revision = "a" * 40
|
||||
failed_revision = "d" * 40
|
||||
legacy = _agenttestlab_project(tmp_path)
|
||||
legacy["_repository_name"] = "jens/agenttestlab"
|
||||
receipt = _receipt(legacy, previous_revision, tree="c" * 40)
|
||||
project = {**legacy, "containers": deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS.copy()}
|
||||
project["_repository_path"] = str(tmp_path)
|
||||
snapshot = {
|
||||
"revision": previous_revision,
|
||||
"receipt": receipt,
|
||||
"containers": [
|
||||
{
|
||||
"name": name,
|
||||
"container_id": receipt["containers"][name]["container_id"],
|
||||
"image_id": receipt["containers"][name]["image_id"],
|
||||
"image_ref": receipt["containers"][name]["image_ref"],
|
||||
"rollback_ref": f"gitea-deploy-rollback/agenttestlab-{index}:{previous_revision[:12]}",
|
||||
}
|
||||
for index, name in enumerate(deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS)
|
||||
],
|
||||
}
|
||||
observed_projects = []
|
||||
observed_receipts = []
|
||||
monkeypatch.setattr(deploy_module, "run", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(deploy_module, "inspect_container", lambda _name: None)
|
||||
snapshot_images = {item["image_ref"]: item["image_id"] for item in snapshot["containers"]}
|
||||
monkeypatch.setattr(
|
||||
deploy_module, "inspect_image", lambda reference: {"Id": snapshot_images[reference]}
|
||||
)
|
||||
monkeypatch.setattr(deploy_module, "repository_path_from_project", lambda _project: tmp_path)
|
||||
monkeypatch.setattr(deploy_module, "sync_source", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(deploy_module, "copy_environment", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
deploy_module,
|
||||
"remove_attempt_only_compose_containers",
|
||||
lambda target, _snapshot: observed_projects.append(list(target["containers"])),
|
||||
)
|
||||
monkeypatch.setattr(deploy_module, "deployment_environment", lambda *_args: {})
|
||||
monkeypatch.setattr(
|
||||
deploy_module,
|
||||
"restore_snapshot_runtime",
|
||||
lambda target, *_args, **_kwargs: observed_projects.append(list(target["containers"])),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
deploy_module,
|
||||
"validate_recorded_receipt",
|
||||
lambda target, saved, *_args: observed_projects.append(list(target["containers"]))
|
||||
or saved["build"],
|
||||
)
|
||||
monkeypatch.setattr(deploy_module, "cleanup_rollback_image_tags", lambda *_args: None)
|
||||
|
||||
def verify(target, _build, *, expected_image_ids):
|
||||
observed_projects.append(list(target["containers"]))
|
||||
observed = copy.deepcopy(receipt)
|
||||
for item in observed["containers"].values():
|
||||
item["container_id"] = "3" * 64
|
||||
if drift == "image":
|
||||
observed["containers"][deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS[0]]["image_id"] = "sha256:" + "9" * 64
|
||||
elif drift == "build":
|
||||
observed["build"]["builtAt"] = "2026-10-02T12:00:00.000Z"
|
||||
elif drift == "image_ref":
|
||||
observed["containers"][deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS[0]]["image_ref"] = "unexpected:production"
|
||||
observed_receipts.append(observed)
|
||||
return observed
|
||||
|
||||
monkeypatch.setattr(deploy_module, "verify_deployment", verify)
|
||||
|
||||
if drift is None:
|
||||
deploy_module.restore_compose_snapshot(project, snapshot, "jens/agenttestlab", failed_revision)
|
||||
assert project["_rollback_receipt"] == observed_receipts[0]
|
||||
assert project["_rollback_receipt"]["containers"][
|
||||
deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS[0]
|
||||
]["container_id"] == "3" * 64
|
||||
assert observed_projects == [
|
||||
deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS,
|
||||
deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS,
|
||||
deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS,
|
||||
deploy_module.AGENTTESTLAB_LEGACY_CONTAINERS,
|
||||
]
|
||||
else:
|
||||
with pytest.raises(deploy_module.DeployError, match="Rollback runtime no longer matches"):
|
||||
deploy_module.restore_compose_snapshot(
|
||||
project, snapshot, "jens/agenttestlab", failed_revision
|
||||
)
|
||||
assert "_rollback_receipt" not in project
|
||||
|
||||
|
||||
def test_agenttestlab_registry_replacement_failure_leaves_original_bytes_intact(
|
||||
deploy_module, tmp_path, monkeypatch
|
||||
):
|
||||
original = {"jens/agenttestlab": _agenttestlab_project(tmp_path)}
|
||||
original["jens/agenttestlab"].pop("_repository_name")
|
||||
original["jens/agenttestlab"].pop("_repository_path")
|
||||
original_bytes = (json.dumps(original, indent=2) + "\n").encode()
|
||||
registry = tmp_path / "projects.json"
|
||||
registry.write_bytes(original_bytes)
|
||||
registry.chmod(0o600)
|
||||
expected = copy.deepcopy(original)
|
||||
expected["jens/agenttestlab"]["containers"] = deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS.copy()
|
||||
replace = os.replace
|
||||
calls = 0
|
||||
|
||||
def fail_registry_replace(source, destination):
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls == 1:
|
||||
raise OSError("synthetic atomic replacement failure")
|
||||
replace(source, destination)
|
||||
|
||||
monkeypatch.setattr(deploy_module, "CONFIG_PATH", registry)
|
||||
monkeypatch.setattr(deploy_module, "load_projects", lambda: json.loads(registry.read_text()))
|
||||
monkeypatch.setattr(deploy_module, "_root_owned", lambda _path: True)
|
||||
monkeypatch.setattr(deploy_module.os, "replace", fail_registry_replace)
|
||||
|
||||
with pytest.raises(OSError, match="synthetic atomic replacement failure"):
|
||||
deploy_module._replace_agenttestlab_registry_atomically(
|
||||
original, original_bytes, expected
|
||||
)
|
||||
|
||||
assert registry.read_bytes() == original_bytes
|
||||
assert list(tmp_path.glob(".projects.agenttestlab.*.tmp")) == []
|
||||
|
||||
|
||||
def test_agenttestlab_registry_replacement_rejects_group_readable_registry(
|
||||
deploy_module, tmp_path, monkeypatch
|
||||
):
|
||||
if os.name == "nt":
|
||||
pytest.skip("POSIX registry permission checks are unavailable on Windows")
|
||||
|
||||
original = {"jens/agenttestlab": _agenttestlab_project(tmp_path)}
|
||||
original["jens/agenttestlab"].pop("_repository_name")
|
||||
original["jens/agenttestlab"].pop("_repository_path")
|
||||
original_bytes = (json.dumps(original, indent=2) + "\n").encode()
|
||||
registry = tmp_path / "projects.json"
|
||||
registry.write_bytes(original_bytes)
|
||||
registry.chmod(0o644)
|
||||
expected = copy.deepcopy(original)
|
||||
expected["jens/agenttestlab"]["containers"] = deploy_module.AGENTTESTLAB_PRODUCT_CONTAINERS.copy()
|
||||
|
||||
monkeypatch.setattr(deploy_module, "CONFIG_PATH", registry)
|
||||
monkeypatch.setattr(deploy_module, "load_projects", lambda: json.loads(registry.read_text()))
|
||||
monkeypatch.setattr(deploy_module, "_root_owned", lambda _path: True)
|
||||
|
||||
with pytest.raises(deploy_module.DeployError, match="private root-owned file"):
|
||||
deploy_module._replace_agenttestlab_registry_atomically(
|
||||
original, original_bytes, expected
|
||||
)
|
||||
|
||||
assert registry.read_bytes() == original_bytes
|
||||
assert registry.stat().st_mode & 0o777 == 0o644
|
||||
|
||||
|
||||
def test_deployment_environment_exports_verified_build_date(deploy_module, tmp_path):
|
||||
project = _project(tmp_path)
|
||||
repository = "jens/unraid-appops-gateway"
|
||||
|
||||
Reference in new issue
Block a user