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,345 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import http.client
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from contextlib import redirect_stdout
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
BROKER_DIR = REPO_ROOT / "ops" / "gitea-deploy-broker"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def storage_effects():
|
||||
_load_module("repopulse_adoption_store", BROKER_DIR / "repopulse_adoption_store.py")
|
||||
return _load_module(
|
||||
"repopulse_adoption_storage_effects_test",
|
||||
BROKER_DIR / "repopulse_adoption_storage_effects.py",
|
||||
)
|
||||
|
||||
|
||||
def _run_git(*args: str, cwd: Path | None = None) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=cwd,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def _bare_repo_with_helper(tmp_path: Path, helper: bytes) -> tuple[Path, str]:
|
||||
worktree = tmp_path / "worktree"
|
||||
bare_repo = tmp_path / "repositories" / "jens" / "repopulse.git"
|
||||
worktree.mkdir()
|
||||
bare_repo.parent.mkdir(parents=True)
|
||||
_run_git("init", "--bare", str(bare_repo))
|
||||
_run_git("init", "--initial-branch=main", str(worktree))
|
||||
_run_git("config", "user.name", "Storage effects test", cwd=worktree)
|
||||
_run_git("config", "user.email", "storage-effects@example.invalid", cwd=worktree)
|
||||
helper_path = worktree / "scripts" / "repopulse_db_migration.py"
|
||||
helper_path.parent.mkdir()
|
||||
helper_path.write_bytes(helper)
|
||||
_run_git("add", "scripts/repopulse_db_migration.py", cwd=worktree)
|
||||
_run_git("commit", "-m", "Add reviewed migration helper", cwd=worktree)
|
||||
_run_git("remote", "add", "origin", str(bare_repo), cwd=worktree)
|
||||
_run_git("push", "origin", "main", cwd=worktree)
|
||||
return bare_repo, _run_git("rev-parse", "HEAD", cwd=worktree)
|
||||
|
||||
|
||||
def test_maintenance_http_error_is_sanitized(storage_effects):
|
||||
with pytest.raises(storage_effects.StorageEffectsError) as error:
|
||||
storage_effects._maintenance_is_drained(
|
||||
{
|
||||
"before": {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"enabled": False,
|
||||
"queue_depth": 0,
|
||||
"active_jobs": 0,
|
||||
},
|
||||
},
|
||||
"enter": {"status": [], "body": None},
|
||||
"status": {"status": 200, "body": {"enabled": True}},
|
||||
}
|
||||
)
|
||||
|
||||
assert error.value.code == "maintenance_response_invalid"
|
||||
|
||||
|
||||
def test_maintenance_queue_precheck_avoids_freezing_queued_work(
|
||||
storage_effects, monkeypatch
|
||||
):
|
||||
token = "test-maintenance-token-value-0123456789"
|
||||
calls: list[tuple[str, str]] = []
|
||||
|
||||
class Response:
|
||||
status = 200
|
||||
|
||||
def __init__(self, payload):
|
||||
self.payload = json.dumps(payload).encode("utf-8")
|
||||
|
||||
def read(self, _limit):
|
||||
return self.payload
|
||||
|
||||
class Connection:
|
||||
def __init__(self, _host, *args, **kwargs):
|
||||
assert args == (8000,)
|
||||
assert kwargs["timeout"] == 5
|
||||
|
||||
def request(self, method, path, **_kwargs):
|
||||
calls.append((method, path))
|
||||
|
||||
def getresponse(self):
|
||||
if len(calls) == 1:
|
||||
return Response(
|
||||
{
|
||||
"enabled": False,
|
||||
"queue_depth": 2,
|
||||
"active_jobs": 0,
|
||||
"active_drained": True,
|
||||
"drained": False,
|
||||
}
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"enabled": True,
|
||||
"queue_depth": 2,
|
||||
"active_jobs": 0,
|
||||
"active_drained": True,
|
||||
"drained": False,
|
||||
}
|
||||
)
|
||||
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(http.client, "HTTPConnection", Connection)
|
||||
monkeypatch.setattr(sys, "stdin", io.StringIO(token))
|
||||
output = io.StringIO()
|
||||
with redirect_stdout(output):
|
||||
exec( # noqa: S102 - exercise the exact fixed in-container exchange script
|
||||
storage_effects._REMOTE_MAINTENANCE_EXCHANGE, {"__name__": "__main__"}
|
||||
)
|
||||
|
||||
response = json.loads(output.getvalue())
|
||||
assert calls == [("GET", storage_effects.MAINTENANCE_STATUS_PATH)]
|
||||
assert response["enter"] is None
|
||||
assert token not in output.getvalue()
|
||||
|
||||
|
||||
def test_legacy_main_revision_mismatch_stops_before_loading_helper(
|
||||
storage_effects, tmp_path, monkeypatch
|
||||
):
|
||||
helper_bytes = b"# reviewed test helper\n"
|
||||
bare_repo, actual_revision = _bare_repo_with_helper(tmp_path, helper_bytes)
|
||||
source_root = tmp_path / "sources" / "repopulse"
|
||||
helper_source = source_root / "scripts" / "repopulse_db_migration.py"
|
||||
helper_source.parent.mkdir(parents=True)
|
||||
helper_source.write_bytes(helper_bytes)
|
||||
expected_revision = "0" * 40
|
||||
assert actual_revision != expected_revision
|
||||
|
||||
core = type("GitCore", (), {})()
|
||||
core.repository_path = lambda _repository, _project: bare_repo
|
||||
core.output = lambda command: subprocess.run(
|
||||
command,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
core.validate_commit = lambda repo, branch, revision: storage_effects._fixed_core(
|
||||
None
|
||||
).validate_commit(repo, branch, revision)
|
||||
|
||||
monkeypatch.setattr(storage_effects, "EXACT_SOURCE_ROOT", source_root.parent)
|
||||
monkeypatch.setattr(storage_effects._store, "_ROOT_UID", source_root.stat().st_uid)
|
||||
|
||||
def controller_directory(path: Path, *, root_owned: bool = False):
|
||||
storage_effects._store._assert_no_symlink_components(path)
|
||||
info = path.lstat()
|
||||
assert info.st_mode
|
||||
return info
|
||||
|
||||
monkeypatch.setattr(storage_effects, "_validate_directory", controller_directory)
|
||||
monkeypatch.setattr(
|
||||
storage_effects,
|
||||
"_validate_git_repository_directory",
|
||||
lambda path: path.lstat(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
storage_effects._store,
|
||||
"_safe_directory",
|
||||
lambda path, **_kwargs: (
|
||||
storage_effects._store._assert_no_symlink_components(path) or path.lstat()
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(storage_effects.StorageEffectsError) as error:
|
||||
storage_effects._load_reviewed_migration_helper(
|
||||
core,
|
||||
{"source_dir": str(source_root)},
|
||||
expected_revision,
|
||||
)
|
||||
|
||||
assert error.value.code == "main_revision_mismatch"
|
||||
|
||||
|
||||
def test_oversized_helper_is_rejected_before_git_returns_blob(
|
||||
storage_effects, tmp_path, monkeypatch
|
||||
):
|
||||
helper_bytes = b"#" * (storage_effects._HELPER_MAX_BYTES + 1)
|
||||
bare_repo, revision = _bare_repo_with_helper(tmp_path, helper_bytes)
|
||||
real_run = subprocess.run
|
||||
commands: list[list[str]] = []
|
||||
|
||||
def recording_run(args, *positional, **keyword):
|
||||
commands.append(list(args))
|
||||
return real_run(args, *positional, **keyword)
|
||||
|
||||
monkeypatch.setattr(storage_effects.subprocess, "run", recording_run)
|
||||
|
||||
with pytest.raises(storage_effects.StorageEffectsError) as error:
|
||||
storage_effects._read_git_blob(bare_repo, revision)
|
||||
|
||||
assert error.value.code == "reviewed_migration_helper_invalid"
|
||||
assert len(commands) == 1
|
||||
assert commands[0][2:4] == ["cat-file", "-s"]
|
||||
|
||||
|
||||
def test_legacy_maintenance_404_blocks_stop(storage_effects, tmp_path, monkeypatch):
|
||||
revision = "a" * 40
|
||||
container_id = "b" * 64
|
||||
image_id = "sha256:" + "c" * 64
|
||||
image_ref = "ghcr.io/jens/repopulse:legacy"
|
||||
token = "fixed-maintenance-token-value-0123456789"
|
||||
intent_id = "rpa-0123456789abcdef0123"
|
||||
binding = "d" * 64
|
||||
container_id_sha256 = hashlib.sha256(container_id.encode("ascii")).hexdigest()
|
||||
image_sha256 = hashlib.sha256(image_ref.encode("utf-8")).hexdigest()
|
||||
|
||||
class Store:
|
||||
def _lock_identity(self):
|
||||
return ("project-lock", "global-lock")
|
||||
|
||||
def _transaction_owner(self, _identity):
|
||||
return threading.get_ident()
|
||||
|
||||
def load_intent(self):
|
||||
return {
|
||||
"intentId": intent_id,
|
||||
"intentBindingSha256": binding,
|
||||
"status": "in_progress",
|
||||
"phase": "target_gates_ready",
|
||||
}
|
||||
|
||||
class Core:
|
||||
def __init__(self):
|
||||
self.commands: list[list[str]] = []
|
||||
|
||||
def inspect_container(self, _name):
|
||||
return {
|
||||
"Id": container_id,
|
||||
"Name": "/repopulse",
|
||||
"Image": image_id,
|
||||
"Config": {
|
||||
"Image": image_ref,
|
||||
"Env": [f"REPOPULSE_MAINTENANCE_TOKEN={token}"],
|
||||
"Labels": {
|
||||
"com.docker.compose.project": "repopulse",
|
||||
"org.opencontainers.image.revision": revision,
|
||||
},
|
||||
},
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": True,
|
||||
"Health": {"Status": "healthy"},
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "repopulse_data",
|
||||
"Destination": "/data",
|
||||
"Source": str(storage_effects.LEGACY_VOLUME_MOUNTPOINT),
|
||||
"RW": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def inspect_image(self, _image_id):
|
||||
return {
|
||||
"Id": image_id,
|
||||
"Config": {
|
||||
"Labels": {
|
||||
"org.opencontainers.image.revision": revision,
|
||||
"org.opencontainers.image.source": "jens/repopulse",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def run(self, command):
|
||||
self.commands.append(list(command))
|
||||
|
||||
core = Core()
|
||||
effects = storage_effects.FixedRepoPulseStorageEffects(
|
||||
store=Store(), deployment_core=core
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
effects,
|
||||
"_project_and_helper",
|
||||
lambda _intent, _revision: ({}, ModuleType("unused"), "e" * 64),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
storage_effects, "_private_environment_facts", lambda: (token, True, True)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
storage_effects,
|
||||
"_private_record_path",
|
||||
lambda _name, _intent: tmp_path / "unused-receipt.json",
|
||||
)
|
||||
monkeypatch.setattr(storage_effects, "_read_private_record", lambda *_args: None)
|
||||
monkeypatch.setattr(
|
||||
storage_effects,
|
||||
"_assert_legacy_volume_retained",
|
||||
lambda _core: storage_effects.LEGACY_VOLUME_MOUNTPOINT,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
storage_effects,
|
||||
"_maintenance_exchange",
|
||||
lambda *_args: {
|
||||
"before": {"status": 404, "body": None},
|
||||
"enter": None,
|
||||
"status": None,
|
||||
},
|
||||
)
|
||||
|
||||
with pytest.raises(storage_effects.StorageEffectsError) as error:
|
||||
effects.ensure_quiesced(
|
||||
intent_id=intent_id,
|
||||
intent_binding_sha256=binding,
|
||||
expected_main_revision=revision,
|
||||
expected_container_id_sha256=container_id_sha256,
|
||||
expected_container_image_sha256=image_sha256,
|
||||
)
|
||||
|
||||
assert error.value.code == "maintenance_endpoint_unavailable"
|
||||
assert core.commands == []
|
||||
Reference in new issue
Block a user