diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts index 98934049c87..26275e04e74 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts @@ -5,7 +5,7 @@ import { TemplateSrv } from 'app/features/templating/template_srv'; import createMockQuery from '../__mocks__/query'; import { createTemplateVariables } from '../__mocks__/utils'; -import { singleVariable, subscriptionsVariable } from '../__mocks__/variables'; +import { multiVariable, singleVariable, subscriptionsVariable } from '../__mocks__/variables'; import AzureMonitorDatasource from '../datasource'; import { AzureDataSourceJsonData, AzureMonitorLocationsResponse, AzureQueryType } from '../types'; @@ -122,6 +122,43 @@ describe('AzureMonitorDatasource', () => { }, }); }); + + it('expand template variables in resource groups and names', () => { + const resourceGroup = '$rg'; + const resourceName = '$rn'; + templateSrv.init([ + { + id: 'rg', + name: 'rg', + current: { + value: `rg1,rg2`, + }, + }, + { + id: 'rn', + name: 'rn', + current: { + value: `rn1,rn2`, + }, + }, + ]); + const query = createMockQuery({ + azureMonitor: { + resources: [{ resourceGroup, resourceName }], + }, + }); + const templatedQuery = ctx.ds.azureMonitorDatasource.applyTemplateVariables(query, {}); + expect(templatedQuery).toMatchObject({ + azureMonitor: { + resources: [ + { resourceGroup: 'rg1', resourceName: 'rn1' }, + { resourceGroup: 'rg2', resourceName: 'rn1' }, + { resourceGroup: 'rg1', resourceName: 'rn2' }, + { resourceGroup: 'rg2', resourceName: 'rn2' }, + ], + }, + }); + }); }); describe('When performing getMetricNamespaces', () => { @@ -570,6 +607,41 @@ describe('AzureMonitorDatasource', () => { expect(results[0].value).toEqual('nodeapp'); }); }); + + it('should return multiple resources from a template variable', () => { + const tsrv = new TemplateSrv(); + tsrv.replace = jest + .fn() + .mockImplementation((value: string) => (value === `$${multiVariable.id}` ? 'foo,bar' : value)); + const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv); + ds.azureMonitorDatasource.templateSrv = tsrv; + ds.azureMonitorDatasource.getResource = jest + .fn() + .mockImplementationOnce((path: string) => { + expect(path).toMatch('foo'); + return Promise.resolve(response); + }) + .mockImplementationOnce((path: string) => { + expect(path).toMatch('bar'); + return Promise.resolve({ + value: [ + { + name: resourceGroup + '2', + type: metricNamespace, + }, + ], + }); + }); + return ds + .getResourceNames(subscription, `$${multiVariable.id}`, metricNamespace) + .then((results: Array<{ text: string; value: string }>) => { + expect(results.length).toEqual(2); + expect(results[0].text).toEqual('nodeapp'); + expect(results[0].value).toEqual('nodeapp'); + expect(results[1].text).toEqual('nodeapp2'); + expect(results[1].value).toEqual('nodeapp2'); + }); + }); }); describe('and the metric definition is blobServices', () => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts index 16c37b1b2e5..4a30a99c961 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts @@ -22,6 +22,7 @@ import { AzureMonitorLocations, AzureMonitorProvidersResponse, AzureMonitorLocationsResponse, + AzureGetResourceNamesQuery, } from '../types'; import { routeNames } from '../utils/common'; import migrateQuery from '../utils/migrateQuery'; @@ -98,10 +99,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend ({ - resourceGroup: templateSrv.replace(r.resourceGroup, scopedVars), - resourceName: templateSrv.replace(r.resourceName, scopedVars), - })); + const resources = item.resources?.map((r) => this.replaceTemplateVariables(r, scopedVars)).flat(); const metricNamespace = templateSrv.replace(item.metricNamespace, scopedVars); const customNamespace = templateSrv.replace(item.customNamespace, scopedVars); const timeGrain = templateSrv.replace((item.timeGrain || '').toString(), scopedVars); @@ -165,53 +163,57 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { - let list: Array<{ text: string; value: string }> = []; - if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) { - list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts'); - for (let i = 0; i < list.length; i++) { - list[i].text += '/default'; - list[i].value += '/default'; - } - } else { - list = ResponseParser.parseResourceNames(result, metricNamespace); + async getResourceNames(query: AzureGetResourceNamesQuery, skipToken?: string) { + const promises = this.replaceTemplateVariables(query).map(({ metricNamespace, subscriptionId, resourceGroup }) => { + const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/') + ? 'microsoft.storage/storageaccounts' + : metricNamespace; + let url = `${this.resourcePath}/subscriptions/${subscriptionId}`; + if (resourceGroup) { + url += `/resourceGroups/${resourceGroup}`; } - - if (result.nextLink) { - // If there is a nextLink, we should request more pages - const nextURL = new URL(result.nextLink); - const nextToken = nextURL.searchParams.get('$skiptoken'); - if (!nextToken) { - throw Error('unable to request the next page of resources'); - } - const nextPage = await this.getResourceNames(subscriptionId, resourceGroup, metricNamespace, nextToken); - list = list.concat(nextPage); + url += `/resources?api-version=${this.listByResourceGroupApiVersion}`; + if (validMetricNamespace) { + url += `&$filter=resourceType eq '${validMetricNamespace}'`; } + if (skipToken) { + url += `&$skiptoken=${skipToken}`; + } + return this.getResource(url).then(async (result: any) => { + let list: Array<{ text: string; value: string }> = []; + if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) { + list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts'); + for (let i = 0; i < list.length; i++) { + list[i].text += '/default'; + list[i].value += '/default'; + } + } else { + list = ResponseParser.parseResourceNames(result, metricNamespace); + } - return list; + if (result.nextLink) { + // If there is a nextLink, we should request more pages + const nextURL = new URL(result.nextLink); + const nextToken = nextURL.searchParams.get('$skiptoken'); + if (!nextToken) { + throw Error('unable to request the next page of resources'); + } + const nextPage = await this.getResourceNames({ metricNamespace, subscriptionId, resourceGroup }, nextToken); + list = list.concat(nextPage); + } + + return list; + }); }); + return (await Promise.all(promises)).flat(); } getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean) { const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl( this.resourcePath, this.apiPreviewVersion, - this.replaceTemplateVariables(query), + // Only use the first query, as the metric namespaces should be the same for all queries + this.replaceSingleTemplateVariables(query), globalRegion, this.templateSrv ); @@ -246,7 +248,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { @@ -259,7 +262,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { @@ -293,16 +297,42 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend 0; } - private replaceTemplateVariables(query: T) { - const templateSrv = getTemplateSrv(); + private replaceSingleTemplateVariables(query: T, scopedVars?: ScopedVars) { + // This method evaluates template variables supporting multiple values but only returns the first value. + // This will work as far as the the first combination of variables is valid. + // For example if 'rg1' contains 'res1' and 'rg2' contains 'res2' then + // { resourceGroup: ['rg1', 'rg2'], resourceName: ['res1', 'res2'] } would return + // { resourceGroup: 'rg1', resourceName: 'res1' } which is valid but + // { resourceGroup: ['rg1', 'rg2'], resourceName: ['res2'] } would result in + // { resourceGroup: 'rg1', resourceName: 'res2' } which is not. + return this.replaceTemplateVariables(query, scopedVars)[0]; + } - const workingQuery: { [K in keyof T]: string } = { ...query }; + private replaceTemplateVariables(query: T, scopedVars?: ScopedVars) { + const workingQueries: Array<{ [K in keyof T]: string }> = [{ ...query }]; const keys = Object.keys(query) as Array; keys.forEach((key) => { - workingQuery[key] = templateSrv.replace(workingQuery[key]); + const replaced = this.templateSrv.replace(workingQueries[0][key], scopedVars, 'raw'); + if (replaced.includes(',')) { + const multiple = replaced.split(','); + const currentQueries = [...workingQueries]; + multiple.forEach((value, i) => { + currentQueries.forEach((q) => { + if (i === 0) { + q[key] = value; + } else { + workingQueries.push({ ...q, [key]: value }); + } + }); + }); + } else { + workingQueries.forEach((q) => { + q[key] = replaced; + }); + } }); - return workingQuery; + return workingQueries; } async getProvider(providerName: string) { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index a3903e0bceb..aadb5583421 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -156,11 +156,7 @@ export default class Datasource extends DataSourceWithBackend