diff --git a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts index c8a99a18ff9..166ea418af2 100644 --- a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts +++ b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts @@ -66,7 +66,7 @@ describe('AzureMonitor resourcePickerData', () => { }); }); - it('makes multiple requests when arg returns a skipToken and passes the right skipToken to each subsequent call', async () => { + it('makes multiple requests for subscriptions when arg returns a skipToken and passes the right skipToken to each subsequent call', async () => { const response1 = { ...createMockARGSubscriptionResponse(), $skipToken: 'skipfirst100', @@ -82,6 +82,48 @@ describe('AzureMonitor resourcePickerData', () => { expect(postBody.options.$skipToken).toEqual('skipfirst100'); }); + it('makes multiple requests for resource groups when arg returns a skipToken and passes the right skipToken to each subsequent call', async () => { + const subscriptionResponse = createMockARGSubscriptionResponse(); + const resourceGroupResponse1 = { + ...createMockARGResourceGroupsResponse(), + $skipToken: 'skipfirst100', + }; + const resourceGroupResponse2 = createMockARGResourceGroupsResponse(); + const { resourcePickerData, postResource } = createResourcePickerData([ + subscriptionResponse, + resourceGroupResponse1, + resourceGroupResponse2, + ]); + + await resourcePickerData.getResourceGroupsBySubscriptionId('1', 'metrics'); + + expect(postResource).toHaveBeenCalledTimes(3); + const secondCall = postResource.mock.calls[2]; + const [_, postBody] = secondCall; + expect(postBody.options.$skipToken).toEqual('skipfirst100'); + }); + + it('makes multiple requests for resources when arg returns a skipToken and passes the right skipToken to each subsequent call', async () => { + const subscriptionResponse = createMockARGSubscriptionResponse(); + const resourcesResponse1 = { + ...createARGResourcesResponse(), + $skipToken: 'skipfirst100', + }; + const resourcesResponse2 = createARGResourcesResponse(); + const { resourcePickerData, postResource } = createResourcePickerData([ + subscriptionResponse, + resourcesResponse1, + resourcesResponse2, + ]); + + await resourcePickerData.getResourcesForResourceGroup('resourceGroupURI', 'metrics'); + + expect(postResource).toHaveBeenCalledTimes(3); + const secondCall = postResource.mock.calls[2]; + const [_, postBody] = secondCall; + expect(postBody.options.$skipToken).toEqual('skipfirst100'); + }); + it('returns a concatenates a formatted array of subscriptions when there are multiple pages from arg', async () => { const response1 = { ...createMockARGSubscriptionResponse(), @@ -129,7 +171,9 @@ describe('AzureMonitor resourcePickerData', () => { const firstCall = postResource.mock.calls[0]; const [path, postBody] = firstCall; expect(path).toEqual('resourcegraph/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01'); - expect(postBody.query).toContain("type == 'microsoft.resources/subscriptions/resourcegroups'"); + expect(postBody.query).toContain( + 'extend resourceGroupURI = strcat("/subscriptions/", subscriptionId, "/resourcegroups/", resourceGroup)' + ); expect(postBody.query).toContain("where subscriptionId == '123'"); }); diff --git a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts index c0f9bd9500b..135dc332572 100644 --- a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts +++ b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts @@ -187,18 +187,23 @@ export default class ResourcePickerData extends DataSourceWithBackend< type: ResourcePickerQueryType ): Promise { // We can use subscription ID for the filtering here as they're unique + // The logic of this query is: + // Retrieve _all_ resources a user/app registration/identity has access to + // Filter by the namespaces that support metrics + // Filter to resources contained within the subscription + // Conduct a left-outer join on the resourcecontainers table to allow us to get the case-sensitive resource group name + // Return the count of resources in a group, the URI, and name of the group in ascending order const query = ` - resources - | join kind=inner ( - ResourceContainers - | where type == 'microsoft.resources/subscriptions/resourcegroups' - | project resourceGroupURI=id, resourceGroupName=name, resourceGroup, subscriptionId - ) on resourceGroup, subscriptionId - - ${await this.filterByType(type)} - | where subscriptionId == '${subscriptionId}' - | summarize count() by resourceGroupName, resourceGroupURI - | order by resourceGroupURI asc`; + resources + ${await this.filterByType(type)} + | where subscriptionId == '${subscriptionId}' + | extend resourceGroupURI = strcat("/subscriptions/", subscriptionId, "/resourcegroups/", resourceGroup) + | join kind=leftouter (resourcecontainers + | where type =~ 'microsoft.resources/subscriptions/resourcegroups' + | project resourceGroupName=name, resourceGroupURI=tolower(id)) on resourceGroupURI + | project resourceGroupName=iff(resourceGroupName != "", resourceGroupName, resourceGroup), resourceGroupURI + | summarize count() by resourceGroupName, resourceGroupURI + | order by tolower(resourceGroupName) asc `; let resourceGroups: RawAzureResourceGroupItem[] = []; let allFetched = false; @@ -240,13 +245,30 @@ export default class ResourcePickerData extends DataSourceWithBackend< // We use resource group URI for the filtering here because resource group names are not unique across subscriptions // We also add a slash at the end of the resource group URI to ensure we do not pull resources from a resource group // that has a similar naming prefix e.g. resourceGroup1 and resourceGroup10 - const { data: response } = await this.makeResourceGraphRequest(` - resources - | where id hasprefix "${resourceGroupUri}/" - ${await this.filterByType(type)} - `); + const query = ` + resources + | where id hasprefix "${resourceGroupUri}/" + ${await this.filterByType(type)} + | order by tolower(name) asc`; - return response.map((item) => { + let resources: RawAzureResourceItem[] = []; + let allFetched = false; + let $skipToken = undefined; + while (!allFetched) { + // The response may include several pages + let options: Partial = {}; + if ($skipToken) { + options = { + $skipToken, + }; + } + const resourceResponse = await this.makeResourceGraphRequest(query, 1, options); + resources = resources.concat(resourceResponse.data); + $skipToken = resourceResponse.$skipToken; + allFetched = !$skipToken; + } + + return resources.map((item) => { const parsedUri = parseResourceURI(item.id); if (!parsedUri || !parsedUri.resourceName) { throw new Error('unable to fetch resource details');