feat(deploy): project infrastructure capabilities
Dependency Audit / dependency-audit (push) Successful in 1m44s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m43s

This commit is contained in:
2026-08-07 01:59:45 +02:00
parent dce725636d
commit 612a44bc8e
8 changed files with 731 additions and 4 deletions
+116 -2
View File
@@ -33,6 +33,10 @@ 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.capabilities import ( # noqa: E402
capability_change_impacts,
infrastructure_capability_document,
)
from govoplan_deploy.cluster_evidence import ( # noqa: E402
collect_kubernetes_evidence,
)
@@ -414,6 +418,27 @@ class DeploymentInstallerTests(unittest.TestCase):
self.assertNotIn("db-secret", rendered)
self.assertNotIn("redis-secret", rendered)
self.assertNotIn("object-secret", rendered)
capability_config = next(
item
for item in manifest["items"]
if item["kind"] == "ConfigMap"
and item["metadata"]["name"].endswith("infrastructure-capabilities")
)
capability_payload = json.loads(
capability_config["data"]["infrastructure-capabilities.json"]
)
self.assertEqual(1, capability_payload["schema_version"])
self.assertNotIn("db-secret", json.dumps(capability_payload))
api_container = deployments["govoplan-cluster-api"]["spec"]["template"]["spec"]["containers"][0]
self.assertIn(
{
"name": "deployment-capabilities",
"mountPath": "/etc/govoplan/deployment/infrastructure-capabilities.json",
"subPath": "infrastructure-capabilities.json",
"readOnly": True,
},
api_container["volumeMounts"],
)
self.assertNotIn("PersistentVolumeClaim", kinds)
self.assertNotIn("StatefulSet", kinds)
self.assertEqual(3, deployments["govoplan-cluster-api"]["spec"]["replicas"])
@@ -723,6 +748,10 @@ class DeploymentInstallerTests(unittest.TestCase):
compose["services"]["load-balancer"]["ports"],
)
self.assertNotIn("ports", compose["services"]["web"])
self.assertIn(
"./infrastructure-capabilities.json:/etc/govoplan/deployment/infrastructure-capabilities.json:ro",
compose["services"]["api"]["volumes"],
)
self.assertEqual(1, compose["services"]["api"]["scale"])
self.assertEqual(1, compose["services"]["web"]["scale"])
@@ -948,6 +977,63 @@ class DeploymentInstallerTests(unittest.TestCase):
reconciled["GARAGE_RPC_SECRET"],
)
def test_infrastructure_capability_document_exposes_refs_not_secrets(self) -> None:
spec = default_spec(
installation_id="govoplan-shared",
postgres_mode="external",
redis_mode="external",
storage_mode="s3",
mail_mode="external-relay",
module_set="full",
)
values = initial_secrets(
spec,
supplied={
"DATABASE_URL": "postgresql+psycopg://user:database-secret@db.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",
},
)
document = infrastructure_capability_document(spec, values)
rendered = json.dumps(document, sort_keys=True)
capabilities = {item["id"]: item for item in document["capabilities"]}
self.assertNotIn("database-secret", rendered)
self.assertNotIn("redis-secret", rendered)
self.assertNotIn("object-secret", rendered)
self.assertNotIn("object-key", rendered)
self.assertEqual("externally_supplied", capabilities["database.postgresql"]["state"])
self.assertEqual("db.example.test", capabilities["database.postgresql"]["endpoint"]["host"])
self.assertEqual(["env:DATABASE_URL"], capabilities["database.postgresql"]["secret_refs"])
self.assertEqual("available_unconfigured", capabilities["mail.smtp"]["state"])
self.assertEqual("mail.smtp-profile", document["post_install_tasks"][0]["id"])
def test_capability_impact_detects_external_endpoint_rebinding(self) -> None:
spec = default_spec(postgres_mode="external", module_set="full")
previous = infrastructure_capability_document(
spec,
{"DATABASE_URL": "postgresql://user:old-secret@old-db.example.test/govoplan"},
)
desired = infrastructure_capability_document(
spec,
{"DATABASE_URL": "postgresql://user:new-secret@new-db.example.test/govoplan"},
)
impacts = {
item.capability_id: item
for item in capability_change_impacts(previous, desired)
}
self.assertEqual("reconfigure", impacts["database.postgresql"].action)
self.assertIn("changed endpoint binding", impacts["database.postgresql"].detail)
self.assertNotIn("old-secret", impacts["database.postgresql"].detail)
self.assertNotIn("new-secret", impacts["database.postgresql"].detail)
def test_replica_counts_drive_compose_and_load_balancer_discovery(self) -> None:
spec = default_spec(
storage_mode="garage",
@@ -1077,10 +1163,14 @@ class DeploymentInstallerTests(unittest.TestCase):
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
paths = bundle_paths(Path(directory))
paths.root.chmod(0o700)
first_spec = default_spec(mail_mode="test-mail")
first_spec = default_spec(mail_mode="test-mail", module_set="full")
first_environment = initial_secrets(first_spec)
write_env(paths.env, first_environment)
first_plan = build_plan(first_spec, paths, include_host_checks=False)
first_capabilities = infrastructure_capability_document(
first_spec,
first_environment,
)
atomic_write(
paths.receipt,
canonical_json(
@@ -1091,12 +1181,17 @@ class DeploymentInstallerTests(unittest.TestCase):
first_plan.desired_environment_fingerprint
),
"services": list(render_compose(first_spec)["services"]),
"infrastructure_capabilities": first_capabilities,
}
),
mode=0o600,
)
second_spec = default_spec(redis_mode="disabled", mail_mode="disabled")
second_spec = default_spec(
redis_mode="disabled",
mail_mode="disabled",
module_set="full",
)
write_env(
paths.env,
reconcile_runtime_environment(second_spec, first_environment),
@@ -1112,6 +1207,20 @@ class DeploymentInstallerTests(unittest.TestCase):
{"redis", "worker", "scheduler", "test-mail"},
removed,
)
impacts = {
item.capability_id: item
for item in second_plan.capability_impacts
}
self.assertEqual("remove", impacts["coordination.redis"].action)
self.assertEqual("remove", impacts["mail.smtp"].action)
self.assertIn("mail", impacts["mail.smtp"].dependent_modules)
self.assertTrue(
any(
check.id == "capability.change.mail.smtp"
and check.level == "warning"
for check in second_plan.checks
)
)
def test_secret_change_is_planned_without_exposing_secret_values(self) -> None:
with tempfile.TemporaryDirectory(prefix="govoplan-deploy-test-") as directory:
@@ -1480,6 +1589,11 @@ class DeploymentInstallerTests(unittest.TestCase):
receipt["listen"],
)
self.assertNotIn("installer", receipt["services"])
self.assertEqual(
1,
receipt["infrastructure_capabilities"]["schema_version"],
)
self.assertTrue((root / "infrastructure-capabilities.json").is_file())
def test_installation_root_symlink_is_rejected(self) -> None:
if not hasattr(Path, "symlink_to"):