The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.test.ts

566 lines
22 KiB

import {
createARGResourcesResponse,
createMockARGResourceGroupsResponse,
createMockARGSubscriptionResponse,
} from '../__mocks__/argResourcePickerResponse';
import createMockDatasource from '../__mocks__/datasource';
import { createMockInstanceSetttings } from '../__mocks__/instanceSettings';
import { resourceTypes } from '../azureMetadata';
import AzureResourceGraphDatasource from '../azure_resource_graph/azure_resource_graph_datasource';
import { ResourceRowType } from '../components/ResourcePicker/types';
import { AzureGraphResponse } from '../types';
import ResourcePickerData from './resourcePickerData';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getTemplateSrv: () => ({
replace: (val: string) => {
return val;
},
}),
}));
const createResourcePickerData = (responses: AzureGraphResponse[], noNamespaces?: boolean) => {
const instanceSettings = createMockInstanceSetttings();
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
.fn()
.mockResolvedValueOnce(
noNamespaces ? [] : [{ text: 'Microsoft.Storage/storageAccounts', value: 'Microsoft.Storage/storageAccounts' }]
);
const resourcePickerData = new ResourcePickerData(
instanceSettings,
mockDatasource.azureMonitorDatasource,
mockDatasource.azureResourceGraphDatasource
);
return { resourcePickerData, postResource, mockDatasource };
};
describe('AzureMonitor resourcePickerData', () => {
describe('getSubscriptions', () => {
it('makes 1 call to ARG with the correct path and query arguments', async () => {
const mockResponse = createMockARGSubscriptionResponse();
const { resourcePickerData, postResource } = createResourcePickerData([mockResponse]);
await resourcePickerData.getSubscriptions();
expect(postResource).toBeCalledTimes(1);
const firstCall = postResource.mock.calls[0];
const [path, postBody] = firstCall;
expect(path).toEqual('resourcegraph/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01');
expect(postBody.query).toContain("where type == 'microsoft.resources/subscriptions'");
});
it('returns formatted subscriptions', async () => {
const mockResponse = createMockARGSubscriptionResponse();
const { resourcePickerData } = createResourcePickerData([mockResponse]);
const subscriptions = await resourcePickerData.getSubscriptions();
expect(subscriptions.length).toEqual(6);
expect(subscriptions[0]).toEqual({
id: '1',
name: 'Primary Subscription',
type: 'Subscription',
typeLabel: 'Subscription',
uri: '/subscriptions/1',
children: [],
});
});
it('makes multiple requests for subscriptions when arg returns a skipToken and passes the right skipToken to each subsequent call', async () => {
const response1 = {
...createMockARGSubscriptionResponse(),
$skipToken: 'skipfirst100',
};
const response2 = createMockARGSubscriptionResponse();
const { resourcePickerData, postResource } = createResourcePickerData([response1, response2]);
await resourcePickerData.getSubscriptions();
expect(postResource).toHaveBeenCalledTimes(2);
const secondCall = postResource.mock.calls[1];
const [_, postBody] = secondCall;
expect(postBody.options.$skipToken).toEqual('skipfirst100');
});
it('makes multiple requests for resource groups when arg returns a skipToken and passes the right skipToken to each subsequent call', async () => {
const subscriptionResponse = createMockARGSubscriptionResponse();
const resourceGroupResponse1 = {
...createMockARGResourceGroupsResponse(),
$skipToken: 'skipfirst100',
};
const resourceGroupResponse2 = createMockARGResourceGroupsResponse();
const { resourcePickerData, postResource } = createResourcePickerData([
subscriptionResponse,
resourceGroupResponse1,
resourceGroupResponse2,
]);
await resourcePickerData.getResourceGroupsBySubscriptionId('1', 'metrics');
expect(postResource).toHaveBeenCalledTimes(3);
const secondCall = postResource.mock.calls[2];
const [_, postBody] = secondCall;
expect(postBody.options.$skipToken).toEqual('skipfirst100');
});
it('makes multiple requests for resources when arg returns a skipToken and passes the right skipToken to each subsequent call', async () => {
const subscriptionResponse = createMockARGSubscriptionResponse();
const resourcesResponse1 = {
...createARGResourcesResponse(),
$skipToken: 'skipfirst100',
};
const resourcesResponse2 = createARGResourcesResponse();
const { resourcePickerData, postResource } = createResourcePickerData([
subscriptionResponse,
resourcesResponse1,
resourcesResponse2,
]);
await resourcePickerData.getResourcesForResourceGroup('resourceGroupURI', 'metrics');
expect(postResource).toHaveBeenCalledTimes(3);
const secondCall = postResource.mock.calls[2];
const [_, postBody] = secondCall;
expect(postBody.options.$skipToken).toEqual('skipfirst100');
});
it('returns a concatenates a formatted array of subscriptions when there are multiple pages from arg', async () => {
const response1 = {
...createMockARGSubscriptionResponse(),
$skipToken: 'skipfirst100',
};
const response2 = createMockARGSubscriptionResponse();
const { resourcePickerData } = createResourcePickerData([response1, response2]);
const subscriptions = await resourcePickerData.getSubscriptions();
expect(subscriptions.length).toEqual(12);
expect(subscriptions[0]).toEqual({
id: '1',
name: 'Primary Subscription',
type: 'Subscription',
typeLabel: 'Subscription',
uri: '/subscriptions/1',
children: [],
});
});
it('throws an error if it does not recieve data from arg', async () => {
const mockResponse = { data: [] };
const { resourcePickerData } = createResourcePickerData([mockResponse]);
try {
await resourcePickerData.getSubscriptions();
throw Error('expected getSubscriptions to fail but it succeeded');
} catch (err) {
if (err instanceof Error) {
expect(err.message).toEqual('No subscriptions were found');
} else {
throw err;
}
}
});
});
describe('getResourceGroupsBySubscriptionId', () => {
it('makes 1 call to ARG with the correct path and query arguments', async () => {
const mockResponse = createMockARGResourceGroupsResponse();
const { resourcePickerData, postResource } = createResourcePickerData([mockResponse]);
await resourcePickerData.getResourceGroupsBySubscriptionId('123', 'logs');
expect(postResource).toBeCalledTimes(1);
const firstCall = postResource.mock.calls[0];
const [path, postBody] = firstCall;
expect(path).toEqual('resourcegraph/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01');
expect(postBody.query).toContain(
'extend resourceGroupURI = strcat("/subscriptions/", subscriptionId, "/resourcegroups/", resourceGroup)'
);
expect(postBody.query).toContain("where subscriptionId == '123'");
});
it('returns formatted resourceGroups', async () => {
const mockResponse = createMockARGResourceGroupsResponse();
const { resourcePickerData } = createResourcePickerData([mockResponse]);
const resourceGroups = await resourcePickerData.getResourceGroupsBySubscriptionId('123', 'logs');
expect(resourceGroups.length).toEqual(6);
expect(resourceGroups[0]).toEqual({
id: 'prod',
name: 'Production',
type: 'ResourceGroup',
typeLabel: 'Resource Group',
uri: '/subscriptions/abc-123/resourceGroups/prod',
children: [],
});
});
it('makes multiple requests when it is returned a skip token', async () => {
const response1 = {
...createMockARGResourceGroupsResponse(),
$skipToken: 'skipfirst100',
};
const response2 = createMockARGResourceGroupsResponse();
const { resourcePickerData, postResource } = createResourcePickerData([response1, response2]);
await resourcePickerData.getResourceGroupsBySubscriptionId('123', 'logs');
expect(postResource).toHaveBeenCalledTimes(2);
const secondCall = postResource.mock.calls[1];
const [_, postBody] = secondCall;
expect(postBody.options.$skipToken).toEqual('skipfirst100');
});
it('returns a concatonized and formatted array of resourceGroups when there are multiple pages', async () => {
const response1 = {
...createMockARGResourceGroupsResponse(),
$skipToken: 'skipfirst100',
};
const response2 = createMockARGResourceGroupsResponse();
const { resourcePickerData } = createResourcePickerData([response1, response2]);
const resourceGroups = await resourcePickerData.getResourceGroupsBySubscriptionId('123', 'logs');
expect(resourceGroups.length).toEqual(12);
expect(resourceGroups[0]).toEqual({
id: 'prod',
name: 'Production',
type: 'ResourceGroup',
typeLabel: 'Resource Group',
uri: '/subscriptions/abc-123/resourceGroups/prod',
children: [],
});
});
it('throws an error if it recieves data with a malformed uri', async () => {
const mockResponse = {
data: [
{
resourceGroupURI: '/a-differently-formatted/uri/than/the/type/we/planned/to/parse',
resourceGroupName: 'Production',
},
],
};
const { resourcePickerData } = createResourcePickerData([mockResponse]);
try {
await resourcePickerData.getResourceGroupsBySubscriptionId('123', 'logs');
throw Error('expected getResourceGroupsBySubscriptionId to fail but it succeeded');
} catch (err) {
if (err instanceof Error) {
expect(err.message).toEqual('unable to fetch resource groups');
} else {
throw err;
}
}
});
it('filters by metric specific resources', async () => {
const mockSubscriptionsResponse = createMockARGSubscriptionResponse();
const mockResourceGroupsResponse = createMockARGResourceGroupsResponse();
const { resourcePickerData, postResource } = createResourcePickerData([
mockSubscriptionsResponse,
mockResourceGroupsResponse,
]);
await resourcePickerData.getResourceGroupsBySubscriptionId('123', 'metrics');
expect(postResource).toBeCalledTimes(2);
const secondCall = postResource.mock.calls[1];
const [_, postBody] = secondCall;
expect(postBody.query).toContain('microsoft.storage/storageaccounts');
});
});
describe('getResourcesForResourceGroup', () => {
it('makes 1 call to ARG with the correct path and query arguments', async () => {
const mockResponse = createARGResourcesResponse();
const { resourcePickerData, postResource } = createResourcePickerData([mockResponse]);
await resourcePickerData.getResourcesForResourceGroup('/subscription/sub1/resourceGroups/dev', 'logs');
expect(postResource).toBeCalledTimes(1);
const firstCall = postResource.mock.calls[0];
const [path, postBody] = firstCall;
expect(path).toEqual('resourcegraph/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01');
expect(postBody.query).toContain('resources');
expect(postBody.query).toContain('where id hasprefix "/subscription/sub1/resourceGroups/dev/"');
});
it('returns formatted resources', async () => {
const mockResponse = createARGResourcesResponse();
const { resourcePickerData } = createResourcePickerData([mockResponse]);
const resources = await resourcePickerData.getResourcesForResourceGroup('dev', 'logs');
expect(resources.length).toEqual(4);
expect(resources[0]).toEqual({
id: 'web-server',
name: 'web-server',
type: 'Resource',
location: 'northeurope',
locationDisplayName: 'northeurope',
resourceGroupName: 'dev',
typeLabel: 'Microsoft.Compute/virtualMachines',
uri: '/subscriptions/def-456/resourceGroups/dev/providers/Microsoft.Compute/virtualMachines/web-server',
});
});
it('should filter metrics resources', async () => {
const mockSubscriptionsResponse = createMockARGSubscriptionResponse();
const mockResourcesResponse = createARGResourcesResponse();
const { resourcePickerData, postResource } = createResourcePickerData([
mockSubscriptionsResponse,
mockResourcesResponse,
]);
await resourcePickerData.getResourcesForResourceGroup('dev', 'metrics');
expect(postResource).toBeCalledTimes(2);
const secondCall = postResource.mock.calls[1];
const [_, postBody] = secondCall;
expect(postBody.query).toContain('microsoft.storage/storageaccounts');
});
});
describe('search', () => {
it('makes requests for metrics searches', async () => {
const mockSubscriptionsResponse = createMockARGSubscriptionResponse();
const mockResponse = {
data: [
{
id: '/subscriptions/subId/resourceGroups/rgName/providers/Microsoft.Compute/virtualMachines/vmname',
name: 'vmName',
type: 'microsoft.compute/virtualmachines',
resourceGroup: 'rgName',
subscriptionId: 'subId',
location: 'northeurope',
},
],
};
const { resourcePickerData, postResource, mockDatasource } = createResourcePickerData([
mockSubscriptionsResponse,
mockResponse,
]);
const formattedResults = await resourcePickerData.search('vmname', 'metrics');
expect(postResource).toHaveBeenCalledTimes(2);
expect(mockDatasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith(
{
resourceUri: '/subscriptions/1',
},
false,
'westeurope'
);
expect(mockDatasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith(
{
resourceUri: '/subscriptions/1',
},
false,
'eastus'
);
expect(mockDatasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith(
{
resourceUri: '/subscriptions/1',
},
false,
'japaneast'
);
const secondCall = postResource.mock.calls[1];
const [_, postBody] = secondCall;
expect(postBody.query).not.toContain('union resourcecontainers');
expect(postBody.query).toContain('where id contains "vmname"');
expect(formattedResults[0]).toEqual({
id: 'vmname',
name: 'vmName',
type: 'Resource',
location: 'northeurope',
resourceGroupName: 'rgName',
typeLabel: 'Virtual machines',
uri: '/subscriptions/subId/resourceGroups/rgName/providers/Microsoft.Compute/virtualMachines/vmname',
});
});
it('metrics searches - fallback namespaces', async () => {
const mockSubscriptionsResponse = createMockARGSubscriptionResponse();
const mockResponse = {
data: [
{
id: '/subscriptions/subId/resourceGroups/rgName/providers/Microsoft.Compute/virtualMachines/vmname',
name: 'vmName',
type: 'microsoft.compute/virtualmachines',
resourceGroup: 'rgName',
subscriptionId: 'subId',
location: 'northeurope',
},
],
};
const { resourcePickerData, postResource, mockDatasource } = createResourcePickerData(
[mockSubscriptionsResponse, mockResponse],
true
);
await resourcePickerData.search('vmname', 'metrics');
expect(postResource).toHaveBeenCalledTimes(2);
expect(mockDatasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith(
{
resourceUri: '/subscriptions/1',
},
false,
'westeurope'
);
expect(mockDatasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith(
{
resourceUri: '/subscriptions/1',
},
false,
'eastus'
);
expect(mockDatasource.azureMonitorDatasource.getMetricNamespaces).toHaveBeenCalledWith(
{
resourceUri: '/subscriptions/1',
},
false,
'japaneast'
);
const secondCall = postResource.mock.calls[1];
const [_, postBody] = secondCall;
const namespaces = (postBody.query as string).match(/\((.*?)\)/)?.[0].split(',');
expect(namespaces?.length).toEqual(resourceTypes.length);
});
it('makes requests for logs searches', async () => {
const mockResponse = {
data: [
{
id: '/subscriptions/subId/resourceGroups/rgName',
name: 'rgName',
type: 'microsoft.resources/subscriptions/resourcegroups',
resourceGroup: 'rgName',
subscriptionId: 'subId',
location: 'northeurope',
},
],
};
const { resourcePickerData, postResource } = createResourcePickerData([mockResponse]);
const formattedResults = await resourcePickerData.search('rgName', 'logs');
expect(postResource).toBeCalledTimes(1);
const firstCall = postResource.mock.calls[0];
const [_, postBody] = firstCall;
expect(postBody.query).toContain('union resourcecontainers');
expect(formattedResults[0]).toEqual({
id: 'rgName',
name: 'rgName',
type: 'ResourceGroup',
location: 'northeurope',
resourceGroupName: 'rgName',
typeLabel: 'Resource groups',
uri: '/subscriptions/subId/resourceGroups/rgName',
});
});
it('throws an error if it receives data it can not parse', 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.search('dev', 'logs');
throw Error('expected search test to fail but it succeeded');
} catch (err) {
if (err instanceof Error) {
expect(err.message).toEqual('unable to fetch resource details');
} else {
throw err;
}
}
});
});
describe('fetchInitialRows', () => {
it('returns a list of subscriptions', async () => {
const { resourcePickerData } = createResourcePickerData([createMockARGSubscriptionResponse()]);
const rows = await resourcePickerData.fetchInitialRows('logs');
expect(rows.length).toEqual(createMockARGSubscriptionResponse().data.length);
});
it('fetches resource groups and resources', async () => {
const { resourcePickerData } = createResourcePickerData([createMockARGSubscriptionResponse()]);
resourcePickerData.getResourceGroupsBySubscriptionId = jest
.fn()
.mockResolvedValue([{ id: 'rg1', uri: '/subscriptions/1/resourceGroups/rg1' }]);
resourcePickerData.getResourcesForResourceGroup = jest.fn().mockResolvedValue([
{ id: 'vm1', uri: '/subscriptions/1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1' },
{ id: 'vm2', uri: '/subscriptions/1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm2' },
]);
const rows = await resourcePickerData.fetchInitialRows('logs', [
{
subscription: '1',
resourceGroup: 'rg1',
resourceName: 'vm1',
metricNamespace: 'Microsoft.Compute/virtualMachines',
},
{
subscription: '1',
resourceGroup: 'rg1',
resourceName: 'vm2',
metricNamespace: 'Microsoft.Compute/virtualMachines',
},
]);
expect(rows[0]).toMatchObject({
id: '1',
children: [
{
id: 'rg1',
children: [{ id: 'vm1' }, { id: 'vm2' }],
},
],
});
// getResourceGroupsBySubscriptionId should only be called once because the subscription
// of both resources is the same
expect(resourcePickerData.getResourceGroupsBySubscriptionId).toBeCalledTimes(1);
// getResourcesForResourceGroup should only be called once because the resource group
// of both resources is the same
expect(resourcePickerData.getResourcesForResourceGroup).toBeCalledTimes(1);
});
});
describe('parseRows', () => {
[
{
input: '/subscriptions/def-456/resourceGroups/dev/providers/Microsoft.Compute/virtualMachines/web-server',
expected: {
id: 'web-server',
name: 'web-server',
type: ResourceRowType.Resource,
uri: '/subscriptions/def-456/resourceGroups/dev/providers/Microsoft.Compute/virtualMachines/web-server',
typeLabel: 'Virtual machines',
},
},
{
input: {
subscription: 'def-456',
},
expected: {
id: 'def-456',
name: 'def-456',
type: ResourceRowType.Subscription,
uri: '/subscriptions/def-456',
typeLabel: '',
},
},
].forEach(({ input, expected }) => {
const { resourcePickerData } = createResourcePickerData([]);
expect(resourcePickerData.parseRows([input])[0]).toMatchObject(expected);
});
});
});