diff --git a/pyproject.toml b/pyproject.toml index 97d4260..2a1b07f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "azure-identity>=1.17,<2", "azure-keyvault-secrets>=4.9,<5", "azure-mgmt-resource>=23.1,<26", + "azure-mgmt-resource-deployments>=1.0.0b1,<2", "azure-mgmt-resource-subscriptions>=1.0.0b1,<2", "azure-mgmt-authorization>=4.0,<5", "azure-mgmt-keyvault>=10.3,<11", diff --git a/src/azurefox/clients/factory.py b/src/azurefox/clients/factory.py index b6ba680..b5fe887 100644 --- a/src/azurefox/clients/factory.py +++ b/src/azurefox/clients/factory.py @@ -12,6 +12,7 @@ class AzureClients: subscription_id: str subscription: SubscriptionRef resource: object + resource_deployments: object authorization: object automation: object web: object @@ -40,6 +41,7 @@ def build_clients(session: AuthSession, requested_subscription: str | None) -> A from azure.mgmt.network import NetworkManagementClient from azure.mgmt.postgresqlflexibleservers import PostgreSQLManagementClient from azure.mgmt.resource import ResourceManagementClient + from azure.mgmt.resource.deployments import DeploymentsMgmtClient from azure.mgmt.resource.subscriptions import SubscriptionClient from azure.mgmt.sql import SqlManagementClient from azure.mgmt.storage import StorageManagementClient @@ -89,6 +91,7 @@ def build_clients(session: AuthSession, requested_subscription: str | None) -> A subscription_id=subscription_id, subscription=subscription_ref, resource=ResourceManagementClient(session.credential, subscription_id), + resource_deployments=DeploymentsMgmtClient(session.credential, subscription_id), authorization=AuthorizationManagementClient(session.credential, subscription_id), automation=AutomationClient(session.credential, subscription_id), web=WebSiteManagementClient(session.credential, subscription_id), diff --git a/src/azurefox/collectors/provider.py b/src/azurefox/collectors/provider.py index cdad287..a55310b 100644 --- a/src/azurefox/collectors/provider.py +++ b/src/azurefox/collectors/provider.py @@ -748,7 +748,8 @@ def arm_deployments(self) -> dict: deployments: list[dict] = [] try: - for deployment in self.clients.resource.deployments.list_at_subscription_scope(): + iterator = self.clients.resource_deployments.deployments.list_at_subscription_scope() + for deployment in iterator: deployments.append( _deployment_summary( deployment, @@ -767,7 +768,9 @@ def arm_deployments(self) -> dict: for resource_group in resource_groups: try: - iterator = self.clients.resource.deployments.list_by_resource_group(resource_group) + iterator = self.clients.resource_deployments.deployments.list_by_resource_group( + resource_group + ) for deployment in iterator: deployments.append( _deployment_summary( diff --git a/tests/test_clients_factory.py b/tests/test_clients_factory.py index 423c7d7..b4c0e59 100644 --- a/tests/test_clients_factory.py +++ b/tests/test_clients_factory.py @@ -56,6 +56,7 @@ def __init__(self, credential: object) -> None: "azure.mgmt.mysqlflexibleservers": ("MySQLManagementClient",), "azure.mgmt.network": ("NetworkManagementClient",), "azure.mgmt.postgresqlflexibleservers": ("PostgreSQLManagementClient",), + "azure.mgmt.resource.deployments": ("DeploymentsMgmtClient",), "azure.mgmt.sql": ("SqlManagementClient",), "azure.mgmt.storage": ("StorageManagementClient",), "azure.mgmt.web": ("WebSiteManagementClient",), @@ -96,3 +97,4 @@ def test_build_clients_supports_split_subscription_package(monkeypatch) -> None: assert clients.subscription_id == "sub-b" assert clients.subscription.display_name == "Beta" assert clients.resource.subscription_id == "sub-b" + assert clients.resource_deployments.subscription_id == "sub-b" diff --git a/tests/test_collectors.py b/tests/test_collectors.py index ce10e41..e4df6e7 100644 --- a/tests/test_collectors.py +++ b/tests/test_collectors.py @@ -2306,6 +2306,87 @@ def test_collect_arm_deployments(fixture_provider, options) -> None: assert output.deployments[2].name == "kv-secrets" +def test_collect_arm_deployments_uses_split_deployments_client(options) -> None: + def deployment( + name: str, + deployment_id: str, + *, + providers: list[str], + outputs_count: int = 0, + output_resource_count: int = 0, + ) -> SimpleNamespace: + return SimpleNamespace( + id=deployment_id, + name=name, + properties=SimpleNamespace( + provisioning_state="Succeeded", + outputs={f"output-{index}": {} for index in range(outputs_count)}, + output_resources=[object() for _ in range(output_resource_count)], + providers=[SimpleNamespace(namespace=namespace) for namespace in providers], + template_link=None, + parameters_link=None, + mode="Incremental", + timestamp="2026-04-18T00:00:00Z", + duration="PT1M", + ), + ) + + provider = AzureProvider.__new__(AzureProvider) + provider.session = SimpleNamespace( + tenant_id="tenant-id", + token_source="fixture", + auth_mode="fixture", + ) + provider.clients = SimpleNamespace( + subscription_id="subscription-id", + resource=SimpleNamespace( + resource_groups=SimpleNamespace( + list=lambda: [SimpleNamespace(name="rg-apps")], + ) + ), + resource_deployments=SimpleNamespace( + deployments=SimpleNamespace( + list_at_subscription_scope=lambda: [ + deployment( + "sub-foundation", + ( + "/subscriptions/subscription-id/providers/Microsoft.Resources/" + "deployments/sub-foundation" + ), + providers=["Microsoft.Resources"], + outputs_count=2, + ) + ], + list_by_resource_group=lambda resource_group: [ + deployment( + "rg-apps-deploy", + ( + "/subscriptions/subscription-id/resourceGroups/rg-apps/providers/" + "Microsoft.Resources/deployments/rg-apps-deploy" + ), + providers=["Microsoft.Web"], + output_resource_count=1, + ) + ] + if resource_group == "rg-apps" + else [], + ) + ), + ) + + output = collect_arm_deployments(provider, options) + + assert [item.name for item in output.deployments] == [ + "sub-foundation", + "rg-apps-deploy", + ] + assert [item.scope_type for item in output.deployments] == [ + "subscription", + "resource_group", + ] + assert output.issues == [] + + def test_collect_arm_deployments_sorts_failures_and_linked_rows_first(options) -> None: output = collect_arm_deployments(DriftOrderingFixtureProvider(Path(".")), options)