[release-11.5.4] Azure: Resource picker improvements (#103638)

Azure: Resource picker improvements (#101462)

* Update resource group query

- Updates the resource groups query to support users/apps with restricted permissions

* Update resources request to be paginated

- Also order by name
- Add tests

* Update test

(cherry picked from commit 4595df9be6)
pull/103650/head
Andreas Christou 8 months ago committed by GitHub
parent 268d873715
commit 0b6f023cd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 48
      public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts
  2. 56
      public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.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'");
});

@ -187,18 +187,23 @@ export default class ResourcePickerData extends DataSourceWithBackend<
type: ResourcePickerQueryType
): Promise<ResourceRowGroup> {
// 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<RawAzureResourceItem[]>(`
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<AzureResourceGraphOptions> = {};
if ($skipToken) {
options = {
$skipToken,
};
}
const resourceResponse = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(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');

Loading…
Cancel
Save