feat(deploy): project infrastructure capabilities
This commit is contained in:
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user