diff --git a/app/runtime/server/setup/prerequisites.py b/app/runtime/server/setup/prerequisites.py index 5b2a1fd..f08d73a 100644 --- a/app/runtime/server/setup/prerequisites.py +++ b/app/runtime/server/setup/prerequisites.py @@ -89,6 +89,9 @@ async def deploy(self, req: web.Request) -> web.Response: "message": "Key Vault already configured", }) + if not await self._register_keyvault_provider(steps): + return _fail(steps) + if not await self._ensure_rg(prereq_rg, location, steps): return _fail(steps) @@ -133,6 +136,9 @@ async def ensure_keyvault_ready( prereq_rg = _DEFAULT_PREREQ_RG + if not await self._register_keyvault_provider(steps): + return steps + if not await self._ensure_rg(prereq_rg, location, steps): return steps @@ -144,6 +150,19 @@ async def ensure_keyvault_ready( await self._wait_for_access(steps) return steps + async def _register_keyvault_provider(self, steps: list[dict]) -> bool: + """Register the Microsoft.KeyVault resource provider on the active subscription.""" + ok, msg = await run_sync( + self._az.ok, + "provider", "register", "--namespace", "Microsoft.KeyVault", "--wait", + ) + steps.append({ + "step": "provider_registration", + "status": "ok" if ok else "failed", + "detail": "Microsoft.KeyVault registered" if ok else (msg or "Unknown error"), + }) + return ok + def _link_existing_keyvault(self) -> None: """Ensure the existing Key Vault resource is registered on the current deployment.""" if not self._deploy_store: diff --git a/app/runtime/tests/test_prerequisites.py b/app/runtime/tests/test_prerequisites.py index a80f3c7..e078fa6 100644 --- a/app/runtime/tests/test_prerequisites.py +++ b/app/runtime/tests/test_prerequisites.py @@ -1,4 +1,4 @@ -"""Tests for PrerequisitesRoutes._link_existing_keyvault.""" +"""Tests for PrerequisitesRoutes helpers.""" from __future__ import annotations @@ -7,15 +7,17 @@ from app.runtime.config.settings import cfg from app.runtime.server.setup.prerequisites import PrerequisitesRoutes -from app.runtime.state.deploy_state import DeployStateStore, DeploymentRecord +from app.runtime.state.deploy_state import DeploymentRecord, DeployStateStore from app.runtime.state.infra_config import InfraConfigStore +from app.runtime.util.result import Result def _make_routes( tmp_path: Path, deploy_store: DeployStateStore | None = None, + az: MagicMock | None = None, ) -> PrerequisitesRoutes: - az = MagicMock() + az = az or MagicMock() store = InfraConfigStore(path=tmp_path / "infra.json") return PrerequisitesRoutes(az, store, deploy_store=deploy_store) @@ -75,3 +77,36 @@ def test_noop_without_kv_name(self, tmp_path: Path) -> None: updated = ds.get("cccc3333") assert len(updated.resources) == 0 + + +class TestRegisterKeyvaultProvider: + async def test_success_appends_ok_step(self, tmp_path: Path) -> None: + az = MagicMock() + az.ok.return_value = Result.ok("Registered") + routes = _make_routes(tmp_path, az=az) + + steps: list[dict] = [] + result = await routes._register_keyvault_provider(steps) + + assert result is True + assert len(steps) == 1 + assert steps[0]["step"] == "provider_registration" + assert steps[0]["status"] == "ok" + assert "Microsoft.KeyVault" in steps[0]["detail"] + az.ok.assert_called_once_with( + "provider", "register", "--namespace", "Microsoft.KeyVault", "--wait", + ) + + async def test_failure_appends_failed_step(self, tmp_path: Path) -> None: + az = MagicMock() + az.ok.return_value = Result.fail("Subscription not found") + routes = _make_routes(tmp_path, az=az) + + steps: list[dict] = [] + result = await routes._register_keyvault_provider(steps) + + assert result is False + assert len(steps) == 1 + assert steps[0]["step"] == "provider_registration" + assert steps[0]["status"] == "failed" + assert "Subscription not found" in steps[0]["detail"]