fix: list every service's resources in the ARM resource index, at both scopes - #270
Conversation
GET subscriptions/{sub}/resourceGroups/{rg}/resources returned only the
subsystems ArmHandler knows inline (Storage, Key Vault, Web) plus Network,
API Management, Managed Identity, and whatever implements the CDI
ResourceIndexContributor interface — where AciHandler was the sole
implementation. Every other service with a management plane was absent from
its own resource group: a VM answered a direct GET with 200 and appeared
under /providers/Microsoft.Compute/virtualMachines, yet the group listing
came back without it.
That listing is what the azurerm provider reads before deleting a resource
group to verify it is empty (the reason ResourceIndexContributor exists), and
what any caller enumerating a group generically — az resource list -g, the
Resource Management SDKs, drift-checking tools — depends on.
Registers the eight missing providers through the existing extension point:
Compute/virtualMachines, ContainerService/managedClusters,
ContainerRegistry/registries, Cache/Redis, DBforPostgreSQL/flexibleServers,
DBforMySQL/flexibleServers, DBforMariaDB/servers and Sql/servers. Each
contributes from the state its own list endpoint already reads, so the index
cannot drift from the type-scoped listing.
ArmResources.indexEntry centralises the entry shape Azure returns for a
generic resource — id, name, type, location, tags, and no properties, which
arrive only under $expand. AciModels now builds its entry through it, so the
one pre-existing contributor and the eight new ones share a single
definition.
|
| Filename | Overview |
|---|---|
| src/main/java/io/floci/az/services/arm/ArmHandler.java | Centralizes subscription and resource-group resource aggregation through one scope-aware assembly path. |
| src/main/java/io/floci/az/core/arm/ResourceIndexContributor.java | Extends the contributor contract with an explicit subscription-scoped listing method implemented by every in-repository contributor. |
| src/main/java/io/floci/az/core/arm/ArmResources.java | Adds a shared constructor for minimal generic ARM index entries. |
| src/main/java/io/floci/az/services/vm/VmHandler.java | Registers VMs in both generic resource indexes using the handler's persisted VM state. |
| src/main/java/io/floci/az/services/aks/AksHandler.java | Registers managed clusters in subscription and resource-group indexes with correctly scoped storage-key filtering. |
| src/main/java/io/floci/az/services/acr/AcrHandler.java | Registers container registries in generic ARM indexes using minimal resource entries. |
| src/main/java/io/floci/az/services/redis/RedisHandler.java | Registers Redis caches in both generic indexes while honoring service enablement. |
| src/main/java/io/floci/az/services/sql/SqlHandler.java | Projects SQL server state into subscription- and resource-group-scoped generic listings. |
| src/test/java/io/floci/az/services/arm/ArmResourceIndexTest.java | Covers scope separation, subscription aggregation, minimal contributor shape, and retained Key Vault compatibility fields. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Client[Generic ARM list request] --> Scope{Requested scope}
Scope -->|Subscription| Subscription[Subscription resource index]
Scope -->|Resource group| ResourceGroup[Resource-group resource index]
Subscription --> Aggregate[indexedResources]
ResourceGroup --> Aggregate
Aggregate --> Own[Storage / Key Vault / Web]
Aggregate --> Providers[Network / API Management / Managed Identity]
Aggregate --> Contributors[CDI ResourceIndexContributor implementations]
Contributors --> Services[ACI / ACR / AKS / VM / Redis / SQL-family]
Own --> Response[ARM value array]
Providers --> Response
Services --> Response
Reviews (2): Last reviewed commit: "fix: aggregate the subscription resource..." | Re-trigger Greptile
GET subscriptions/{sub}/resources returned only Key Vaults and API
Management services, so a subscription holding neither answered with an empty
list while its resource groups held VMs, virtual networks, storage accounts
and database servers. Azure's subscription-scoped listing returns everything
the subscription holds.
Widens ResourceIndexContributor with listSubscriptionResources(sub) — no
default implementation, so a service cannot appear in one listing while
falling silently out of the other, which is the failure this interface exists
to prevent — and gives Network and Managed Identity the subscription-scoped
overload they lacked beside their resource-group one.
Both ARM listings now answer from one assembly, indexedResources(sub, rg),
where a null resource group means subscription scope. They cannot disagree
about what the estate holds.
ArmHandler's own Storage / Key Vault / Web state keeps contributing its full
stored body rather than the trimmed index entry: the azurerm provider reads
this listing to populate its Key Vault cache and looks vaults up by
properties.vaultUri, which a properties-free entry would not carry. That
deviation from Azure — which returns properties only under $expand — is now
recorded where the assembly happens, and pinned by a test.
|
Thank you for extending this to the subscription scope; the estate-level test is the right shape. The widened listing breaks one real client, which I reproduced with the repo's own Terraform config. (blocking) azurerm fills its Key Vault cache from (follow-up, separate PR) The |
Summary
Both of ARM's generic resource listings were incomplete, for the same reason: nothing assembled them from a single source of truth about what the estate holds.
GET .../resourceGroups/{rg}/resourcesbuilt its answer from a hand-maintained set —ArmHandler's own Storage / Key Vault / Web state, plus Network, API Management and Managed Identity — plus the CDIResourceIndexContributorlane, whereAciHandlerwas the only implementation. Every other service with a management plane was missing from its own resource group.GET /subscriptions/{sub}/resourcesaggregated only Key Vaults and API Management services, so a subscription holding neither answered{"value":[]}while its groups held VMs, virtual networks, storage accounts and database servers.This PR registers the eight absent providers through the existing extension point:
Microsoft.Compute/virtualMachinesVmHandlerMicrosoft.ContainerService/managedClustersAksHandlerMicrosoft.ContainerRegistry/registriesAcrHandlerMicrosoft.Cache/RedisRedisHandlerMicrosoft.DBforPostgreSQL/flexibleServersPostgresHandlerMicrosoft.DBforMySQL/flexibleServersMySqlHandlerMicrosoft.DBforMariaDB/serversMariaDbHandlerMicrosoft.Sql/serversSqlHandlerand then makes both listings answer from one assembly,
ArmHandler.indexedResources(sub, rg), where a null resource group means subscription scope — so they cannot disagree about the estate.ResourceIndexContributorgainslistSubscriptionResources(sub)with no default implementation, so a service cannot appear in one listing while falling silently out of the other; Network and Managed Identity gain the subscription-scoped overload they lacked beside their resource-group one.Each contributor answers from the state its own list endpoint already reads (
scanAll()for the container-backed services,state.listServersBy...for the DB family), so the index cannot drift from the type-scoped listing.ArmResources.indexEntry(...)centralises the entry shape —id,name,type,location,tags, and noproperties, which real Azure returns only under$expand.AciModelsbuilds its entry through it too, so nine contributors share one definition rather than nine copies.One deliberate deviation is preserved and now documented where the assembly happens:
ArmHandler's own Storage / Key Vault / Web state contributes its full stored body, not the trimmed entry, because the azurerm provider reads this listing to populate its Key Vault cache and looks vaults up byproperties.vaultUri. A test pins it.Cosmos, Service Bus, Event Hubs and App Configuration are deliberately untouched: they have no ARM management plane in floci-az, so they own no resource to index.
Closes #269.
Type of change
fix:)feat:)feat!:orfix!:)Azure Compatibility
Incorrect behavior: real Azure returns every resource in scope from both listings. floci-az returned a subset from one and almost nothing from the other. A VM created in a group answered a direct
GETwith200and appeared under/providers/Microsoft.Compute/virtualMachines, yet the group listing came back without it — and the subscription listing came back empty on an estate with a storage account and a virtual network in it.That resource-group listing is what the
azurermprovider reads before deleting a resource group to verify it is empty;ResourceIndexContributor's own javadoc records that a service which skips registration letsterraform destroyremove a group whose resources still exist. Both listings are also what anything enumerating an estate generically depends on —az resource list, the Resource Management SDKs, and drift checkers that diff deployed state against expected, which report real resources as missing.Verified live against the release image and a build of this branch, same requests both times:
Tags round-trip into the index entry,
propertiesstays out of it (matchingGenericResourceExpanded), and the group listing stays scoped to its own group while the subscription listing spans every group.Checklist
io.floci.az.corepackage plus every test class in the packages this touches (aci,acr,aks,vm,redis,postgres,mysql,mariadb,sql,arm,apim,managedidentity,network), excluding the*DockerTestclasses. Full disclosure, same as in fix: honor X-Forwarded-Proto in /metadata/endpoints so URLs are https behind a TLS proxy #253: the complete./mvnw testspins up docker-based service emulators I can't run in my sandbox, so I gated on those packages instead. CI ran the rest — the first commit came back green on all 13 checks including every SDK compat suite and compat-terraform/opentofu/azcli.ArmResourceIndexTestfor what a per-service test cannot show: that the group listing stays scoped while the subscription listing spans groups, that a contributor's entry carries identity withoutproperties, and that a key vault'svaultUrisurvives.src/mainto the pre-fix state leaves 8 failures, one per newly-registered provider; reverting it to the resource-group-only state leaves 11, exactly the subscription-scope assertions. With both commits applied, all pass.