Initial curated Unraid-AppOps-Gateway source from 3bd6f6fee310125bfbb4d4650389a817f498724d
This commit is contained in:
commit
1dfbca08a1
528 files changed
+131054
No files matched your search
@@ -0,0 +1,413 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
CLI_PATH = REPO_ROOT / "ops" / "gitea-deploy-broker" / "repopulse_adoption_cli.py"
|
||||
|
||||
|
||||
def _load_module(name: str, path: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
cli = _load_module("repopulse_adoption_cli_test", CLI_PATH)
|
||||
|
||||
|
||||
def _sha(value: bytes | str) -> str:
|
||||
raw = value.encode("ascii") if isinstance(value, str) else value
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def _canonical_sha(value: object) -> str:
|
||||
raw = json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
ensure_ascii=True,
|
||||
allow_nan=False,
|
||||
).encode("ascii")
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def _projection() -> dict[str, object]:
|
||||
return {
|
||||
"repository": "jens/repopulse",
|
||||
"main_revision": "a" * 40,
|
||||
"live_revision": "b" * 40,
|
||||
"container_id_sha256": _sha("legacy-container"),
|
||||
"container_image_sha256": _sha("legacy-image"),
|
||||
"queue": {"queued": 0, "active": 0, "completed": 10, "failed": 0},
|
||||
"database": {
|
||||
"database_bytes": 12345,
|
||||
"journal_bytes": 0,
|
||||
"scan_count": 10,
|
||||
"compacted_scan_count": 4,
|
||||
},
|
||||
"container_state": "running",
|
||||
"container_health": "healthy",
|
||||
}
|
||||
|
||||
|
||||
def _payload() -> dict[str, object]:
|
||||
idempotency = _sha("itworx.repopulse-legacy-adoption/v1:jens/repopulse:cli-test")
|
||||
projection = _projection()
|
||||
return {
|
||||
"intentId": f"rpa-{idempotency[:20]}",
|
||||
"idempotencySha256": idempotency,
|
||||
"preflightSha256": _canonical_sha(projection),
|
||||
"preflightProjection": projection,
|
||||
"targetCredential": "A" * 64,
|
||||
}
|
||||
|
||||
|
||||
def _raw(payload: dict[str, object] | None = None) -> bytes:
|
||||
return json.dumps(payload or _payload(), separators=(",", ":")).encode("utf-8")
|
||||
|
||||
|
||||
def _core():
|
||||
return cli._load_core_modules()
|
||||
|
||||
|
||||
def _parsed(payload: dict[str, object] | None = None):
|
||||
core = _core()
|
||||
return cli.parse_handoff(_raw(payload), core.executor)
|
||||
|
||||
|
||||
def test_parser_accepts_exact_handoff_and_rejects_duplicate_keys_at_any_depth():
|
||||
core = _core()
|
||||
payload = _payload()
|
||||
parsed = cli.parse_handoff(_raw(payload), core.executor)
|
||||
|
||||
assert parsed.intent_id == payload["intentId"]
|
||||
assert parsed.target_credential == b"A" * 64
|
||||
assert parsed.preflight_projection == payload["preflightProjection"]
|
||||
|
||||
regular = _raw(payload)
|
||||
duplicate_top = (
|
||||
b'{"intentId":'
|
||||
+ json.dumps(payload["intentId"]).encode("ascii")
|
||||
+ bytes([44])
|
||||
+ regular[1:]
|
||||
)
|
||||
with pytest.raises(cli.ControllerCliError) as error:
|
||||
cli.parse_handoff(duplicate_top, core.executor)
|
||||
assert error.value.code == "controller_handoff_duplicate_key"
|
||||
|
||||
duplicate_nested = regular.replace(b'"queued":0', b'"queued":0,"queued":0', 1)
|
||||
with pytest.raises(cli.ControllerCliError) as error:
|
||||
cli.parse_handoff(duplicate_nested, core.executor)
|
||||
assert error.value.code == "controller_handoff_duplicate_key"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"change",
|
||||
[
|
||||
lambda value: value.pop("targetCredential"),
|
||||
lambda value: value.update({"extra": True}),
|
||||
lambda value: value.update({"targetCredential": "short"}),
|
||||
lambda value: value.update({"targetCredential": "x" * 48 + "\n"}),
|
||||
],
|
||||
)
|
||||
def test_parser_rejects_schema_changes_and_noncontroller_credentials(change):
|
||||
core = _core()
|
||||
payload = _payload()
|
||||
change(payload)
|
||||
|
||||
with pytest.raises(cli.ControllerCliError):
|
||||
cli.parse_handoff(_raw(payload), core.executor)
|
||||
|
||||
|
||||
def test_parser_enforces_bounded_stdin_size():
|
||||
core = _core()
|
||||
with pytest.raises(cli.ControllerCliError) as error:
|
||||
cli.parse_handoff(b"x" * (cli._MAX_HANDOFF_BYTES + 1), core.executor)
|
||||
assert error.value.code == "controller_handoff_too_large"
|
||||
|
||||
|
||||
def test_reserved_binding_requires_existing_matching_intent_and_credential():
|
||||
core = _core()
|
||||
handoff = _parsed()
|
||||
binding = core.store.intent_binding_sha256(
|
||||
handoff.intent_id,
|
||||
handoff.idempotency_sha256,
|
||||
handoff.preflight_sha256,
|
||||
)
|
||||
record = {
|
||||
"intentId": handoff.intent_id,
|
||||
"idempotencySha256": handoff.idempotency_sha256,
|
||||
"preflightSha256": handoff.preflight_sha256,
|
||||
"intentBindingSha256": binding,
|
||||
"expectedTargetCredentialSha256": _sha(handoff.target_credential),
|
||||
"status": "in_progress",
|
||||
"registration": None,
|
||||
}
|
||||
|
||||
class Store:
|
||||
def read_only_status(self):
|
||||
return {
|
||||
"status": record["status"],
|
||||
"hashes": {
|
||||
"idempotencySha256": record["idempotencySha256"],
|
||||
"preflightSha256": record["preflightSha256"],
|
||||
"intentBindingSha256": record["intentBindingSha256"],
|
||||
},
|
||||
}
|
||||
|
||||
def load_intent(self):
|
||||
return record
|
||||
|
||||
assert cli._require_reserved_binding(
|
||||
store=Store(), store_module=core.store, handoff=handoff
|
||||
) is record
|
||||
|
||||
mismatched = replace(handoff, preflight_sha256=_sha("another preflight"))
|
||||
with pytest.raises(cli.ControllerCliError) as error:
|
||||
cli._require_reserved_binding(
|
||||
store=Store(), store_module=core.store, handoff=mismatched
|
||||
)
|
||||
assert error.value.code == "controller_intent_binding_mismatch"
|
||||
|
||||
wrong_credential = replace(handoff, target_credential=b"B" * 64)
|
||||
with pytest.raises(cli.ControllerCliError) as error:
|
||||
cli._require_reserved_binding(
|
||||
store=Store(), store_module=core.store, handoff=wrong_credential
|
||||
)
|
||||
assert error.value.code == "target_credential_binding_mismatch"
|
||||
|
||||
|
||||
def test_missing_reservation_is_rejected_before_loading_or_creating_journal():
|
||||
core = _core()
|
||||
handoff = _parsed()
|
||||
|
||||
class Store:
|
||||
def read_only_status(self):
|
||||
return None
|
||||
|
||||
def load_intent(self):
|
||||
raise AssertionError("missing reservation must not load mutable state")
|
||||
|
||||
with pytest.raises(cli.ControllerCliError) as error:
|
||||
cli._require_reserved_binding(
|
||||
store=Store(), store_module=core.store, handoff=handoff
|
||||
)
|
||||
assert error.value.code == "controller_intent_not_reserved"
|
||||
|
||||
|
||||
def test_fixed_storage_bridge_forwards_executor_revision_and_keeps_credential_in_memory():
|
||||
handoff = _parsed()
|
||||
|
||||
class StorageHooks:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def ensure_quiesced(self, **kwargs):
|
||||
self.calls.append(("quiesce", kwargs))
|
||||
return {"ok": True}
|
||||
|
||||
def create_or_verify_fresh_snapshot(self, **kwargs):
|
||||
self.calls.append(("snapshot", kwargs))
|
||||
return {"ok": True}
|
||||
|
||||
def copy_and_verify_managed_data(self, **kwargs):
|
||||
self.calls.append(("copy", kwargs))
|
||||
return {"ok": True}
|
||||
|
||||
hooks = StorageHooks()
|
||||
bridge = cli._FixedHandoffStorageEffects(
|
||||
store=object(),
|
||||
store_module=SimpleNamespace(REPOSITORY="jens/repopulse"),
|
||||
storage_effects=hooks,
|
||||
legacy_rollback=object(),
|
||||
registry_restore=object(),
|
||||
handoff=handoff,
|
||||
)
|
||||
revision = handoff.preflight_projection["main_revision"]
|
||||
binding = "c" * 64
|
||||
|
||||
bridge.ensure_quiesced(
|
||||
intent_id=handoff.intent_id,
|
||||
intent_binding_sha256=binding,
|
||||
expected_main_revision=revision,
|
||||
expected_container_id_sha256="d" * 64,
|
||||
expected_container_image_sha256="e" * 64,
|
||||
)
|
||||
bridge.create_or_verify_fresh_snapshot(
|
||||
intent_id=handoff.intent_id,
|
||||
intent_binding_sha256=binding,
|
||||
expected_main_revision=revision,
|
||||
expected_snapshot=None,
|
||||
)
|
||||
bridge.copy_and_verify_managed_data(
|
||||
intent_id=handoff.intent_id,
|
||||
expected_main_revision=revision,
|
||||
snapshot_evidence={"intentId": handoff.intent_id},
|
||||
)
|
||||
|
||||
assert [call[0] for call in hooks.calls] == ["quiesce", "snapshot", "copy"]
|
||||
assert all(call[1]["expected_main_revision"] == revision for call in hooks.calls)
|
||||
assert bridge.registration_material(intent_id=handoff.intent_id) == (
|
||||
f"enr-{handoff.intent_id[4:]}",
|
||||
b"A" * 64,
|
||||
)
|
||||
with pytest.raises(cli.ControllerCliError):
|
||||
bridge.registration_material(intent_id="rpa-" + "f" * 20)
|
||||
|
||||
|
||||
def test_executor_store_proxy_snapshots_registry_before_registration_cas():
|
||||
events = []
|
||||
|
||||
class Store:
|
||||
def register_target(self, **kwargs):
|
||||
events.append(("register", kwargs))
|
||||
return {"status": "registered"}
|
||||
|
||||
def read_only_status(self):
|
||||
return {"status": "in_progress"}
|
||||
|
||||
class RegistryRestore:
|
||||
def preserve_before_registry_cas(self, store):
|
||||
assert isinstance(store, Store)
|
||||
events.append(("preserve", None))
|
||||
|
||||
proxy = cli._RegistrySnapshotStoreProxy(Store(), RegistryRestore())
|
||||
assert proxy.register_target(intent_id="rpa-" + "a" * 20) == {
|
||||
"status": "registered"
|
||||
}
|
||||
assert proxy.read_only_status() == {"status": "in_progress"}
|
||||
assert [name for name, _value in events] == ["preserve", "register"]
|
||||
|
||||
|
||||
def test_serialized_result_rejects_secret_leaks_and_unexpected_fields():
|
||||
credential = b"A" * 64
|
||||
intent_id = "rpa-" + "a" * 20
|
||||
result = {key: None for key in cli._RESULT_KEYS}
|
||||
result.update(
|
||||
{
|
||||
"apiVersion": "itworx.repopulse-controller-adoption-execution-result/v1",
|
||||
"repository": "jens/repopulse",
|
||||
"applicationId": "repopulse",
|
||||
"intentId": intent_id,
|
||||
"status": "deployment_verified",
|
||||
"phase": "completion_verified",
|
||||
"legacyVolume": "repopulse_data",
|
||||
"legacyVolumeRetained": True,
|
||||
}
|
||||
)
|
||||
result["deploymentReceipt"] = {"note": credential.decode("ascii")}
|
||||
|
||||
with pytest.raises(cli.ControllerCliError) as error:
|
||||
cli._serialize_result(
|
||||
result, target_credential=credential, intent_id=intent_id
|
||||
)
|
||||
assert error.value.code == "controller_result_secret_detected"
|
||||
|
||||
result["deploymentReceipt"] = {}
|
||||
result["targetCredential"] = credential.decode("ascii")
|
||||
with pytest.raises(cli.ControllerCliError) as error:
|
||||
cli._serialize_result(
|
||||
result, target_credential=credential, intent_id=intent_id
|
||||
)
|
||||
assert error.value.code == "controller_result_schema_invalid"
|
||||
|
||||
|
||||
def test_disabled_controller_does_not_read_handoff_or_load_effects():
|
||||
core = cli.CoreModules(
|
||||
store=SimpleNamespace(EXECUTION_ENABLED=False),
|
||||
executor=SimpleNamespace(EXECUTION_ENABLED=False),
|
||||
)
|
||||
effects_loaded = False
|
||||
root_checked = False
|
||||
|
||||
class UnreadableInput:
|
||||
def read(self, _size):
|
||||
raise AssertionError("disabled controller must not read stdin")
|
||||
|
||||
def load_effects(_core):
|
||||
nonlocal effects_loaded
|
||||
effects_loaded = True
|
||||
raise AssertionError("disabled controller must not construct effects")
|
||||
|
||||
def require_root():
|
||||
nonlocal root_checked
|
||||
root_checked = True
|
||||
|
||||
output = io.StringIO()
|
||||
code = cli._run_cli(
|
||||
argv=(),
|
||||
stdin=UnreadableInput(),
|
||||
stdout=output,
|
||||
load_core=lambda: core,
|
||||
load_effects=load_effects,
|
||||
require_root=require_root,
|
||||
)
|
||||
|
||||
assert code == 2
|
||||
assert json.loads(output.getvalue()) == {
|
||||
"status": "disabled",
|
||||
"reason": "controller_adoption_execution_disabled",
|
||||
}
|
||||
assert not effects_loaded
|
||||
assert root_checked
|
||||
|
||||
|
||||
def test_nonroot_controller_rejects_before_loading_modules_or_reading_handoff():
|
||||
core_loaded = False
|
||||
|
||||
class UnreadableInput:
|
||||
def read(self, _size):
|
||||
raise AssertionError("nonroot controller must not read stdin")
|
||||
|
||||
def load_core():
|
||||
nonlocal core_loaded
|
||||
core_loaded = True
|
||||
raise AssertionError("nonroot controller must not load modules")
|
||||
|
||||
def require_root():
|
||||
raise cli.ControllerCliError("controller_root_required")
|
||||
|
||||
output = io.StringIO()
|
||||
code = cli._run_cli(
|
||||
argv=(),
|
||||
stdin=UnreadableInput(),
|
||||
stdout=output,
|
||||
load_core=load_core,
|
||||
require_root=require_root,
|
||||
)
|
||||
|
||||
assert code == 2
|
||||
assert json.loads(output.getvalue()) == {
|
||||
"status": "blocked",
|
||||
"reason": "controller_root_required",
|
||||
}
|
||||
assert not core_loaded
|
||||
|
||||
|
||||
def test_real_fixed_effect_modules_compose_without_side_effects():
|
||||
core = cli._load_core_modules()
|
||||
effects = cli._load_effect_modules(core)
|
||||
handoff = _parsed()
|
||||
store = core.store.RepoPulseAdoptionStore()
|
||||
|
||||
executor = cli._compose_executor(
|
||||
core=core,
|
||||
effects=effects,
|
||||
store=store,
|
||||
handoff=handoff,
|
||||
)
|
||||
|
||||
assert isinstance(executor, core.executor.RepoPulseAdoptionExecutor)
|
||||
assert executor._effects.__class__.__name__ == "_RevisionBoundRuntimeEffects"
|
||||
Reference in new issue
Block a user