Azure Monitor: Allow multi-value variables (#62238)

This commit is contained in:
Andres Martinez Gotor 2023-01-27 11:40:49 +01:00 committed by GitHub
parent 3447ad2602
commit 6292a41b24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 158 additions and 54 deletions

View File

@ -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', () => {

View File

@ -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<AzureM
const templateSrv = getTemplateSrv();
const subscriptionId = templateSrv.replace(target.subscription || this.defaultSubscriptionId, scopedVars);
const resources = item.resources?.map((r) => ({
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<AzureM
});
}
getResourceNames(subscriptionId: string, resourceGroup?: string, metricNamespace?: string, skipToken?: string) {
const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')
? 'microsoft.storage/storageaccounts'
: metricNamespace;
let url = `${this.resourcePath}/subscriptions/${subscriptionId}`;
if (resourceGroup) {
url += `/resourceGroups/${resourceGroup}`;
}
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);
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<AzureM
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
this.resourcePath,
this.apiVersion,
this.replaceTemplateVariables(query),
// Only use the first query, as the metric names should be the same for all queries
this.replaceSingleTemplateVariables(query),
this.templateSrv
);
return this.getResource(url).then((result: AzureMonitorMetricNamesResponse) => {
@ -259,7 +262,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
this.resourcePath,
this.apiVersion,
this.replaceTemplateVariables(query),
// Only use the first query, as the metric metadata should be the same for all queries
this.replaceSingleTemplateVariables(query),
this.templateSrv
);
return this.getResource(url).then((result: AzureMonitorMetricsMetadataResponse) => {
@ -293,16 +297,42 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
return typeof field === 'string' && field.length > 0;
}
private replaceTemplateVariables<T extends { [K in keyof T]: string }>(query: T) {
const templateSrv = getTemplateSrv();
private replaceSingleTemplateVariables<T extends { [K in keyof T]: string }>(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<T extends { [K in keyof T]: string }>(query: T, scopedVars?: ScopedVars) {
const workingQueries: Array<{ [K in keyof T]: string }> = [{ ...query }];
const keys = Object.keys(query) as Array<keyof T>;
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) {

View File

@ -156,11 +156,7 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
}
getResourceNames(subscriptionId: string, resourceGroup?: string, metricNamespace?: string) {
return this.azureMonitorDatasource.getResourceNames(
this.templateSrv.replace(subscriptionId),
this.templateSrv.replace(resourceGroup),
this.templateSrv.replace(metricNamespace)
);
return this.azureMonitorDatasource.getResourceNames({ subscriptionId, resourceGroup, metricNamespace });
}
getMetricNames(subscriptionId: string, resourceGroup: string, metricNamespace: string, resourceName: string) {

View File

@ -259,6 +259,12 @@ export interface LegacyAzureGetMetricMetadataQuery {
metricName: string;
}
export interface AzureGetResourceNamesQuery {
subscriptionId: string;
resourceGroup?: string;
metricNamespace?: string;
}
export interface AzureMonitorLocations {
displayName: string;
name: string;