From f084a0f3a97ab884d89896ef99c6499884669e37 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Fri, 31 Jul 2026 22:48:07 +0200 Subject: [PATCH] Add managed storage operational probes --- src/govoplan_files/backend/manifest.py | 12 ++++ .../backend/operational_checks.py | 67 +++++++++++++++++++ tests/test_operational_checks.py | 42 ++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 src/govoplan_files/backend/operational_checks.py create mode 100644 tests/test_operational_checks.py diff --git a/src/govoplan_files/backend/manifest.py b/src/govoplan_files/backend/manifest.py index 48345c1..9185a37 100644 --- a/src/govoplan_files/backend/manifest.py +++ b/src/govoplan_files/backend/manifest.py @@ -23,6 +23,7 @@ from govoplan_core.core.modules import ( PermissionDefinition, RoleTemplate, ) +from govoplan_core.core.operations import OperationalCheckProviderRegistration from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_files.backend.change_tracking import register_files_change_tracking @@ -710,6 +711,17 @@ manifest = ModuleManifest( CAPABILITY_FILES_ACCESS: lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["access_capability"]).access_capability(context), "files.campaign_attachments": lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["campaign_capability"]).campaign_capability(context), }, + operational_check_providers=( + OperationalCheckProviderRegistration( + module_id="files", + check_id="files.managed_storage_roundtrip", + provider=lambda: __import__( + "govoplan_files.backend.operational_checks", + fromlist=["managed_storage_roundtrip_check"], + ).managed_storage_roundtrip_check(), + cache_seconds=60, + ), + ), ) diff --git a/src/govoplan_files/backend/operational_checks.py b/src/govoplan_files/backend/operational_checks.py new file mode 100644 index 0000000..20f7df4 --- /dev/null +++ b/src/govoplan_files/backend/operational_checks.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import hashlib +import secrets +from uuid import uuid4 + +from govoplan_core.core.operations import OperationalCheck +from govoplan_files.backend.storage.backends import ( + StorageBackendError, + get_storage_backend, +) + + +def managed_storage_roundtrip_check() -> OperationalCheck: + """Exercise the configured managed store without retaining probe data.""" + + backend = get_storage_backend() + key = f".govoplan-health/probes/{uuid4().hex}.bin" + payload = secrets.token_bytes(64) + expected_digest = hashlib.sha256(payload).hexdigest() + delete_error: Exception | None = None + try: + backend.put_bytes(key, payload, content_type="application/octet-stream") + stored = backend.get_bytes(key) + info = backend.stat(key) + if info.size_bytes != len(payload): + raise StorageBackendError("Managed storage returned an unexpected object size") + if hashlib.sha256(stored).hexdigest() != expected_digest: + raise StorageBackendError("Managed storage returned different bytes than were written") + except Exception as exc: # noqa: BLE001 - operational boundary reports provider failures. + return OperationalCheck( + id="files.managed_storage_roundtrip", + label="Managed file storage", + state="error", + detail=( + "The configured managed file store failed a bounded write/read/stat/delete " + f"probe ({type(exc).__name__})." + ), + readiness_critical=True, + metrics={"backend": backend.name, "probe_bytes": len(payload)}, + ) + finally: + try: + backend.delete(key) + except Exception as exc: # noqa: BLE001 - reported below when the data probe passed. + delete_error = exc + + if delete_error is not None: + return OperationalCheck( + id="files.managed_storage_roundtrip", + label="Managed file storage", + state="error", + detail=( + "Managed file bytes round-tripped, but probe cleanup failed " + f"({type(delete_error).__name__})." + ), + readiness_critical=True, + metrics={"backend": backend.name, "probe_bytes": len(payload)}, + ) + return OperationalCheck( + id="files.managed_storage_roundtrip", + label="Managed file storage", + state="ok", + detail="The configured managed file store passed write, read, stat, integrity, and delete checks.", + metrics={"backend": backend.name, "probe_bytes": len(payload)}, + ) + diff --git a/tests/test_operational_checks.py b/tests/test_operational_checks.py new file mode 100644 index 0000000..566ca17 --- /dev/null +++ b/tests/test_operational_checks.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from unittest.mock import patch + +from govoplan_files.backend.operational_checks import managed_storage_roundtrip_check +from govoplan_files.backend.storage.backends import LocalFilesystemStorageBackend + + +def test_managed_storage_roundtrip_removes_probe(tmp_path) -> None: + backend = LocalFilesystemStorageBackend(tmp_path) + + with patch( + "govoplan_files.backend.operational_checks.get_storage_backend", + return_value=backend, + ): + result = managed_storage_roundtrip_check() + + assert result.state == "ok" + assert result.metrics["backend"] == "local" + assert list(tmp_path.rglob("*.bin")) == [] + + +def test_managed_storage_roundtrip_fails_closed() -> None: + class BrokenBackend: + name = "broken" + + def put_bytes(self, *_args, **_kwargs): + raise OSError("private provider detail") + + def delete(self, *_args, **_kwargs): + return None + + with patch( + "govoplan_files.backend.operational_checks.get_storage_backend", + return_value=BrokenBackend(), + ): + result = managed_storage_roundtrip_check() + + assert result.state == "error" + assert result.readiness_critical is True + assert "private provider detail" not in result.detail +