mirror of
https://github.com/grafana/grafana.git
synced 2025-08-06 20:59:35 +08:00
Azure: Variable editor and resource picker improvements (#101695)
* Update namespace endpoint to filter out only relevant namespaces * Update tests * Fix url builder tests * Add todo comments * Update func to use ARG to retrieve namespaces with metrics * Refactor getMetricNamespaces for readability * Lint * Remove comments * Remove type assertion * Refactor ARG query * Update tests and refactor class to use ARG * 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 * Start refactoring azure monitor util functions to resource graph * Minor lint * Add getMetricNamespaces resource graph function * Modify getMetricNamespaces call - Use resource graph function for variable queries * Return names for getResourceNames values * Use getMetricNamespaces variable specific req in editor * Substantial refactor - Update Azure Resource Graph data source with a method for making paged requests and a method for retrieving metric namespaces (and add tests) - Extract helpers from azure_monitor_datasource to utils and generalise them (also revert previous changes to datasource and test file) - Update mock with Azure Resource Graph data source - Revert response parser changes - Revert url builder changes - Update get metric namespaces query to use the resource graph method for variable queries - Update docs * Lint * Oops * Fix type * Lint and betterer * Simplify imports * Improve type * Simplify resource picker class * Start updating tests * Fix naming and include missing error * Update resource graph data source mock * Update tests * Remove unneeded parser * Add get subscriptions to resource graph * Generalise resource groups request * Fix resource names request to ensure no clashing * Update types * Use resource graph queries for resource picker * Correctly map resource group names * Update mocks * Update tests * Fix mapping * Refactor getResourceNames - Remove most of the logic from resourcePickerData - Add helper for parsing resource names as template vars - Some renames for clarity - Update types - Update tests * Ensure namespaces are lowercase * Update docs/sources/datasources/azure-monitor/template-variables/index.md Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com> * Prettier write * Ensure we return all namespaces if resourceGroup isn't specified --------- Co-authored-by: alyssabull <alyssabull@gmail.com> Co-authored-by: Larissa Wandzura <126723338+lwandz13@users.noreply.github.com>
This commit is contained in:
@ -10,26 +10,38 @@ export const createMockARGSubscriptionResponse = (): AzureGraphResponse<RawAzure
|
|||||||
{
|
{
|
||||||
subscriptionId: '1',
|
subscriptionId: '1',
|
||||||
subscriptionName: 'Primary Subscription',
|
subscriptionName: 'Primary Subscription',
|
||||||
|
subscriptionURI: '/subscriptions/1',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subscriptionId: '2',
|
subscriptionId: '2',
|
||||||
subscriptionName: 'Dev Subscription',
|
subscriptionName: 'Dev Subscription',
|
||||||
|
subscriptionURI: '/subscriptions/2',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subscriptionId: '3',
|
subscriptionId: '3',
|
||||||
subscriptionName: 'Dev Subscription',
|
subscriptionName: 'Dev Subscription',
|
||||||
|
subscriptionURI: '/subscriptions/3',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subscriptionId: '4',
|
subscriptionId: '4',
|
||||||
subscriptionName: 'Primary Subscription',
|
subscriptionName: 'Primary Subscription',
|
||||||
|
subscriptionURI: '/subscriptions/4',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subscriptionId: '5',
|
subscriptionId: '5',
|
||||||
subscriptionName: 'Primary Subscription',
|
subscriptionName: 'Primary Subscription',
|
||||||
|
subscriptionURI: '/subscriptions/5',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subscriptionId: '6',
|
subscriptionId: '6',
|
||||||
subscriptionName: 'Dev Subscription',
|
subscriptionName: 'Dev Subscription',
|
||||||
|
subscriptionURI: '/subscriptions/6',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -39,31 +51,37 @@ export const createMockARGResourceGroupsResponse = (): AzureGraphResponse<RawAzu
|
|||||||
{
|
{
|
||||||
resourceGroupURI: '/subscriptions/abc-123/resourceGroups/prod',
|
resourceGroupURI: '/subscriptions/abc-123/resourceGroups/prod',
|
||||||
resourceGroupName: 'Production',
|
resourceGroupName: 'Production',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
resourceGroupURI: '/subscriptions/def-456/resourceGroups/dev',
|
resourceGroupURI: '/subscriptions/def-456/resourceGroups/dev',
|
||||||
resourceGroupName: 'Development',
|
resourceGroupName: 'Development',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
resourceGroupURI: '/subscriptions/def-456/resourceGroups/test',
|
resourceGroupURI: '/subscriptions/def-456/resourceGroups/test',
|
||||||
resourceGroupName: 'Test',
|
resourceGroupName: 'Test',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
resourceGroupURI: '/subscriptions/abc-123/resourceGroups/test',
|
resourceGroupURI: '/subscriptions/abc-123/resourceGroups/test',
|
||||||
resourceGroupName: 'Test',
|
resourceGroupName: 'Test',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
resourceGroupURI: '/subscriptions/abc-123/resourceGroups/pre-prod',
|
resourceGroupURI: '/subscriptions/abc-123/resourceGroups/pre-prod',
|
||||||
resourceGroupName: 'Pre-production',
|
resourceGroupName: 'Pre-production',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
resourceGroupURI: '/subscriptions/def-456/resourceGroups/qa',
|
resourceGroupURI: '/subscriptions/def-456/resourceGroups/qa',
|
||||||
resourceGroupName: 'QA',
|
resourceGroupName: 'QA',
|
||||||
|
count: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -75,7 +75,10 @@ export default function createMockDatasource(overrides?: DeepPartial<Datasource>
|
|||||||
getResourceURIDisplayProperties: jest.fn().mockResolvedValue({}),
|
getResourceURIDisplayProperties: jest.fn().mockResolvedValue({}),
|
||||||
},
|
},
|
||||||
|
|
||||||
azureResourceGraphDatasource: {},
|
azureResourceGraphDatasource: {
|
||||||
|
pagedResourceGraphRequest: jest.fn().mockResolvedValue([]),
|
||||||
|
...overrides?.azureResourceGraphDatasource,
|
||||||
|
},
|
||||||
getVariablesRaw: jest.fn().mockReturnValue([]),
|
getVariablesRaw: jest.fn().mockReturnValue([]),
|
||||||
currentUserAuth: false,
|
currentUserAuth: false,
|
||||||
...overrides,
|
...overrides,
|
||||||
|
@ -7,7 +7,13 @@ import createMockQuery from '../__mocks__/query';
|
|||||||
import { createTemplateVariables } from '../__mocks__/utils';
|
import { createTemplateVariables } from '../__mocks__/utils';
|
||||||
import { multiVariable } from '../__mocks__/variables';
|
import { multiVariable } from '../__mocks__/variables';
|
||||||
import AzureMonitorDatasource from '../datasource';
|
import AzureMonitorDatasource from '../datasource';
|
||||||
import { AzureAPIResponse, AzureMonitorDataSourceInstanceSettings, Location } from '../types';
|
import {
|
||||||
|
AzureAPIResponse,
|
||||||
|
AzureMonitorDataSourceInstanceSettings,
|
||||||
|
Location,
|
||||||
|
RawAzureResourceGroupItem,
|
||||||
|
RawAzureResourceItem,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
// We want replace to just return the value as is in general/
|
// We want replace to just return the value as is in general/
|
||||||
// We declare this as a function so that we can overwrite it in each test
|
// We declare this as a function so that we can overwrite it in each test
|
||||||
@ -754,20 +760,20 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('When performing getResourceGroups', () => {
|
describe('When performing getResourceGroups', () => {
|
||||||
const response = {
|
const response = {
|
||||||
value: [{ name: 'grp1' }, { name: 'grp2' }],
|
data: [{ resourceGroupName: 'grp1' }, { resourceGroupName: 'grp2' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
|
ctx.ds.azureResourceGraphDatasource.postResource = jest.fn().mockResolvedValue(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return list of Resource Groups', () => {
|
it('should return list of Resource Groups', () => {
|
||||||
return ctx.ds.getResourceGroups('subscriptionId').then((results: Array<{ text: string; value: string }>) => {
|
return ctx.ds.getResourceGroups('subscriptionId').then((results: RawAzureResourceGroupItem[]) => {
|
||||||
expect(results.length).toEqual(2);
|
expect(results.length).toEqual(2);
|
||||||
expect(results[0].text).toEqual('grp1');
|
expect(results[0].resourceGroupName).toEqual('grp1');
|
||||||
expect(results[0].value).toEqual('grp1');
|
expect(results[0].resourceGroupName).toEqual('grp1');
|
||||||
expect(results[1].text).toEqual('grp2');
|
expect(results[1].resourceGroupName).toEqual('grp2');
|
||||||
expect(results[1].value).toEqual('grp2');
|
expect(results[1].resourceGroupName).toEqual('grp2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -786,11 +792,7 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
|
|
||||||
describe('and there are no special cases', () => {
|
describe('and there are no special cases', () => {
|
||||||
const response = {
|
const response = {
|
||||||
value: [
|
data: [
|
||||||
{
|
|
||||||
name: 'Failure Anomalies - nodeapp',
|
|
||||||
type: 'microsoft.insights/alertrules',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: resourceGroup,
|
name: resourceGroup,
|
||||||
type: metricNamespace,
|
type: metricNamespace,
|
||||||
@ -799,13 +801,7 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
ctx.ds.azureResourceGraphDatasource.postResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
|
|
||||||
expect(path).toBe(
|
|
||||||
`${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricNamespace}'${
|
|
||||||
region ? ` and location eq '${region}'` : ''
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -813,10 +809,10 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
it('should return list of Resource Names', () => {
|
it('should return list of Resource Names', () => {
|
||||||
return ctx.ds
|
return ctx.ds
|
||||||
.getResourceNames(subscription, resourceGroup, metricNamespace)
|
.getResourceNames(subscription, resourceGroup, metricNamespace)
|
||||||
.then((results: Array<{ text: string; value: string }>) => {
|
.then((results: RawAzureResourceItem[]) => {
|
||||||
expect(results.length).toEqual(1);
|
expect(results.length).toEqual(1);
|
||||||
expect(results[0].text).toEqual('nodeapp');
|
expect(results[0].name).toEqual('nodeapp');
|
||||||
expect(results[0].value).toEqual('nodeapp');
|
expect(results[0].name).toEqual('nodeapp');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -824,10 +820,10 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
metricNamespace = 'microsoft.insights/Components';
|
metricNamespace = 'microsoft.insights/Components';
|
||||||
return ctx.ds
|
return ctx.ds
|
||||||
.getResourceNames(subscription, resourceGroup, metricNamespace)
|
.getResourceNames(subscription, resourceGroup, metricNamespace)
|
||||||
.then((results: Array<{ text: string; value: string }>) => {
|
.then((results: RawAzureResourceItem[]) => {
|
||||||
expect(results.length).toEqual(1);
|
expect(results.length).toEqual(1);
|
||||||
expect(results[0].text).toEqual('nodeapp');
|
expect(results[0].name).toEqual('nodeapp');
|
||||||
expect(results[0].value).toEqual('nodeapp');
|
expect(results[0].name).toEqual('nodeapp');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -835,10 +831,10 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
region = 'eastus';
|
region = 'eastus';
|
||||||
return ctx.ds
|
return ctx.ds
|
||||||
.getResourceNames(subscription, resourceGroup, metricNamespace, region)
|
.getResourceNames(subscription, resourceGroup, metricNamespace, region)
|
||||||
.then((results: Array<{ text: string; value: string }>) => {
|
.then((results: RawAzureResourceItem[]) => {
|
||||||
expect(results.length).toEqual(1);
|
expect(results.length).toEqual(1);
|
||||||
expect(results[0].text).toEqual('nodeapp');
|
expect(results[0].name).toEqual('nodeapp');
|
||||||
expect(results[0].value).toEqual('nodeapp');
|
expect(results[0].name).toEqual('nodeapp');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -869,43 +865,34 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
return target === `$${multiVariable.id}` ? 'foo,bar' : (target ?? '');
|
return target === `$${multiVariable.id}` ? 'foo,bar' : (target ?? '');
|
||||||
};
|
};
|
||||||
const ds = new AzureMonitorDatasource(ctx.instanceSettings);
|
const ds = new AzureMonitorDatasource(ctx.instanceSettings);
|
||||||
//ds.azureMonitorDatasource.templateSrv = tsrv;
|
ds.azureResourceGraphDatasource.postResource = jest
|
||||||
ds.azureMonitorDatasource.getResource = jest
|
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce((path: string) => {
|
.mockImplementationOnce(() => Promise.resolve(response))
|
||||||
expect(path).toMatch('foo');
|
.mockImplementationOnce(() =>
|
||||||
return Promise.resolve(response);
|
Promise.resolve({
|
||||||
})
|
data: [
|
||||||
.mockImplementationOnce((path: string) => {
|
|
||||||
expect(path).toMatch('bar');
|
|
||||||
return Promise.resolve({
|
|
||||||
value: [
|
|
||||||
{
|
{
|
||||||
name: resourceGroup + '2',
|
name: resourceGroup + '2',
|
||||||
type: metricNamespace,
|
type: metricNamespace,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
})
|
||||||
});
|
);
|
||||||
return ds
|
return ds
|
||||||
.getResourceNames(subscription, `$${multiVariable.id}`, metricNamespace)
|
.getResourceNames(subscription, `$${multiVariable.id}`, metricNamespace)
|
||||||
.then((results: Array<{ text: string; value: string }>) => {
|
.then((results: RawAzureResourceItem[]) => {
|
||||||
expect(results.length).toEqual(2);
|
expect(results.length).toEqual(2);
|
||||||
expect(results[0].text).toEqual('nodeapp');
|
expect(results[0].name).toEqual('nodeapp');
|
||||||
expect(results[0].value).toEqual('nodeapp');
|
expect(results[0].name).toEqual('nodeapp');
|
||||||
expect(results[1].text).toEqual('nodeapp2');
|
expect(results[1].name).toEqual('nodeapp2');
|
||||||
expect(results[1].value).toEqual('nodeapp2');
|
expect(results[1].name).toEqual('nodeapp2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and the metric definition is blobServices', () => {
|
describe('and the metric definition is blobServices', () => {
|
||||||
const response = {
|
const response = {
|
||||||
value: [
|
data: [
|
||||||
{
|
|
||||||
name: 'Failure Anomalies - nodeapp',
|
|
||||||
type: 'microsoft.insights/alertrules',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'storagetest',
|
name: 'storagetest',
|
||||||
type: 'microsoft.storage/storageaccounts',
|
type: 'microsoft.storage/storageaccounts',
|
||||||
@ -916,78 +903,32 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
it('should return list of Resource Names', () => {
|
it('should return list of Resource Names', () => {
|
||||||
metricNamespace = 'microsoft.storage/storageaccounts/blobservices';
|
metricNamespace = 'microsoft.storage/storageaccounts/blobservices';
|
||||||
const validMetricNamespace = 'microsoft.storage/storageaccounts';
|
const validMetricNamespace = 'microsoft.storage/storageaccounts';
|
||||||
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
ctx.ds.azureResourceGraphDatasource.postResource = jest
|
||||||
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
|
.fn()
|
||||||
expect(path).toBe(
|
.mockImplementation(() => Promise.resolve(response));
|
||||||
basePath +
|
|
||||||
`/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${validMetricNamespace}'`
|
|
||||||
);
|
|
||||||
return Promise.resolve(response);
|
|
||||||
});
|
|
||||||
return ctx.ds
|
return ctx.ds
|
||||||
.getResourceNames(subscription, resourceGroup, metricNamespace)
|
.getResourceNames(subscription, resourceGroup, metricNamespace)
|
||||||
.then((results: Array<{ text: string; value: string }>) => {
|
.then((results: RawAzureResourceItem[]) => {
|
||||||
expect(results.length).toEqual(1);
|
expect(results.length).toEqual(1);
|
||||||
expect(results[0].text).toEqual('storagetest/default');
|
expect(results[0].name).toEqual('storagetest');
|
||||||
expect(results[0].value).toEqual('storagetest/default');
|
expect(results[0].name).toEqual('storagetest');
|
||||||
expect(ctx.ds.azureMonitorDatasource.getResource).toHaveBeenCalledWith(
|
expect(ctx.ds.azureResourceGraphDatasource.postResource).toHaveBeenCalledWith(
|
||||||
`azuremonitor/subscriptions/${subscription}/resourceGroups/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${validMetricNamespace}'`
|
'resourcegraph/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01',
|
||||||
|
{
|
||||||
|
options: { resultFormat: 'objectArray' },
|
||||||
|
query: `resources
|
||||||
|
| where id hasprefix \"/subscriptions/${subscription}/resourceGroups/${resourceGroup}/\"
|
||||||
|
| where type == '${validMetricNamespace}'
|
||||||
|
| order by tolower(name) asc`,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('and there are several pages', () => {
|
|
||||||
const skipToken = 'token';
|
|
||||||
const response1 = {
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
name: `${resourceGroup}1`,
|
|
||||||
type: metricNamespace,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
nextLink: `https://management.azure.com/resourceuri?$skiptoken=${skipToken}`,
|
|
||||||
};
|
|
||||||
const response2 = {
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
name: `${resourceGroup}2`,
|
|
||||||
type: metricNamespace,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const fn = jest.fn();
|
|
||||||
ctx.ds.azureMonitorDatasource.getResource = fn;
|
|
||||||
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
|
|
||||||
const expectedPath = `${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricNamespace}'`;
|
|
||||||
// first page
|
|
||||||
fn.mockImplementationOnce((path: string) => {
|
|
||||||
expect(path).toBe(expectedPath);
|
|
||||||
return Promise.resolve(response1);
|
|
||||||
});
|
|
||||||
// second page
|
|
||||||
fn.mockImplementationOnce((path: string) => {
|
|
||||||
expect(path).toBe(`${expectedPath}&$skiptoken=${skipToken}`);
|
|
||||||
return Promise.resolve(response2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return list of Resource Names', () => {
|
|
||||||
return ctx.ds
|
|
||||||
.getResourceNames(subscription, resourceGroup, metricNamespace)
|
|
||||||
.then((results: Array<{ text: string; value: string }>) => {
|
|
||||||
expect(results.length).toEqual(2);
|
|
||||||
expect(results[0].value).toEqual(`${resourceGroup}1`);
|
|
||||||
expect(results[1].value).toEqual(`${resourceGroup}2`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('without a resource group or a metric definition', () => {
|
describe('without a resource group or a metric definition', () => {
|
||||||
const response = {
|
const response = {
|
||||||
value: [
|
data: [
|
||||||
{
|
{
|
||||||
name: 'Failure Anomalies - nodeapp',
|
name: 'Failure Anomalies - nodeapp',
|
||||||
type: 'microsoft.insights/alertrules',
|
type: 'microsoft.insights/alertrules',
|
||||||
@ -1000,15 +941,13 @@ describe('AzureMonitorDatasource', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
ctx.ds.azureResourceGraphDatasource.postResource = jest.fn().mockImplementation((path: string) => {
|
||||||
const basePath = `azuremonitor/subscriptions/${subscription}/resources?api-version=2021-04-01`;
|
|
||||||
expect(path).toBe(basePath);
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return list of Resource Names', () => {
|
it('should return list of Resource Names', () => {
|
||||||
return ctx.ds.getResourceNames(subscription).then((results: Array<{ text: string; value: string }>) => {
|
return ctx.ds.getResourceNames(subscription).then((results: RawAzureResourceItem[]) => {
|
||||||
expect(results.length).toEqual(2);
|
expect(results.length).toEqual(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { find, startsWith } from 'lodash';
|
import { find } from 'lodash';
|
||||||
|
|
||||||
import { AzureCredentials } from '@grafana/azure-sdk';
|
import { AzureCredentials } from '@grafana/azure-sdk';
|
||||||
import { ScopedVars } from '@grafana/data';
|
import { ScopedVars } from '@grafana/data';
|
||||||
@ -20,10 +20,8 @@ import {
|
|||||||
AzureMonitorLocations,
|
AzureMonitorLocations,
|
||||||
AzureMonitorProvidersResponse,
|
AzureMonitorProvidersResponse,
|
||||||
AzureAPIResponse,
|
AzureAPIResponse,
|
||||||
AzureGetResourceNamesQuery,
|
|
||||||
Subscription,
|
Subscription,
|
||||||
Location,
|
Location,
|
||||||
ResourceGroup,
|
|
||||||
Metric,
|
Metric,
|
||||||
MetricNamespace,
|
MetricNamespace,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
@ -178,68 +176,6 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceGroups(subscriptionId: string) {
|
|
||||||
return this.getResource(
|
|
||||||
`${this.resourcePath}/subscriptions/${subscriptionId}/resourceGroups?api-version=${this.listByResourceGroupApiVersion}`
|
|
||||||
).then((result: AzureAPIResponse<ResourceGroup>) => {
|
|
||||||
return ResponseParser.parseResponseValues<ResourceGroup>(result, 'name', 'name');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getResourceNames(query: AzureGetResourceNamesQuery, skipToken?: string) {
|
|
||||||
const promises = replaceTemplateVariables(this.templateSrv, query).map(
|
|
||||||
({ metricNamespace, subscriptionId, resourceGroup, region }) => {
|
|
||||||
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}`;
|
|
||||||
const filters: string[] = [];
|
|
||||||
if (validMetricNamespace) {
|
|
||||||
filters.push(`resourceType eq '${validMetricNamespace}'`);
|
|
||||||
}
|
|
||||||
if (region) {
|
|
||||||
filters.push(`location eq '${region}'`);
|
|
||||||
}
|
|
||||||
if (filters.length > 0) {
|
|
||||||
url += `&$filter=${filters.join(' and ')}`;
|
|
||||||
}
|
|
||||||
if (skipToken) {
|
|
||||||
url += `&$skiptoken=${skipToken}`;
|
|
||||||
}
|
|
||||||
return this.getResource(url).then(async (result) => {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note globalRegion should be false when querying custom metric namespaces
|
// Note globalRegion should be false when querying custom metric namespaces
|
||||||
getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean, region?: string, custom?: boolean) {
|
getMetricNamespaces(query: GetMetricNamespacesQuery, globalRegion: boolean, region?: string, custom?: boolean) {
|
||||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
|
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
|
||||||
|
@ -12,7 +12,6 @@ import {
|
|||||||
AzureAPIResponse,
|
AzureAPIResponse,
|
||||||
Location,
|
Location,
|
||||||
Subscription,
|
Subscription,
|
||||||
Resource,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
export default class ResponseParser {
|
export default class ResponseParser {
|
||||||
static parseResponseValues<T>(
|
static parseResponseValues<T>(
|
||||||
@ -40,31 +39,6 @@ export default class ResponseParser {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
static parseResourceNames(
|
|
||||||
result: AzureAPIResponse<Resource>,
|
|
||||||
metricNamespace?: string
|
|
||||||
): Array<{ text: string; value: string }> {
|
|
||||||
const list: Array<{ text: string; value: string }> = [];
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < result.value.length; i++) {
|
|
||||||
if (
|
|
||||||
typeof result.value[i].type === 'string' &&
|
|
||||||
(!metricNamespace || result.value[i].type.toLocaleLowerCase() === metricNamespace.toLocaleLowerCase())
|
|
||||||
) {
|
|
||||||
list.push({
|
|
||||||
text: result.value[i].name,
|
|
||||||
value: result.value[i].name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseMetadata(result: AzureMonitorMetricsMetadataResponse, metricName: string) {
|
static parseMetadata(result: AzureMonitorMetricsMetadataResponse, metricName: string) {
|
||||||
const defaultAggTypes = ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'];
|
const defaultAggTypes = ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'];
|
||||||
const metricData = result?.value.find((v) => v.name.value === metricName);
|
const metricData = result?.value.find((v) => v.name.value === metricName);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
// eslint-disable-next-line lodash/import-scope
|
import { startsWith, includes, find, filter } from 'lodash';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import { ScopedVars } from '@grafana/data';
|
import { ScopedVars } from '@grafana/data';
|
||||||
import { getTemplateSrv, DataSourceWithBackend, TemplateSrv } from '@grafana/runtime';
|
import { getTemplateSrv, DataSourceWithBackend, TemplateSrv } from '@grafana/runtime';
|
||||||
@ -9,10 +8,13 @@ import {
|
|||||||
AzureMonitorQuery,
|
AzureMonitorQuery,
|
||||||
AzureMonitorDataSourceJsonData,
|
AzureMonitorDataSourceJsonData,
|
||||||
AzureQueryType,
|
AzureQueryType,
|
||||||
|
RawAzureResourceGroupItem,
|
||||||
|
AzureGetResourceNamesQuery,
|
||||||
AzureMonitorDataSourceInstanceSettings,
|
AzureMonitorDataSourceInstanceSettings,
|
||||||
RawAzureResourceItem,
|
RawAzureResourceItem,
|
||||||
AzureGraphResponse,
|
AzureGraphResponse,
|
||||||
AzureResourceGraphOptions,
|
AzureResourceGraphOptions,
|
||||||
|
RawAzureSubscriptionItem,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { interpolateVariable, replaceTemplateVariables, routeNames } from '../utils/common';
|
import { interpolateVariable, replaceTemplateVariables, routeNames } from '../utils/common';
|
||||||
|
|
||||||
@ -41,14 +43,14 @@ export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
const variableNames = ts.getVariables().map((v) => `$${v.name}`);
|
const variableNames = ts.getVariables().map((v) => `$${v.name}`);
|
||||||
const subscriptionVar = _.find(target.subscriptions, (sub) => _.includes(variableNames, sub));
|
const subscriptionVar = find(target.subscriptions, (sub) => includes(variableNames, sub));
|
||||||
const interpolatedSubscriptions = ts
|
const interpolatedSubscriptions = ts
|
||||||
.replace(subscriptionVar, scopedVars, (v: string[] | string) => v)
|
.replace(subscriptionVar, scopedVars, (v: string[] | string) => v)
|
||||||
.split(',')
|
.split(',')
|
||||||
.filter((v) => v.length > 0);
|
.filter((v) => v.length > 0);
|
||||||
const subscriptions = [
|
const subscriptions = [
|
||||||
...interpolatedSubscriptions,
|
...interpolatedSubscriptions,
|
||||||
..._.filter(target.subscriptions, (sub) => !_.includes(variableNames, sub)),
|
...filter(target.subscriptions, (sub) => !includes(variableNames, sub)),
|
||||||
];
|
];
|
||||||
const query = ts.replace(item.query, scopedVars, interpolateVariable);
|
const query = ts.replace(item.query, scopedVars, interpolateVariable);
|
||||||
|
|
||||||
@ -63,7 +65,7 @@ export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async pagedResourceGraphRequest<T = unknown>(query: string, maxRetries = 1): Promise<T[]> {
|
async pagedResourceGraphRequest<T>(query: string, maxRetries = 1): Promise<T[]> {
|
||||||
try {
|
try {
|
||||||
let allFetched = false;
|
let allFetched = false;
|
||||||
let $skipToken = undefined;
|
let $skipToken = undefined;
|
||||||
@ -101,6 +103,90 @@ export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSubscriptions() {
|
||||||
|
const query = `
|
||||||
|
resources
|
||||||
|
| join kind=inner (
|
||||||
|
ResourceContainers
|
||||||
|
| where type == 'microsoft.resources/subscriptions'
|
||||||
|
| project subscriptionName=name, subscriptionURI=id, subscriptionId
|
||||||
|
) on subscriptionId
|
||||||
|
| summarize count=count() by subscriptionName, subscriptionURI, subscriptionId
|
||||||
|
| order by subscriptionName desc
|
||||||
|
`;
|
||||||
|
|
||||||
|
const subscriptions = await this.pagedResourceGraphRequest<RawAzureSubscriptionItem>(query, 1);
|
||||||
|
|
||||||
|
return subscriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getResourceGroups(subscriptionId: string, metricNamespacesFilter?: string) {
|
||||||
|
// 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 (if this request is from the resource picker)
|
||||||
|
// 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
|
||||||
|
${metricNamespacesFilter || ''}
|
||||||
|
| 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=count() by resourceGroupName, resourceGroupURI
|
||||||
|
| order by tolower(resourceGroupName) asc `;
|
||||||
|
|
||||||
|
const resourceGroups = await this.pagedResourceGraphRequest<RawAzureResourceGroupItem>(query);
|
||||||
|
|
||||||
|
return resourceGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getResourceNames(query: AzureGetResourceNamesQuery, metricNamespacesFilter?: string) {
|
||||||
|
const promises = replaceTemplateVariables(this.templateSrv, query).map(
|
||||||
|
async ({ metricNamespace, subscriptionId, resourceGroup, region, uri }) => {
|
||||||
|
const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')
|
||||||
|
? 'microsoft.storage/storageaccounts'
|
||||||
|
: metricNamespace;
|
||||||
|
|
||||||
|
// URI takes precedence over subscription ID and resource group
|
||||||
|
let prefix = uri;
|
||||||
|
if (!prefix) {
|
||||||
|
if (subscriptionId) {
|
||||||
|
prefix = `/subscriptions/${subscriptionId}`;
|
||||||
|
}
|
||||||
|
if (resourceGroup) {
|
||||||
|
prefix += `/resourceGroups/${resourceGroup}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters: string[] = [];
|
||||||
|
if (validMetricNamespace) {
|
||||||
|
// Ensure the namespace is always lowercase as that's how it's stored in Resource Graph
|
||||||
|
filters.push(`type == '${validMetricNamespace.toLowerCase()}'`);
|
||||||
|
}
|
||||||
|
if (region) {
|
||||||
|
filters.push(`location == '${region}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use URIs for the filtering here because resource group names are not unique across subscriptions
|
||||||
|
// We also add a slash at the end of the URI to ensure we do not pull resources from a resource group
|
||||||
|
// that has a similar naming prefix e.g. resourceGroup1 and resourceGroup10
|
||||||
|
const query = `resources${metricNamespacesFilter ? '\n' + metricNamespacesFilter : ''}
|
||||||
|
| where id hasprefix "${prefix}/"
|
||||||
|
${filters.length > 0 ? `| where ${filters.join(' and ')}` : ''}
|
||||||
|
| order by tolower(name) asc`;
|
||||||
|
|
||||||
|
const resources = await this.pagedResourceGraphRequest<RawAzureResourceItem>(query);
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return (await Promise.all(promises)).flat();
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve metric namespaces relevant to a subscription/resource group/resource
|
// Retrieve metric namespaces relevant to a subscription/resource group/resource
|
||||||
async getMetricNamespaces(resourceUri: string) {
|
async getMetricNamespaces(resourceUri: string) {
|
||||||
const promises = replaceTemplateVariables(this.templateSrv, { resourceUri }).map(async ({ resourceUri }) => {
|
const promises = replaceTemplateVariables(this.templateSrv, { resourceUri }).map(async ({ resourceUri }) => {
|
||||||
|
@ -34,7 +34,8 @@ export function createMockResourcePickerData() {
|
|||||||
const mockDatasource = createMockDatasource();
|
const mockDatasource = createMockDatasource();
|
||||||
const mockResourcePicker = new ResourcePickerData(
|
const mockResourcePicker = new ResourcePickerData(
|
||||||
createMockInstanceSetttings(),
|
createMockInstanceSetttings(),
|
||||||
mockDatasource.azureMonitorDatasource
|
mockDatasource.azureMonitorDatasource,
|
||||||
|
mockDatasource.azureResourceGraphDatasource
|
||||||
);
|
);
|
||||||
|
|
||||||
mockResourcePicker.getSubscriptions = jest.fn().mockResolvedValue(createMockSubscriptions());
|
mockResourcePicker.getSubscriptions = jest.fn().mockResolvedValue(createMockSubscriptions());
|
||||||
|
@ -10,6 +10,8 @@ import {
|
|||||||
mockResourcesByResourceGroup,
|
mockResourcesByResourceGroup,
|
||||||
mockSearchResults,
|
mockSearchResults,
|
||||||
} from '../../__mocks__/resourcePickerRows';
|
} from '../../__mocks__/resourcePickerRows';
|
||||||
|
import { DeepPartial } from '../../__mocks__/utils';
|
||||||
|
import Datasource from '../../datasource';
|
||||||
import ResourcePickerData, { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData';
|
import ResourcePickerData, { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData';
|
||||||
|
|
||||||
import { ResourceRowType } from './types';
|
import { ResourceRowType } from './types';
|
||||||
@ -32,11 +34,15 @@ const singleResourceSelectionURI =
|
|||||||
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server';
|
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server';
|
||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
function createMockResourcePickerData(preserveImplementation?: string[]) {
|
function createMockResourcePickerData(
|
||||||
const mockDatasource = createMockDatasource();
|
preserveImplementation?: string[],
|
||||||
|
datasourceOverrides?: DeepPartial<Datasource>
|
||||||
|
) {
|
||||||
|
const mockDatasource = createMockDatasource(datasourceOverrides);
|
||||||
const mockResourcePicker = new ResourcePickerData(
|
const mockResourcePicker = new ResourcePickerData(
|
||||||
createMockInstanceSetttings(),
|
createMockInstanceSetttings(),
|
||||||
mockDatasource.azureMonitorDatasource
|
mockDatasource.azureMonitorDatasource,
|
||||||
|
mockDatasource.azureResourceGraphDatasource
|
||||||
);
|
);
|
||||||
|
|
||||||
const mockFunctions = omit(
|
const mockFunctions = omit(
|
||||||
@ -332,7 +338,9 @@ describe('AzureMonitor ResourcePicker', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw an error if no namespaces are found - fallback used', async () => {
|
it('should not throw an error if no namespaces are found - fallback used', async () => {
|
||||||
const resourcePickerData = createMockResourcePickerData(['getResourceGroupsBySubscriptionId']);
|
const resourcePickerData = createMockResourcePickerData(['getResourceGroupsBySubscriptionId'], {
|
||||||
|
azureResourceGraphDatasource: { getResourceGroups: jest.fn().mockResolvedValue([]) },
|
||||||
|
});
|
||||||
resourcePickerData.postResource = jest.fn().mockResolvedValueOnce({ data: [] });
|
resourcePickerData.postResource = jest.fn().mockResolvedValueOnce({ data: [] });
|
||||||
render(
|
render(
|
||||||
<ResourcePicker
|
<ResourcePicker
|
||||||
|
@ -26,7 +26,17 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
const getResourceGroups = jest.fn().mockResolvedValue([{ resourceGroupURI: 'rg', resourceGroupName: 'rg', count: 1 }]);
|
||||||
|
const getResourceNames = jest.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'foobarID',
|
||||||
|
name: 'foobar',
|
||||||
|
subscriptionId: 'subID',
|
||||||
|
resourceGroup: 'resourceGroup',
|
||||||
|
type: 'foobarType',
|
||||||
|
location: 'london',
|
||||||
|
},
|
||||||
|
]);
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
query: {
|
query: {
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
@ -39,7 +49,6 @@ const defaultProps = {
|
|||||||
onChange: jest.fn(),
|
onChange: jest.fn(),
|
||||||
datasource: createMockDatasource({
|
datasource: createMockDatasource({
|
||||||
getSubscriptions: jest.fn().mockResolvedValue([{ text: 'Primary Subscription', value: 'sub' }]),
|
getSubscriptions: jest.fn().mockResolvedValue([{ text: 'Primary Subscription', value: 'sub' }]),
|
||||||
getResourceGroups: jest.fn().mockResolvedValue([{ text: 'rg', value: 'rg' }]),
|
|
||||||
getMetricNamespaces: jest
|
getMetricNamespaces: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementation(
|
.mockImplementation(
|
||||||
@ -50,11 +59,16 @@ const defaultProps = {
|
|||||||
return [{ text: 'foo/custom', value: 'foo/custom' }];
|
return [{ text: 'foo/custom', value: 'foo/custom' }];
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
getResourceNames: jest.fn().mockResolvedValue([{ text: 'foobar', value: 'foobar' }]),
|
|
||||||
getVariablesRaw: jest.fn().mockReturnValue([
|
getVariablesRaw: jest.fn().mockReturnValue([
|
||||||
{ label: 'query0', name: 'sub0' },
|
{ label: 'query0', name: 'sub0' },
|
||||||
{ label: 'query1', name: 'rg', query: { queryType: AzureQueryType.ResourceGroupsQuery } },
|
{ label: 'query1', name: 'rg', query: { queryType: AzureQueryType.ResourceGroupsQuery } },
|
||||||
]),
|
]),
|
||||||
|
azureResourceGraphDatasource: {
|
||||||
|
getResourceGroups,
|
||||||
|
getResourceNames,
|
||||||
|
},
|
||||||
|
getResourceGroups,
|
||||||
|
getResourceNames,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ const VariableEditor = (props: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (subscription) {
|
if (subscription) {
|
||||||
datasource.getResourceGroups(subscription).then((rgs) => {
|
datasource.getResourceGroups(subscription).then((rgs) => {
|
||||||
setResourceGroups(rgs.map((s) => ({ label: s.text, value: s.value })));
|
setResourceGroups(rgs.map((s) => ({ label: s.resourceGroupName, value: s.resourceGroupName })));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [datasource, subscription]);
|
}, [datasource, subscription]);
|
||||||
@ -179,8 +179,8 @@ const VariableEditor = (props: Props) => {
|
|||||||
// When subscription, resource group, and namespace are all set, retrieve resource names
|
// When subscription, resource group, and namespace are all set, retrieve resource names
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (subscription && resourceGroup && namespace) {
|
if (subscription && resourceGroup && namespace) {
|
||||||
datasource.getResourceNames(subscription, resourceGroup, namespace).then((rgs) => {
|
datasource.getResourceNames(subscription, resourceGroup, namespace).then((resources) => {
|
||||||
setResources(rgs.map((s) => ({ label: s.text, value: s.value })));
|
setResources(resources.map((s) => ({ label: s.name, value: s.name })));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [datasource, subscription, resourceGroup, namespace]);
|
}, [datasource, subscription, resourceGroup, namespace]);
|
||||||
|
@ -49,7 +49,11 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
|
|||||||
this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings);
|
this.azureMonitorDatasource = new AzureMonitorDatasource(instanceSettings);
|
||||||
this.azureResourceGraphDatasource = new AzureResourceGraphDatasource(instanceSettings);
|
this.azureResourceGraphDatasource = new AzureResourceGraphDatasource(instanceSettings);
|
||||||
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(instanceSettings);
|
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(instanceSettings);
|
||||||
this.resourcePickerData = new ResourcePickerData(instanceSettings, this.azureMonitorDatasource);
|
this.resourcePickerData = new ResourcePickerData(
|
||||||
|
instanceSettings,
|
||||||
|
this.azureMonitorDatasource,
|
||||||
|
this.azureResourceGraphDatasource
|
||||||
|
);
|
||||||
|
|
||||||
this.pseudoDatasource = {
|
this.pseudoDatasource = {
|
||||||
[AzureQueryType.AzureMonitor]: this.azureMonitorDatasource,
|
[AzureQueryType.AzureMonitor]: this.azureMonitorDatasource,
|
||||||
@ -164,10 +168,6 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Azure Monitor REST API methods */
|
/* Azure Monitor REST API methods */
|
||||||
getResourceGroups(subscriptionId: string) {
|
|
||||||
return this.azureMonitorDatasource.getResourceGroups(this.templateSrv.replace(subscriptionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
getMetricNamespaces(
|
getMetricNamespaces(
|
||||||
subscriptionId: string,
|
subscriptionId: string,
|
||||||
resourceGroup?: string,
|
resourceGroup?: string,
|
||||||
@ -200,10 +200,6 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getResourceNames(subscriptionId: string, resourceGroup?: string, metricNamespace?: string, region?: string) {
|
|
||||||
return this.azureMonitorDatasource.getResourceNames({ subscriptionId, resourceGroup, metricNamespace, region });
|
|
||||||
}
|
|
||||||
|
|
||||||
getMetricNames(
|
getMetricNames(
|
||||||
subscriptionId: string,
|
subscriptionId: string,
|
||||||
resourceGroup: string,
|
resourceGroup: string,
|
||||||
@ -220,13 +216,27 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSubscriptions() {
|
||||||
|
return this.azureMonitorDatasource.getSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
/*Azure Log Analytics */
|
/*Azure Log Analytics */
|
||||||
getAzureLogAnalyticsWorkspaces(subscriptionId: string) {
|
getAzureLogAnalyticsWorkspaces(subscriptionId: string) {
|
||||||
return this.azureLogAnalyticsDatasource.getWorkspaces(subscriptionId);
|
return this.azureLogAnalyticsDatasource.getWorkspaces(subscriptionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscriptions() {
|
/*Azure Resource Graph */
|
||||||
return this.azureMonitorDatasource.getSubscriptions();
|
getResourceGroups(subscriptionId: string) {
|
||||||
|
return this.azureResourceGraphDatasource.getResourceGroups(this.templateSrv.replace(subscriptionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
getResourceNames(subscriptionId: string, resourceGroup?: string, metricNamespace?: string, region?: string) {
|
||||||
|
return this.azureResourceGraphDatasource.getResourceNames({
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
metricNamespace,
|
||||||
|
region,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interpolateVariablesInQueries(queries: AzureMonitorQuery[], scopedVars: ScopedVars): AzureMonitorQuery[] {
|
interpolateVariablesInQueries(queries: AzureMonitorQuery[], scopedVars: ScopedVars): AzureMonitorQuery[] {
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
import createMockDatasource from '../__mocks__/datasource';
|
import createMockDatasource from '../__mocks__/datasource';
|
||||||
import { createMockInstanceSetttings } from '../__mocks__/instanceSettings';
|
import { createMockInstanceSetttings } from '../__mocks__/instanceSettings';
|
||||||
import { resourceTypes } from '../azureMetadata';
|
import { resourceTypes } from '../azureMetadata';
|
||||||
|
import AzureResourceGraphDatasource from '../azure_resource_graph/azure_resource_graph_datasource';
|
||||||
import { ResourceRowType } from '../components/ResourcePicker/types';
|
import { ResourceRowType } from '../components/ResourcePicker/types';
|
||||||
import { AzureGraphResponse } from '../types';
|
import { AzureGraphResponse } from '../types';
|
||||||
|
|
||||||
@ -22,18 +23,24 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
|
|
||||||
const createResourcePickerData = (responses: AzureGraphResponse[], noNamespaces?: boolean) => {
|
const createResourcePickerData = (responses: AzureGraphResponse[], noNamespaces?: boolean) => {
|
||||||
const instanceSettings = createMockInstanceSetttings();
|
const instanceSettings = createMockInstanceSetttings();
|
||||||
const mockDatasource = createMockDatasource();
|
const azureResourceGraphDatasource = new AzureResourceGraphDatasource(instanceSettings);
|
||||||
|
const postResource = jest.fn();
|
||||||
|
responses.forEach((res) => {
|
||||||
|
postResource.mockResolvedValueOnce(res);
|
||||||
|
});
|
||||||
|
azureResourceGraphDatasource.postResource = postResource;
|
||||||
|
const mockDatasource = createMockDatasource({ azureResourceGraphDatasource: azureResourceGraphDatasource });
|
||||||
mockDatasource.azureMonitorDatasource.getMetricNamespaces = jest
|
mockDatasource.azureMonitorDatasource.getMetricNamespaces = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce(
|
.mockResolvedValueOnce(
|
||||||
noNamespaces ? [] : [{ text: 'Microsoft.Storage/storageAccounts', value: 'Microsoft.Storage/storageAccounts' }]
|
noNamespaces ? [] : [{ text: 'Microsoft.Storage/storageAccounts', value: 'Microsoft.Storage/storageAccounts' }]
|
||||||
);
|
);
|
||||||
const resourcePickerData = new ResourcePickerData(instanceSettings, mockDatasource.azureMonitorDatasource);
|
|
||||||
const postResource = jest.fn();
|
const resourcePickerData = new ResourcePickerData(
|
||||||
responses.forEach((res) => {
|
instanceSettings,
|
||||||
postResource.mockResolvedValueOnce(res);
|
mockDatasource.azureMonitorDatasource,
|
||||||
});
|
mockDatasource.azureResourceGraphDatasource
|
||||||
resourcePickerData.postResource = postResource;
|
);
|
||||||
return { resourcePickerData, postResource, mockDatasource };
|
return { resourcePickerData, postResource, mockDatasource };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -301,32 +308,6 @@ describe('AzureMonitor resourcePickerData', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if it recieves data with a malformed uri', async () => {
|
|
||||||
const mockResponse = {
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
id: '/a-differently-formatted/uri/than/the/type/we/planned/to/parse',
|
|
||||||
name: 'web-server',
|
|
||||||
type: 'Microsoft.Compute/virtualMachines',
|
|
||||||
resourceGroup: 'dev',
|
|
||||||
subscriptionId: 'def-456',
|
|
||||||
location: 'northeurope',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const { resourcePickerData } = createResourcePickerData([mockResponse]);
|
|
||||||
try {
|
|
||||||
await resourcePickerData.getResourcesForResourceGroup('dev', 'logs');
|
|
||||||
throw Error('expected getResourcesForResourceGroup to fail but it succeeded');
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
expect(err.message).toEqual('unable to fetch resource details');
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should filter metrics resources', async () => {
|
it('should filter metrics resources', async () => {
|
||||||
const mockSubscriptionsResponse = createMockARGSubscriptionResponse();
|
const mockSubscriptionsResponse = createMockARGSubscriptionResponse();
|
||||||
const mockResourcesResponse = createARGResourcesResponse();
|
const mockResourcesResponse = createARGResourcesResponse();
|
||||||
|
@ -2,6 +2,7 @@ import { DataSourceWithBackend, reportInteraction } from '@grafana/runtime';
|
|||||||
|
|
||||||
import { logsResourceTypes, resourceTypeDisplayNames, resourceTypes } from '../azureMetadata';
|
import { logsResourceTypes, resourceTypeDisplayNames, resourceTypes } from '../azureMetadata';
|
||||||
import AzureMonitorDatasource from '../azure_monitor/azure_monitor_datasource';
|
import AzureMonitorDatasource from '../azure_monitor/azure_monitor_datasource';
|
||||||
|
import AzureResourceGraphDatasource from '../azure_resource_graph/azure_resource_graph_datasource';
|
||||||
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types';
|
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types';
|
||||||
import {
|
import {
|
||||||
addResources,
|
addResources,
|
||||||
@ -14,18 +15,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
AzureMonitorDataSourceInstanceSettings,
|
AzureMonitorDataSourceInstanceSettings,
|
||||||
AzureMonitorDataSourceJsonData,
|
AzureMonitorDataSourceJsonData,
|
||||||
AzureGraphResponse,
|
|
||||||
AzureMonitorResource,
|
AzureMonitorResource,
|
||||||
AzureMonitorQuery,
|
AzureMonitorQuery,
|
||||||
AzureResourceGraphOptions,
|
|
||||||
AzureResourceSummaryItem,
|
AzureResourceSummaryItem,
|
||||||
RawAzureResourceGroupItem,
|
|
||||||
RawAzureResourceItem,
|
RawAzureResourceItem,
|
||||||
RawAzureSubscriptionItem,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { routeNames } from '../utils/common';
|
|
||||||
|
|
||||||
const RESOURCE_GRAPH_URL = '/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01';
|
|
||||||
|
|
||||||
const logsSupportedResourceTypesKusto = logsResourceTypes.map((v) => `"${v}"`).join(',');
|
const logsSupportedResourceTypesKusto = logsResourceTypes.map((v) => `"${v}"`).join(',');
|
||||||
|
|
||||||
@ -35,18 +29,19 @@ export default class ResourcePickerData extends DataSourceWithBackend<
|
|||||||
AzureMonitorQuery,
|
AzureMonitorQuery,
|
||||||
AzureMonitorDataSourceJsonData
|
AzureMonitorDataSourceJsonData
|
||||||
> {
|
> {
|
||||||
private resourcePath: string;
|
|
||||||
resultLimit = 200;
|
resultLimit = 200;
|
||||||
azureMonitorDatasource;
|
azureMonitorDatasource;
|
||||||
|
azureResourceGraphDatasource;
|
||||||
supportedMetricNamespaces = '';
|
supportedMetricNamespaces = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
instanceSettings: AzureMonitorDataSourceInstanceSettings,
|
instanceSettings: AzureMonitorDataSourceInstanceSettings,
|
||||||
azureMonitorDatasource: AzureMonitorDatasource
|
azureMonitorDatasource: AzureMonitorDatasource,
|
||||||
|
azureResourceGraphDatasource: AzureResourceGraphDatasource
|
||||||
) {
|
) {
|
||||||
super(instanceSettings);
|
super(instanceSettings);
|
||||||
this.resourcePath = `${routeNames.resourceGraph}`;
|
|
||||||
this.azureMonitorDatasource = azureMonitorDatasource;
|
this.azureMonitorDatasource = azureMonitorDatasource;
|
||||||
|
this.azureResourceGraphDatasource = azureResourceGraphDatasource;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchInitialRows(
|
async fetchInitialRows(
|
||||||
@ -111,7 +106,8 @@ export default class ResourcePickerData extends DataSourceWithBackend<
|
|||||||
| order by tolower(name) asc
|
| order by tolower(name) asc
|
||||||
| limit ${this.resultLimit}
|
| limit ${this.resultLimit}
|
||||||
`;
|
`;
|
||||||
const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(searchQuery);
|
const response =
|
||||||
|
await this.azureResourceGraphDatasource.pagedResourceGraphRequest<RawAzureResourceItem>(searchQuery);
|
||||||
return response.map((item) => {
|
return response.map((item) => {
|
||||||
const parsedUri = parseResourceURI(item.id);
|
const parsedUri = parseResourceURI(item.id);
|
||||||
if (!parsedUri || !(parsedUri.resourceName || parsedUri.resourceGroup || parsedUri.subscription)) {
|
if (!parsedUri || !(parsedUri.resourceName || parsedUri.resourceGroup || parsedUri.subscription)) {
|
||||||
@ -138,41 +134,14 @@ export default class ResourcePickerData extends DataSourceWithBackend<
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// private
|
|
||||||
async getSubscriptions(): Promise<ResourceRowGroup> {
|
async getSubscriptions(): Promise<ResourceRowGroup> {
|
||||||
const query = `
|
const subscriptions = await this.azureResourceGraphDatasource.getSubscriptions();
|
||||||
resources
|
|
||||||
| join kind=inner (
|
|
||||||
ResourceContainers
|
|
||||||
| where type == 'microsoft.resources/subscriptions'
|
|
||||||
| project subscriptionName=name, subscriptionURI=id, subscriptionId
|
|
||||||
) on subscriptionId
|
|
||||||
| summarize count() by subscriptionName, subscriptionURI, subscriptionId
|
|
||||||
| order by subscriptionName desc
|
|
||||||
`;
|
|
||||||
|
|
||||||
let resources: RawAzureSubscriptionItem[] = [];
|
if (!subscriptions.length) {
|
||||||
|
throw new Error('No subscriptions were found');
|
||||||
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<RawAzureSubscriptionItem[]>(query, 1, options);
|
|
||||||
if (!resourceResponse.data.length) {
|
|
||||||
throw new Error('No subscriptions were found');
|
|
||||||
}
|
|
||||||
resources = resources.concat(resourceResponse.data);
|
|
||||||
$skipToken = resourceResponse.$skipToken;
|
|
||||||
allFetched = !$skipToken;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resources.map((subscription) => ({
|
return subscriptions.map((subscription) => ({
|
||||||
name: subscription.subscriptionName,
|
name: subscription.subscriptionName,
|
||||||
id: subscription.subscriptionId,
|
id: subscription.subscriptionId,
|
||||||
uri: `/subscriptions/${subscription.subscriptionId}`,
|
uri: `/subscriptions/${subscription.subscriptionId}`,
|
||||||
@ -186,41 +155,9 @@ export default class ResourcePickerData extends DataSourceWithBackend<
|
|||||||
subscriptionId: string,
|
subscriptionId: string,
|
||||||
type: ResourcePickerQueryType
|
type: ResourcePickerQueryType
|
||||||
): Promise<ResourceRowGroup> {
|
): Promise<ResourceRowGroup> {
|
||||||
// We can use subscription ID for the filtering here as they're unique
|
const filter = await this.filterByType(type);
|
||||||
// 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
|
|
||||||
${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[] = [];
|
const resourceGroups = await this.azureResourceGraphDatasource.getResourceGroups(subscriptionId, filter);
|
||||||
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<RawAzureResourceGroupItem[]>(query, 1, options);
|
|
||||||
resourceGroups = resourceGroups.concat(resourceResponse.data);
|
|
||||||
$skipToken = resourceResponse.$skipToken;
|
|
||||||
allFetched = !$skipToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
return resourceGroups.map((r) => {
|
return resourceGroups.map((r) => {
|
||||||
const parsedUri = parseResourceURI(r.resourceGroupURI);
|
const parsedUri = parseResourceURI(r.resourceGroupURI);
|
||||||
@ -238,50 +175,20 @@ export default class ResourcePickerData extends DataSourceWithBackend<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResourcesForResourceGroup(
|
// Refactor this one out at a later date
|
||||||
resourceGroupUri: string,
|
async getResourcesForResourceGroup(uri: string, type: ResourcePickerQueryType): Promise<ResourceRowGroup> {
|
||||||
type: ResourcePickerQueryType
|
const resources = await this.azureResourceGraphDatasource.getResourceNames({ uri }, await this.filterByType(type));
|
||||||
): Promise<ResourceRowGroup> {
|
|
||||||
// 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 query = `
|
|
||||||
resources
|
|
||||||
| where id hasprefix "${resourceGroupUri}/"
|
|
||||||
${await this.filterByType(type)}
|
|
||||||
| order by tolower(name) asc`;
|
|
||||||
|
|
||||||
let resources: RawAzureResourceItem[] = [];
|
return resources.map((resource) => {
|
||||||
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');
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
name: item.name,
|
name: resource.name,
|
||||||
id: parsedUri.resourceName,
|
id: resource.name,
|
||||||
uri: item.id,
|
uri: resource.id,
|
||||||
resourceGroupName: item.resourceGroup,
|
resourceGroupName: resource.resourceGroup,
|
||||||
type: ResourceRowType.Resource,
|
type: ResourceRowType.Resource,
|
||||||
typeLabel: resourceTypeDisplayNames[item.type] || item.type,
|
typeLabel: resourceTypeDisplayNames[resource.type] || resource.type,
|
||||||
locationDisplayName: item.location,
|
locationDisplayName: resource.location,
|
||||||
location: item.location,
|
location: resource.location,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -321,7 +228,7 @@ export default class ResourcePickerData extends DataSourceWithBackend<
|
|||||||
| project subscriptionName, resourceGroupName, resourceName
|
| project subscriptionName, resourceGroupName, resourceName
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const { data: response } = await this.makeResourceGraphRequest<AzureResourceSummaryItem[]>(query);
|
const response = await this.azureResourceGraphDatasource.pagedResourceGraphRequest<AzureResourceSummaryItem>(query);
|
||||||
|
|
||||||
if (!response.length) {
|
if (!response.length) {
|
||||||
throw new Error('unable to fetch resource details');
|
throw new Error('unable to fetch resource details');
|
||||||
@ -339,7 +246,7 @@ export default class ResourcePickerData extends DataSourceWithBackend<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getResourceURIFromWorkspace(workspace: string) {
|
async getResourceURIFromWorkspace(workspace: string) {
|
||||||
const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
|
const response = await this.azureResourceGraphDatasource.pagedResourceGraphRequest<RawAzureResourceItem>(`
|
||||||
resources
|
resources
|
||||||
| where properties['customerId'] == "${workspace}"
|
| where properties['customerId'] == "${workspace}"
|
||||||
| project id
|
| project id
|
||||||
@ -352,28 +259,6 @@ export default class ResourcePickerData extends DataSourceWithBackend<
|
|||||||
return response[0].id;
|
return response[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async makeResourceGraphRequest<T = unknown>(
|
|
||||||
query: string,
|
|
||||||
maxRetries = 1,
|
|
||||||
reqOptions?: Partial<AzureResourceGraphOptions>
|
|
||||||
): Promise<AzureGraphResponse<T>> {
|
|
||||||
try {
|
|
||||||
return await this.postResource(this.resourcePath + RESOURCE_GRAPH_URL, {
|
|
||||||
query: query,
|
|
||||||
options: {
|
|
||||||
resultFormat: 'objectArray',
|
|
||||||
...reqOptions,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (maxRetries > 0) {
|
|
||||||
return this.makeResourceGraphRequest(query, maxRetries - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private filterByType = async (t: ResourcePickerQueryType) => {
|
private filterByType = async (t: ResourcePickerQueryType) => {
|
||||||
if (this.supportedMetricNamespaces === '' && t !== 'logs') {
|
if (this.supportedMetricNamespaces === '' && t !== 'logs') {
|
||||||
await this.fetchAllNamespaces();
|
await this.fetchAllNamespaces();
|
||||||
|
@ -145,11 +145,14 @@ export interface AzureResourceSummaryItem {
|
|||||||
export interface RawAzureSubscriptionItem {
|
export interface RawAzureSubscriptionItem {
|
||||||
subscriptionName: string;
|
subscriptionName: string;
|
||||||
subscriptionId: string;
|
subscriptionId: string;
|
||||||
|
subscriptionURI: string;
|
||||||
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawAzureResourceGroupItem {
|
export interface RawAzureResourceGroupItem {
|
||||||
resourceGroupURI: string;
|
resourceGroupURI: string;
|
||||||
resourceGroupName: string;
|
resourceGroupName: string;
|
||||||
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawAzureResourceItem {
|
export interface RawAzureResourceItem {
|
||||||
@ -226,10 +229,11 @@ export interface LegacyAzureGetMetricMetadataQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AzureGetResourceNamesQuery {
|
export interface AzureGetResourceNamesQuery {
|
||||||
subscriptionId: string;
|
subscriptionId?: string;
|
||||||
resourceGroup?: string;
|
resourceGroup?: string;
|
||||||
metricNamespace?: string;
|
metricNamespace?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
|
uri?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AzureMonitorLocations {
|
export interface AzureMonitorLocations {
|
||||||
|
@ -91,7 +91,7 @@ describe('VariableSupport', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can fetch resourceNames with a subscriptionId', async () => {
|
it('can fetch resourceNames with a subscriptionId', async () => {
|
||||||
const expectedResults = ['test'];
|
const expectedResults = [{ name: 'test' }];
|
||||||
const variableSupport = new VariableSupport(
|
const variableSupport = new VariableSupport(
|
||||||
createMockDatasource({
|
createMockDatasource({
|
||||||
getResourceNames: jest.fn().mockResolvedValueOnce(expectedResults),
|
getResourceNames: jest.fn().mockResolvedValueOnce(expectedResults),
|
||||||
@ -113,7 +113,7 @@ describe('VariableSupport', () => {
|
|||||||
],
|
],
|
||||||
} as DataQueryRequest<AzureMonitorQuery>;
|
} as DataQueryRequest<AzureMonitorQuery>;
|
||||||
const result = await lastValueFrom(variableSupport.query(mockRequest));
|
const result = await lastValueFrom(variableSupport.query(mockRequest));
|
||||||
expect(result.data[0].fields[0].values).toEqual(expectedResults);
|
expect(result.data[0].fields[0].values).toEqual([expectedResults[0].name]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can fetch a metricNamespace with a subscriptionId', async () => {
|
it('can fetch a metricNamespace with a subscriptionId', async () => {
|
||||||
@ -448,7 +448,7 @@ describe('VariableSupport', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can fetch resource names', async () => {
|
it('can fetch resource names', async () => {
|
||||||
const expectedResults = ['test'];
|
const expectedResults = [{ name: 'test' }];
|
||||||
const variableSupport = new VariableSupport(
|
const variableSupport = new VariableSupport(
|
||||||
createMockDatasource({
|
createMockDatasource({
|
||||||
getResourceNames: jest.fn().mockResolvedValueOnce(expectedResults),
|
getResourceNames: jest.fn().mockResolvedValueOnce(expectedResults),
|
||||||
@ -464,7 +464,7 @@ describe('VariableSupport', () => {
|
|||||||
],
|
],
|
||||||
} as DataQueryRequest<AzureMonitorQuery>;
|
} as DataQueryRequest<AzureMonitorQuery>;
|
||||||
const result = await lastValueFrom(variableSupport.query(mockRequest));
|
const result = await lastValueFrom(variableSupport.query(mockRequest));
|
||||||
expect(result.data[0].fields[0].values).toEqual(expectedResults);
|
expect(result.data[0].fields[0].values).toEqual([expectedResults[0].name]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns no data if calling resourceNames but the subscription is a template variable with no value', async () => {
|
it('returns no data if calling resourceNames but the subscription is a template variable with no value', async () => {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { startsWith } from 'lodash';
|
||||||
import { from, lastValueFrom, Observable } from 'rxjs';
|
import { from, lastValueFrom, Observable } from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -13,10 +14,26 @@ import UrlBuilder from './azure_monitor/url_builder';
|
|||||||
import VariableEditor from './components/VariableEditor/VariableEditor';
|
import VariableEditor from './components/VariableEditor/VariableEditor';
|
||||||
import DataSource from './datasource';
|
import DataSource from './datasource';
|
||||||
import { migrateQuery } from './grafanaTemplateVariableFns';
|
import { migrateQuery } from './grafanaTemplateVariableFns';
|
||||||
import { AzureMonitorQuery, AzureQueryType } from './types';
|
import { AzureMonitorQuery, AzureQueryType, RawAzureResourceItem } from './types';
|
||||||
import { GrafanaTemplateVariableQuery } from './types/templateVariables';
|
import { GrafanaTemplateVariableQuery } from './types/templateVariables';
|
||||||
import messageFromError from './utils/messageFromError';
|
import messageFromError from './utils/messageFromError';
|
||||||
|
|
||||||
|
export function parseResourceNamesAsTemplateVariable(resources: RawAzureResourceItem[], metricNamespace?: string) {
|
||||||
|
return resources.map((r) => {
|
||||||
|
if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) {
|
||||||
|
return {
|
||||||
|
text: r.name + '/default',
|
||||||
|
value: r.name + '/default',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: r.name,
|
||||||
|
value: r.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export class VariableSupport extends CustomVariableSupport<DataSource, AzureMonitorQuery> {
|
export class VariableSupport extends CustomVariableSupport<DataSource, AzureMonitorQuery> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly datasource: DataSource,
|
private readonly datasource: DataSource,
|
||||||
@ -67,14 +84,14 @@ export class VariableSupport extends CustomVariableSupport<DataSource, AzureMoni
|
|||||||
return { data: [] };
|
return { data: [] };
|
||||||
case AzureQueryType.ResourceNamesQuery:
|
case AzureQueryType.ResourceNamesQuery:
|
||||||
if (queryObj.subscription && this.hasValue(queryObj.subscription)) {
|
if (queryObj.subscription && this.hasValue(queryObj.subscription)) {
|
||||||
const rgs = await this.datasource.getResourceNames(
|
const resources = await this.datasource.getResourceNames(
|
||||||
queryObj.subscription,
|
queryObj.subscription,
|
||||||
queryObj.resourceGroup,
|
queryObj.resourceGroup,
|
||||||
queryObj.namespace,
|
queryObj.namespace,
|
||||||
queryObj.region
|
queryObj.region
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
data: rgs?.length ? [toDataFrame(rgs)] : [],
|
data: resources?.length ? [toDataFrame(parseResourceNamesAsTemplateVariable(resources))] : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { data: [] };
|
return { data: [] };
|
||||||
@ -201,15 +218,28 @@ export class VariableSupport extends CustomVariableSupport<DataSource, AzureMoni
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (query.kind === 'ResourceGroupsQuery') {
|
if (query.kind === 'ResourceGroupsQuery') {
|
||||||
return this.datasource.getResourceGroups(this.replaceVariable(query.subscription));
|
return this.datasource.getResourceGroups(this.replaceVariable(query.subscription)).then((rgs) => {
|
||||||
|
if (rgs.length > 0) {
|
||||||
|
return rgs.map((rg) => ({ text: rg.resourceGroupName, value: rg.resourceGroupName }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.kind === 'ResourceNamesQuery') {
|
if (query.kind === 'ResourceNamesQuery') {
|
||||||
return this.datasource.getResourceNames(
|
return this.datasource
|
||||||
this.replaceVariable(query.subscription),
|
.getResourceNames(
|
||||||
this.replaceVariable(query.resourceGroup),
|
this.replaceVariable(query.subscription),
|
||||||
this.replaceVariable(query.metricNamespace)
|
this.replaceVariable(query.resourceGroup),
|
||||||
);
|
this.replaceVariable(query.metricNamespace)
|
||||||
|
)
|
||||||
|
.then((resources) => {
|
||||||
|
if (resources.length > 0) {
|
||||||
|
return parseResourceNamesAsTemplateVariable(resources, query.metricNamespace);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.kind === 'MetricNamespaceQuery') {
|
if (query.kind === 'MetricNamespaceQuery') {
|
||||||
|
Reference in New Issue
Block a user