Publish curated Unraid-AppOps-Gateway source from ba66029223865607deddc234e283c11127324eb8
This commit is contained in:
1 parent
820244dbb3
commit
8dd4b40252
52 files changed
+11814
-288
No files matched your search
@@ -0,0 +1,389 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MODULE_PATH = ROOT / "ops" / "gitea-deploy-broker" / "registered_legacy_adoption.py"
|
||||
REPOSITORY = "jens/quiz-monitor"
|
||||
REQUEST_ID = "6f9619ff-8b86-4011-b42d-00c04fc964ff"
|
||||
TARGET_SHA = "a" * 40
|
||||
KEY = b"k" * 32
|
||||
|
||||
|
||||
def _load_module():
|
||||
spec = importlib.util.spec_from_file_location("registered_legacy_adoption_test", MODULE_PATH)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def module():
|
||||
return _load_module()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project():
|
||||
return {
|
||||
"slug": "quiz-monitor",
|
||||
"branch": "main",
|
||||
"compose_project": "quiz-monitor",
|
||||
"compose_files": ["docker-compose.yml"],
|
||||
"containers": ["quiz-monitor-quiz-server-1"],
|
||||
"source_dir": "/srv/example/appdata/quiz-monitor/source",
|
||||
"verify_oci_revision": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def baseline_inputs():
|
||||
state_bytes = b'{"schema_version":3,"live_revision":"0c9c1747","live_status":"unknown"}\n'
|
||||
image_id = "sha256:" + "a" * 64
|
||||
observation = {
|
||||
"sourceFingerprint": "1" * 64,
|
||||
"runtimeFingerprint": "2" * 64,
|
||||
"imageIDs": {"quiz-monitor-quiz-server-1": image_id},
|
||||
"runtimeConfiguration": {
|
||||
"environment": {"OPENRGB_HOST": "198.19.1.2", "OPENRGB_TOKEN": "private-test-value"},
|
||||
"composeModel": {"services": {"quiz-server": {"ports": ["1205:8000"]}}},
|
||||
},
|
||||
"artifacts": {
|
||||
"containers": {
|
||||
"quiz-monitor-quiz-server-1": {
|
||||
"image_id": image_id,
|
||||
"container_id": "b" * 64,
|
||||
"image_ref": "quiz-monitor:latest",
|
||||
"service": "quiz-server",
|
||||
"running": True,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return state_bytes, observation
|
||||
|
||||
|
||||
class _Runtime:
|
||||
def __init__(self, observation):
|
||||
self.observation = observation
|
||||
self.restored = []
|
||||
self.snapshots = []
|
||||
|
||||
def observe(self, repository, target, *, receipt_key):
|
||||
assert repository == REPOSITORY
|
||||
assert target["slug"] == "quiz-monitor"
|
||||
assert receipt_key == KEY
|
||||
return copy.deepcopy(self.observation)
|
||||
|
||||
def snapshot(self, repository, target, observation, request_id, *, receipt_key):
|
||||
value = {
|
||||
"revision": "unknown",
|
||||
"legacy_provenance": True,
|
||||
"containers": [
|
||||
{
|
||||
"name": "quiz-monitor-quiz-server-1",
|
||||
"image_id": observation["imageIDs"]["quiz-monitor-quiz-server-1"],
|
||||
"image_ref": "quiz-monitor:latest",
|
||||
"rollback_ref": "gitea-deploy-rollback/quiz-monitor-0:legacy-6f9619ff",
|
||||
"was_running": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
self.snapshots.append(value)
|
||||
return copy.deepcopy(value)
|
||||
|
||||
def restore(self, repository, target, snapshot, current_revision, *, receipt_key):
|
||||
self.restored.append((repository, target["slug"], snapshot, current_revision))
|
||||
return copy.deepcopy(self.observation)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api():
|
||||
def api_json(path, *, query):
|
||||
if path == f"/repos/{REPOSITORY}/branches/main":
|
||||
return {"protected": True, "commit": {"id": TARGET_SHA}}
|
||||
if path == f"/repos/{REPOSITORY}/actions/runs":
|
||||
assert query == {
|
||||
"event": "push", "branch": "main", "head_sha": TARGET_SHA,
|
||||
"page": "1", "limit": "100",
|
||||
}
|
||||
return {"total_count": 1, "workflow_runs": [{
|
||||
"id": 7, "run_number": 19, "run_attempt": 1,
|
||||
"event": "push", "head_sha": TARGET_SHA, "head_branch": "main",
|
||||
"path": "managed-validation.yml@refs/heads/main",
|
||||
"display_title": "Managed validation / full (push)",
|
||||
"created_at": "2026-10-07T12:00:00Z", "status": "completed",
|
||||
"conclusion": "success", "repository": {"full_name": REPOSITORY},
|
||||
"head_repository": {"full_name": REPOSITORY}, "pull_requests": [],
|
||||
}]}
|
||||
if path == f"/repos/{REPOSITORY}/actions/runs/7/jobs":
|
||||
assert query == {"page": "1", "limit": "100"}
|
||||
return {"total_count": 1, "jobs": [{
|
||||
"id": 9, "name": "full", "run_id": 7, "run_attempt": 1,
|
||||
"head_sha": TARGET_SHA, "head_branch": "main",
|
||||
"status": "completed", "conclusion": "success",
|
||||
}]}
|
||||
raise AssertionError(f"unexpected Gitea read: {path}")
|
||||
return api_json
|
||||
|
||||
|
||||
def _read_state(state_bytes):
|
||||
state = json.loads(state_bytes.decode("utf-8")) if state_bytes else {}
|
||||
return lambda _repository, _project: (state, state_bytes)
|
||||
|
||||
|
||||
def _baseline(module, project, state_bytes, observation, tmp_path):
|
||||
(tmp_path / "quiz-monitor.json").write_bytes(state_bytes)
|
||||
return module.baseline(
|
||||
{"repository": REPOSITORY, "requestId": REQUEST_ID},
|
||||
registry={REPOSITORY: project},
|
||||
read_state=_read_state(state_bytes),
|
||||
runtime=_Runtime(observation), receipt_key=KEY, state_root=tmp_path,
|
||||
)
|
||||
|
||||
|
||||
def test_registered_target_policy_accepts_bounded_main_compose_and_rejects_special_targets(
|
||||
module, project
|
||||
):
|
||||
assert module.validate_registered_legacy_target(REPOSITORY, project) == "quiz-monitor"
|
||||
for repository, mutate in [
|
||||
("jens/unraid-appops-gateway", lambda candidate: None),
|
||||
("jens/workmesh", lambda candidate: None),
|
||||
(REPOSITORY, lambda candidate: candidate.update(managed_plan_version="itworx.deployment/v1")),
|
||||
(REPOSITORY, lambda candidate: candidate.update(deploy_script="scripts/deploy.sh")),
|
||||
(REPOSITORY, lambda candidate: candidate.update(containers=[f"quiz-monitor-svc-{n}" for n in range(9)])),
|
||||
(REPOSITORY, lambda candidate: candidate.update(compose_files=["../other/docker-compose.yml"])),
|
||||
]:
|
||||
candidate = copy.deepcopy(project)
|
||||
mutate(candidate)
|
||||
with pytest.raises(module.RegisteredLegacyAdoptionError):
|
||||
module.validate_registered_legacy_target(repository, candidate)
|
||||
|
||||
|
||||
def test_rendered_compose_must_match_observed_environment_mounts_and_published_ports(module):
|
||||
model = {"services": {"quiz-server": {
|
||||
"environment": {"OPENRGB_HOST": "198.19.1.2"},
|
||||
"volumes": [{"type": "bind", "source": "/srv/example/appdata/quiz-monitor/config",
|
||||
"target": "/config", "read_only": False}],
|
||||
"ports": [{"target": 8000, "published": "1205", "protocol": "tcp",
|
||||
"host_ip": "0.0.0.0"}],
|
||||
}}, "networks": {"default": {"name": "quiz-monitor_default"}}}
|
||||
container = {"quiz-monitor-quiz-server-1": {
|
||||
"Config": {"Env": ["OPENRGB_HOST=198.19.1.2"]},
|
||||
"Mounts": [{"Type": "bind", "Source": "/srv/example/appdata/quiz-monitor/config",
|
||||
"Destination": "/config", "RW": True}],
|
||||
"HostConfig": {"NetworkMode": "quiz-monitor_default"},
|
||||
"NetworkSettings": {"Networks": {"quiz-monitor_default": {}},
|
||||
"Ports": {"8000/tcp": [{"HostIp": "0.0.0.0", "HostPort": "1205"}]}},
|
||||
}}
|
||||
module.validate_compose_runtime(model, container)
|
||||
wrong = copy.deepcopy(container)
|
||||
wrong["quiz-monitor-quiz-server-1"]["NetworkSettings"]["Ports"]["8000/tcp"][0]["HostPort"] = "1206"
|
||||
with pytest.raises(module.RegisteredLegacyAdoptionError):
|
||||
module.validate_compose_runtime(model, wrong)
|
||||
|
||||
|
||||
def test_rendered_compose_rejects_changed_command_security_and_named_volume_identity(module):
|
||||
model = {"services": {"quiz-server": {
|
||||
"image": "quiz-monitor:latest", "command": ["python", "server.py"],
|
||||
"security_opt": ["no-new-privileges:true"],
|
||||
"volumes": [{"type": "volume", "source": "quiz-config", "target": "/config"}],
|
||||
}}, "volumes": {"quiz-config": {"name": "quiz-monitor-config"}},
|
||||
"networks": {"default": {"name": "quiz-monitor_default"}}}
|
||||
original = {"quiz-monitor-quiz-server-1": {
|
||||
"Config": {"Cmd": ["python", "server.py"], "Labels": {"com.docker.compose.service": "quiz-server"}},
|
||||
"HostConfig": {"SecurityOpt": ["no-new-privileges:true"], "NetworkMode": "quiz-monitor_default"},
|
||||
"Mounts": [{"Type": "volume", "Name": "quiz-monitor-config", "Destination": "/config", "RW": True}],
|
||||
"NetworkSettings": {"Networks": {"quiz-monitor_default": {}}, "Ports": {}},
|
||||
}}
|
||||
module.validate_compose_runtime(model, original, compose_project="quiz-monitor")
|
||||
changed = copy.deepcopy(original)
|
||||
changed["quiz-monitor-quiz-server-1"]["Config"]["Cmd"] = ["sh", "-c", "unexpected"]
|
||||
with pytest.raises(module.RegisteredLegacyAdoptionError):
|
||||
module.validate_compose_runtime(model, changed, compose_project="quiz-monitor")
|
||||
changed = copy.deepcopy(original)
|
||||
changed["quiz-monitor-quiz-server-1"]["HostConfig"]["SecurityOpt"] = []
|
||||
with pytest.raises(module.RegisteredLegacyAdoptionError):
|
||||
module.validate_compose_runtime(model, changed, compose_project="quiz-monitor")
|
||||
changed = copy.deepcopy(original)
|
||||
changed["quiz-monitor-quiz-server-1"]["Mounts"][0]["Name"] = "unexpected-volume"
|
||||
with pytest.raises(module.RegisteredLegacyAdoptionError):
|
||||
module.validate_compose_runtime(model, changed, compose_project="quiz-monitor")
|
||||
|
||||
|
||||
def test_baseline_binds_exact_runtime_state_without_revealing_stale_sha_or_secrets(
|
||||
module, project, baseline_inputs, tmp_path
|
||||
):
|
||||
state_bytes, observation = baseline_inputs
|
||||
result = _baseline(module, project, state_bytes, observation, tmp_path)
|
||||
|
||||
assert result["previousRevision"] == "unknown"
|
||||
assert result["expectedStateSha256"] == hashlib.sha256(state_bytes).hexdigest()
|
||||
assert result["runtimeFingerprint"] == "2" * 64
|
||||
assert result["imageIDs"] == observation["imageIDs"]
|
||||
assert "0c9c1747" not in json.dumps(result)
|
||||
assert "private-test-value" not in json.dumps(result)
|
||||
root = tmp_path / "registered-legacy-adoption" / "quiz-monitor"
|
||||
assert (root / f"{REQUEST_ID}.baseline.json").is_file()
|
||||
private = root / f"{REQUEST_ID}.private.json"
|
||||
assert private.is_file()
|
||||
assert "private-test-value" not in (root / f"{REQUEST_ID}.baseline.json").read_text()
|
||||
|
||||
|
||||
def test_preflight_requires_latest_exact_main_push_and_registered_full_job(
|
||||
module, project, baseline_inputs, api, tmp_path
|
||||
):
|
||||
state_bytes, observation = baseline_inputs
|
||||
baseline = _baseline(module, project, state_bytes, observation, tmp_path)
|
||||
result = module.preflight(
|
||||
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
|
||||
"expectedStateSha256": baseline["expectedStateSha256"]},
|
||||
registry={REPOSITORY: project}, read_state=_read_state(state_bytes),
|
||||
runtime=_Runtime(observation), api_json=api, receipt_key=KEY, state_root=tmp_path,
|
||||
)
|
||||
assert result["status"] == "preflight-ok"
|
||||
assert result["revision"] == TARGET_SHA
|
||||
assert result["validationEvidenceSha256"]
|
||||
assert result["preflightSha256"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", ["pr_event", "wrong_sha", "failed_latest", "missing_full_job", "unprotected"])
|
||||
def test_preflight_rejects_noncanonical_or_failed_latest_validation(
|
||||
module, project, baseline_inputs, api, tmp_path, invalid
|
||||
):
|
||||
state_bytes, observation = baseline_inputs
|
||||
baseline = _baseline(module, project, state_bytes, observation, tmp_path)
|
||||
|
||||
def bad_api(path, *, query):
|
||||
value = api(path, query=query)
|
||||
if path.endswith("/actions/runs"):
|
||||
run = value["workflow_runs"][0]
|
||||
if invalid == "pr_event":
|
||||
run["event"] = "pull_request"
|
||||
elif invalid == "wrong_sha":
|
||||
run["head_sha"] = "b" * 40
|
||||
elif invalid == "failed_latest":
|
||||
run["run_attempt"] = 2
|
||||
run["conclusion"] = "failure"
|
||||
elif path.endswith("/branches/main") and invalid == "unprotected":
|
||||
value["protected"] = False
|
||||
elif path.endswith("/jobs") and invalid == "missing_full_job":
|
||||
value["jobs"][0]["name"] = "backend"
|
||||
return value
|
||||
|
||||
with pytest.raises(module.RegisteredLegacyAdoptionError):
|
||||
module.preflight(
|
||||
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
|
||||
"expectedStateSha256": baseline["expectedStateSha256"]},
|
||||
registry={REPOSITORY: project}, read_state=_read_state(state_bytes),
|
||||
runtime=_Runtime(observation), api_json=bad_api, receipt_key=KEY, state_root=tmp_path,
|
||||
)
|
||||
|
||||
|
||||
def test_adoption_rechecks_gate_and_cas_then_saves_unknown_rollback_proof(
|
||||
module, project, baseline_inputs, api, tmp_path
|
||||
):
|
||||
state_bytes, observation = baseline_inputs
|
||||
(tmp_path / "quiz-monitor.json").write_bytes(state_bytes)
|
||||
runtime = _Runtime(observation)
|
||||
registry = {REPOSITORY: project}
|
||||
read_state = _read_state(state_bytes)
|
||||
baseline = module.baseline(
|
||||
{"repository": REPOSITORY, "requestId": REQUEST_ID}, registry=registry,
|
||||
read_state=read_state, runtime=runtime, receipt_key=KEY, state_root=tmp_path,
|
||||
)
|
||||
preflight = module.preflight(
|
||||
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
|
||||
"expectedStateSha256": baseline["expectedStateSha256"]},
|
||||
registry=registry, read_state=read_state, runtime=runtime, api_json=api,
|
||||
receipt_key=KEY, state_root=tmp_path,
|
||||
)
|
||||
# Adoption independently rereads main and validation rather than trusting the saved preflight.
|
||||
calls = []
|
||||
|
||||
def rechecked_api(path, *, query):
|
||||
calls.append(path)
|
||||
return api(path, query=query)
|
||||
|
||||
result = module.adopt(
|
||||
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
|
||||
"expectedStateSha256": baseline["expectedStateSha256"],
|
||||
"preflightSha256": preflight["preflightSha256"]},
|
||||
registry=registry, read_state=read_state, runtime=runtime, api_json=rechecked_api,
|
||||
receipt_key=KEY, state_root=tmp_path,
|
||||
)
|
||||
assert result["status"] == "adopted"
|
||||
assert result["previousRevision"] == "unknown"
|
||||
assert any(path.endswith("/actions/runs") for path in calls)
|
||||
receipt = tmp_path / "registered-legacy-adoption" / "quiz-monitor" / f"{REQUEST_ID}.receipt.json"
|
||||
assert receipt.is_file()
|
||||
assert "private-test-value" not in receipt.read_text()
|
||||
|
||||
snapshot = module.capture_deployment_snapshot(
|
||||
REPOSITORY, project, json.loads(state_bytes), request_id=REQUEST_ID,
|
||||
target_revision=TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime,
|
||||
)
|
||||
assert snapshot["revision"] == "unknown"
|
||||
assert snapshot["legacy_provenance"] is True
|
||||
assert snapshot["containers"][0]["image_id"] == observation["imageIDs"]["quiz-monitor-quiz-server-1"]
|
||||
assert snapshot["registered_legacy"]["snapshotSha256"]
|
||||
assert module.status(
|
||||
{"repository": REPOSITORY, "requestId": REQUEST_ID}, registry=registry,
|
||||
read_state=read_state, receipt_key=KEY, state_root=tmp_path,
|
||||
)["status"] == "adopted"
|
||||
|
||||
|
||||
def test_adoption_rejects_a_changed_controller_state_before_snapshot(
|
||||
module, project, baseline_inputs, api, tmp_path
|
||||
):
|
||||
state_bytes, observation = baseline_inputs
|
||||
current_bytes = [state_bytes]
|
||||
def read_state(_repository, _project):
|
||||
return json.loads(current_bytes[0].decode("utf-8")), current_bytes[0]
|
||||
runtime = _Runtime(observation)
|
||||
registry = {REPOSITORY: project}
|
||||
baseline = module.baseline(
|
||||
{"repository": REPOSITORY, "requestId": REQUEST_ID}, registry=registry,
|
||||
read_state=read_state, runtime=runtime, receipt_key=KEY, state_root=tmp_path,
|
||||
)
|
||||
preflight = module.preflight(
|
||||
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
|
||||
"expectedStateSha256": baseline["expectedStateSha256"]},
|
||||
registry=registry, read_state=read_state, runtime=runtime, api_json=api,
|
||||
receipt_key=KEY, state_root=tmp_path,
|
||||
)
|
||||
current_bytes[0] += b" "
|
||||
with pytest.raises(module.RegisteredLegacyAdoptionError):
|
||||
module.adopt(
|
||||
{"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID,
|
||||
"expectedStateSha256": baseline["expectedStateSha256"],
|
||||
"preflightSha256": preflight["preflightSha256"]},
|
||||
registry=registry, read_state=read_state, runtime=runtime, api_json=api,
|
||||
receipt_key=KEY, state_root=tmp_path,
|
||||
)
|
||||
assert not (tmp_path / "registered-legacy-adoption" / "quiz-monitor" / f"{REQUEST_ID}.receipt.json").exists()
|
||||
|
||||
|
||||
def test_status_is_nonblocking_and_worker_failure_is_durable_and_terminal_safe(
|
||||
module, project, baseline_inputs, tmp_path
|
||||
):
|
||||
state_bytes, observation = baseline_inputs
|
||||
_baseline(module, project, state_bytes, observation, tmp_path)
|
||||
pending = module.record_worker_failure(
|
||||
REPOSITORY, REQUEST_ID, "worker_start_failed", receipt_key=KEY, state_root=tmp_path,
|
||||
)
|
||||
assert pending["status"] == "recovery-required"
|
||||
assert module.record_worker_failure(
|
||||
REPOSITORY, REQUEST_ID, "second_failure", receipt_key=KEY, state_root=tmp_path,
|
||||
)["status"] == "recovery-required"
|
||||
assert module.status(
|
||||
{"repository": REPOSITORY, "requestId": REQUEST_ID},
|
||||
registry={REPOSITORY: project}, read_state=_read_state(state_bytes),
|
||||
receipt_key=KEY, state_root=tmp_path,
|
||||
)["status"] == "recovery-required"
|
||||
Reference in new issue
Block a user