feat: implement institutional governance and recovery architecture
Dependency Audit / dependency-audit (push) Failing after 10s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Failing after 9s

This commit is contained in:
2026-08-01 17:46:53 +02:00
parent 3c658fa32d
commit d78b13f9d3
45 changed files with 4388 additions and 262 deletions
+273 -8
View File
@@ -31,6 +31,7 @@ from govoplan_deploy.bundle import ( # noqa: E402
)
from govoplan_deploy.cli import _receipt_uses_direct_web_port, main # noqa: E402
import govoplan_deploy.cli as deployment_cli # noqa: E402
from govoplan_deploy.kubernetes import render_kubernetes # noqa: E402
from govoplan_deploy.model import ( # noqa: E402
SpecError,
default_spec,
@@ -38,6 +39,12 @@ from govoplan_deploy.model import ( # noqa: E402
)
from govoplan_deploy.planning import _endpoint_check, build_plan # noqa: E402
import govoplan_deploy.planning as deployment_planning # noqa: E402
from govoplan_deploy.recovery import ( # noqa: E402
DeploymentOperationJournal,
load_operation,
recover_operation,
write_applied_state,
)
def run_cli(arguments: list[str]) -> tuple[int, str, str]:
@@ -49,6 +56,242 @@ def run_cli(arguments: list[str]) -> tuple[int, str, str]:
class DeploymentInstallerTests(unittest.TestCase):
def test_pre_migration_failure_restores_checksum_verified_applied_bundle(
self,
) -> None:
with tempfile.TemporaryDirectory(
prefix="govoplan-deploy-recovery-"
) as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
spec = default_spec()
previous_environment = initial_secrets(spec)
atomic_write(paths.spec, canonical_json(spec.to_dict()), mode=0o600)
write_env(paths.env, previous_environment)
atomic_write(
paths.compose, canonical_json(render_compose(spec)), mode=0o600
)
atomic_write(
paths.receipt, canonical_json({"spec_sha256": "old"}), mode=0o600
)
write_applied_state(paths)
atomic_write(paths.spec, canonical_json({"changed": True}), mode=0o600)
journal = DeploymentOperationJournal.begin(
paths,
plan={"installation_id": spec.installation_id},
)
journal.failed(RuntimeError("pull failed"))
result = recover_operation(paths, operation_id=journal.operation_id)
self.assertEqual("configuration_restored", result.action)
self.assertEqual(spec.to_dict(), json.loads(paths.spec.read_text()))
self.assertEqual(previous_environment, read_env(paths.env))
def test_post_migration_failure_requires_forward_recovery(self) -> None:
with tempfile.TemporaryDirectory(
prefix="govoplan-deploy-recovery-"
) as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
spec = default_spec()
atomic_write(paths.spec, canonical_json(spec.to_dict()), mode=0o600)
write_env(paths.env, initial_secrets(spec))
atomic_write(
paths.compose, canonical_json(render_compose(spec)), mode=0o600
)
write_applied_state(paths)
journal = DeploymentOperationJournal.begin(
paths,
plan={"installation_id": spec.installation_id},
)
journal.migration_started()
journal.failed(RuntimeError("health failed"))
result = recover_operation(paths, operation_id=journal.operation_id)
self.assertEqual("forward_recovery", result.action)
self.assertIn("not restored", result.detail)
def test_recovery_refuses_tampered_migration_boundary(self) -> None:
with tempfile.TemporaryDirectory(
prefix="govoplan-deploy-recovery-"
) as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
spec = default_spec()
journal = DeploymentOperationJournal.begin(
paths,
plan={"installation_id": spec.installation_id},
)
journal.migration_started()
payload = json.loads(journal.path.joinpath("operation.json").read_text())
payload["migration_started"] = False
atomic_write(
journal.path / "operation.json",
canonical_json(payload),
mode=0o600,
)
with self.assertRaisesRegex(ValueError, "migration boundary"):
load_operation(paths, operation_id=journal.operation_id)
def test_recovery_verifies_complete_snapshot_before_mutating_bundle(self) -> None:
with tempfile.TemporaryDirectory(
prefix="govoplan-deploy-recovery-"
) as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
spec = default_spec()
atomic_write(paths.spec, canonical_json(spec.to_dict()), mode=0o600)
write_env(paths.env, initial_secrets(spec))
atomic_write(
paths.compose, canonical_json(render_compose(spec)), mode=0o600
)
write_applied_state(paths)
journal = DeploymentOperationJournal.begin(
paths,
plan={"installation_id": spec.installation_id},
)
changed_spec = canonical_json({"changed": True})
atomic_write(paths.spec, changed_spec, mode=0o600)
atomic_write(
journal.path / "before" / "compose.json", b"tampered", mode=0o600
)
with self.assertRaisesRegex(ValueError, "checksum failed"):
recover_operation(paths, operation_id=journal.operation_id)
self.assertEqual(changed_spec, paths.spec.read_bytes())
def test_recovery_refuses_tampered_desired_plan(self) -> None:
with tempfile.TemporaryDirectory(
prefix="govoplan-deploy-recovery-"
) as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
spec = default_spec()
journal = DeploymentOperationJournal.begin(
paths,
plan={"installation_id": spec.installation_id},
)
payload = json.loads(journal.path.joinpath("operation.json").read_text())
payload["desired"]["installation_id"] = "other-installation"
atomic_write(
journal.path / "operation.json",
canonical_json(payload),
mode=0o600,
)
with self.assertRaisesRegex(ValueError, "plan"):
load_operation(paths, operation_id=journal.operation_id)
def test_kubernetes_export_is_stateless_and_uses_exact_secret_contract(
self,
) -> None:
spec = default_spec(
installation_id="govoplan-cluster",
postgres_mode="external",
redis_mode="external",
storage_mode="s3",
api_replicas=3,
web_replicas=2,
worker_replicas=4,
api_image="registry.example.test/govoplan-api@sha256:" + "a" * 64,
web_image="registry.example.test/govoplan-web@sha256:" + "b" * 64,
)
environment = initial_secrets(
spec,
supplied={
"DATABASE_URL": "postgresql+psycopg://user:db-secret@postgres.example.test/govoplan",
"GOVOPLAN_DATABASE_URL_PGTOOLS": "postgresql://user:db-secret@postgres.example.test/govoplan",
"REDIS_URL": "rediss://:redis-secret@redis.example.test/0",
"FILE_STORAGE_S3_ENDPOINT_URL": "https://s3.example.test",
"FILE_STORAGE_S3_REGION": "eu-test-1",
"FILE_STORAGE_S3_ACCESS_KEY_ID": "object-key",
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "object-secret",
"FILE_STORAGE_S3_BUCKET": "govoplan",
},
)
manifest = render_kubernetes(spec, environment)
rendered = json.dumps(manifest, sort_keys=True)
kinds = [item["kind"] for item in manifest["items"]]
deployments = {
item["metadata"]["name"]: item
for item in manifest["items"]
if item["kind"] == "Deployment"
}
self.assertNotIn("db-secret", rendered)
self.assertNotIn("redis-secret", rendered)
self.assertNotIn("object-secret", rendered)
self.assertNotIn("PersistentVolumeClaim", kinds)
self.assertNotIn("StatefulSet", kinds)
self.assertEqual(3, deployments["govoplan-cluster-api"]["spec"]["replicas"])
self.assertEqual(4, deployments["govoplan-cluster-worker"]["spec"]["replicas"])
scheduler_command = deployments["govoplan-cluster-scheduler"]["spec"][
"template"
]["spec"]["containers"][0]["command"]
self.assertIn("govoplan_core.commands.fenced_run", scheduler_command)
api_init_command = deployments["govoplan-cluster-api"]["spec"]["template"][
"spec"
]["initContainers"][0]["command"]
self.assertIn("govoplan_core.commands.wait_for_database", api_init_command)
migration = next(item for item in manifest["items"] if item["kind"] == "Job")
self.assertRegex(
migration["metadata"]["name"],
r"^govoplan-cluster-migrate-[0-9a-f]{8}$",
)
self.assertEqual(
"forward-recovery",
migration["metadata"]["annotations"]["govoplan.add-ideas.de/recovery-mode"],
)
config = next(item for item in manifest["items"] if item["kind"] == "ConfigMap")
self.assertEqual("shared", config["data"]["GOVOPLAN_STATE_PROFILE"])
self.assertEqual("3", config["data"]["GOVOPLAN_EXPECTED_API_REPLICAS"])
self.assertEqual(
[{"name": "Host", "value": "127.0.0.1"}],
deployments["govoplan-cluster-api"]["spec"]["template"]["spec"][
"containers"
][0]["readinessProbe"]["httpGet"]["httpHeaders"],
)
def test_kubernetes_export_rejects_mutable_release_images(self) -> None:
spec = default_spec(
installation_id="govoplan-cluster",
postgres_mode="external",
redis_mode="external",
storage_mode="s3",
api_image="registry.example.test/govoplan-api:latest",
web_image="registry.example.test/govoplan-web:latest",
)
environment = initial_secrets(
spec,
supplied={
"DATABASE_URL": "postgresql+psycopg://user:secret@postgres.example.test/govoplan",
"GOVOPLAN_DATABASE_URL_PGTOOLS": "postgresql://user:secret@postgres.example.test/govoplan",
"REDIS_URL": "rediss://:secret@redis.example.test/0",
"FILE_STORAGE_S3_ENDPOINT_URL": "https://s3.example.test",
"FILE_STORAGE_S3_REGION": "eu-test-1",
"FILE_STORAGE_S3_ACCESS_KEY_ID": "object-key",
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "object-secret",
"FILE_STORAGE_S3_BUCKET": "govoplan",
},
)
with self.assertRaisesRegex(ValueError, "digest-pinned"):
render_kubernetes(spec, environment)
def test_kubernetes_export_rejects_installer_managed_state_services(self) -> None:
spec = default_spec(
installation_id="govoplan-cluster",
storage_mode="garage",
)
with self.assertRaisesRegex(ValueError, "external shared PostgreSQL"):
render_kubernetes(spec, initial_secrets(spec))
def test_default_bundle_has_base_modules_and_managed_dependencies(self) -> None:
spec = default_spec()
compose = render_compose(spec)
@@ -80,6 +323,7 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertNotIn("scheduler", compose["services"])
self.assertEqual("false", values["CELERY_ENABLED"])
self.assertEqual("true", values["GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE"])
self.assertEqual("local", values["GOVOPLAN_STATE_PROFILE"])
def test_self_hosted_rejects_insecure_or_test_only_choices(self) -> None:
with self.assertRaisesRegex(SpecError, "must use HTTPS"):
@@ -154,7 +398,7 @@ class DeploymentInstallerTests(unittest.TestCase):
)
self.assertEqual("rediss://:secret@redis.example.test/0", values["REDIS_URL"])
def test_s3_requires_complete_private_configuration_and_https_in_production(
def test_s3_requires_complete_private_configuration_and_https_endpoint(
self,
) -> None:
evaluation = default_spec(storage_mode="s3")
@@ -168,17 +412,21 @@ class DeploymentInstallerTests(unittest.TestCase):
"FILE_STORAGE_S3_SECRET_ACCESS_KEY": "secret",
"FILE_STORAGE_S3_BUCKET": "govoplan",
}
self.assertEqual(
"s3",
initial_secrets(evaluation, supplied=supplied)["FILE_STORAGE_BACKEND"],
)
with self.assertRaisesRegex(ValueError, "must use HTTPS"):
initial_secrets(evaluation, supplied=supplied)
supplied["FILE_STORAGE_S3_ENDPOINT_URL"] = "https://s3.example.test"
values = initial_secrets(evaluation, supplied=supplied)
self.assertEqual("s3", values["FILE_STORAGE_BACKEND"])
self.assertEqual("true", values["FILE_STORAGE_S3_ENDPOINT_TRUSTED"])
production = default_spec(
profile="self-hosted",
public_url="https://govoplan.example.test",
storage_mode="s3",
)
with self.assertRaisesRegex(ValueError, "must use HTTPS"):
initial_secrets(production, supplied=supplied)
self.assertEqual(
"s3",
initial_secrets(production, supplied=supplied)["FILE_STORAGE_BACKEND"],
)
def test_managed_garage_bootstraps_private_s3_storage(self) -> None:
spec = default_spec(storage_mode="garage")
@@ -187,6 +435,7 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertEqual("s3", values["FILE_STORAGE_BACKEND"])
self.assertEqual("true", values["FILE_STORAGE_S3_DEPLOYMENT_MANAGED"])
self.assertEqual("false", values["FILE_STORAGE_S3_ENDPOINT_TRUSTED"])
self.assertEqual(
"http://garage:3900",
values["FILE_STORAGE_S3_ENDPOINT_URL"],
@@ -244,6 +493,16 @@ class DeploymentInstallerTests(unittest.TestCase):
)
self.assertIn("server-template web- 2 web:8080", config)
self.assertIn("server-template api- 3 api:8000", config)
self.assertIn("option httpchk GET /health/ready", config)
self.assertIn("http-check send hdr Host 127.0.0.1", config)
self.assertIn(
"headers={'Host': '127.0.0.1'}",
compose["services"]["api"]["healthcheck"]["test"][-1],
)
self.assertEqual(
"host-shared",
initial_secrets(spec)["GOVOPLAN_STATE_PROFILE"],
)
def test_multi_replica_runtime_requires_shared_redis(self) -> None:
with self.assertRaisesRegex(SpecError, "multiple API replicas"):
@@ -725,11 +984,17 @@ class DeploymentInstallerTests(unittest.TestCase):
for index, command in enumerate(commands)
if "--remove-orphans" in command
)
stop_index = next(
index
for index, command in enumerate(commands)
if "stop" in command and "--timeout" in command
)
self.assertLess(stop_index, migrate_index)
self.assertLess(migrate_index, runtime_index)
self.assertIn("postgres", commands[0])
self.assertIn("redis", commands[0])
wait_for_health.assert_called_once_with(
"http://127.0.0.1:8080/health",
"http://127.0.0.1:8080/health/ready",
timeout_seconds=120.0,
)
receipt = json.loads((root / "receipt.json").read_text(encoding="utf-8"))