Release v0.1.16
Dependency Audit / dependency-audit (push) Failing after 1m47s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m18s
Developer Meta-package Release / publish-package (push) Successful in 11s

This commit is contained in:
2026-08-05 19:52:32 +02:00
parent 61463a24cb
commit 3ca068f76a
42 changed files with 5433 additions and 189 deletions
+178
View File
@@ -158,6 +158,81 @@ class DeploymentInstallerTests(unittest.TestCase):
evidence["snapshot"]["ready_node_names"],
)
def test_kubernetes_api_loss_uses_a_non_json_mutation_command(self) -> None:
initial_pods = [
_kubernetes_test_pod("api-a", "api", "node-a"),
_kubernetes_test_pod("api-b", "api", "node-b"),
_kubernetes_test_pod("web-a", "web", "node-a"),
_kubernetes_test_pod("web-b", "web", "node-b"),
]
replacement_pods = [
_kubernetes_test_pod("api-b", "api", "node-b"),
_kubernetes_test_pod("api-c", "api", "node-a"),
]
deleted = False
actions: list[tuple[str, ...]] = []
def run(arguments):
if "nodes" in arguments:
return {
"items": [
{
"metadata": {"name": name},
"spec": {},
"status": {
"conditions": [
{"type": "Ready", "status": "True"}
]
},
}
for name in ("node-a", "node-b")
]
}
if "deployments" in arguments:
return {
"items": [
_kubernetes_test_deployment("api", 2),
_kubernetes_test_deployment("web", 2),
]
}
return {"items": replacement_pods if deleted else initial_pods}
def act(arguments):
nonlocal deleted
actions.append(tuple(arguments))
deleted = True
evidence = collect_kubernetes_evidence(
installation_id="govoplan-cluster",
namespace="govoplan",
ops_url="https://govoplan.example.test/api/v1/ops/status",
api_key="not-retained",
exercise_api_pod_loss=True,
command_runner=run,
action_runner=act,
json_fetcher=lambda _url, _key: {
"readiness": {"ready": True},
"runtime_cluster": {
"composition": {"skewed": False},
"software_versions": {"skewed": False},
"queues": {"missing": []},
},
"checks": [
{
"id": "database_capacity",
"state": "ok",
"detail": "Within budget",
}
],
},
)
self.assertEqual("passed", evidence["api_pod_loss"]["state"])
self.assertEqual(1, len(actions))
self.assertIn("delete", actions[0])
self.assertNotIn("-o", actions[0])
self.assertNotIn("not-retained", json.dumps(evidence))
def test_pre_migration_failure_restores_checksum_verified_applied_bundle(
self,
) -> None:
@@ -347,6 +422,10 @@ class DeploymentInstallerTests(unittest.TestCase):
"template"
]["spec"]["containers"][0]["command"]
self.assertIn("govoplan_core.commands.fenced_run", scheduler_command)
self.assertEqual(
["--schedule", "/tmp/celerybeat-schedule"],
scheduler_command[-2:],
)
api_init_command = deployments["govoplan-cluster-api"]["spec"]["template"][
"spec"
]["initContainers"][0]["command"]
@@ -385,6 +464,12 @@ class DeploymentInstallerTests(unittest.TestCase):
"spec"
]["containers"][0]["command"]
self.assertIn("--concurrency", worker_command)
for deployment in deployments.values():
spread = deployment["spec"]["template"]["spec"][
"topologySpreadConstraints"
][0]
self.assertEqual("DoNotSchedule", spread["whenUnsatisfiable"])
self.assertEqual(["pod-template-hash"], spread["matchLabelKeys"])
self.assertEqual(
"62",
manifest["metadata"]["annotations"][
@@ -392,6 +477,99 @@ class DeploymentInstallerTests(unittest.TestCase):
],
)
def test_kubernetes_export_mounts_an_optional_s3_ca_on_backend_roles(
self,
) -> None:
spec = default_spec(
installation_id="govoplan-cluster",
postgres_mode="external",
redis_mode="external",
storage_mode="s3",
api_replicas=2,
web_replicas=2,
worker_replicas=2,
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: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",
"GOVOPLAN_DB_CONNECTION_LIMIT": "100",
},
)
manifest = render_kubernetes(
spec,
environment,
s3_ca_secret_name="govoplan-s3-ca",
backup_required=False,
)
backend_pods = [
item["spec"]["template"]["spec"]
for item in manifest["items"]
if item["kind"] in {"Deployment", "Job"}
and item["metadata"]["labels"].get("app.kubernetes.io/component")
in {"api", "worker", "scheduler", "migration"}
]
web = next(
item
for item in manifest["items"]
if item["kind"] == "Deployment"
and item["metadata"]["labels"].get("app.kubernetes.io/component")
== "web"
)
self.assertTrue(backend_pods)
for pod in backend_pods:
self.assertIn(
{
"name": "s3-ca",
"secret": {
"secretName": "govoplan-s3-ca",
"items": [{"key": "ca.crt", "path": "s3-ca.crt"}],
},
},
pod["volumes"],
)
for container in [*pod.get("initContainers", []), *pod["containers"]]:
self.assertIn(
{
"name": "AWS_CA_BUNDLE",
"value": "/etc/govoplan/trust/s3-ca.crt",
},
container["env"],
)
self.assertIn(
{
"name": "s3-ca",
"mountPath": "/etc/govoplan/trust",
"readOnly": True,
},
container["volumeMounts"],
)
self.assertNotIn(
"s3-ca",
{
volume["name"]
for volume in web["spec"]["template"]["spec"]["volumes"]
},
)
with self.assertRaisesRegex(ValueError, "S3 CA secret"):
render_kubernetes(
spec,
environment,
s3_ca_secret_name="INVALID_NAME",
backup_required=False,
)
def test_kubernetes_export_splits_worker_queues_and_rejects_capacity_overrun(
self,
) -> None: