123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- import { DataSourceWithBackend } from '@grafana/runtime';
- import { DataSourceInstanceSettings } from '../../../../../../packages/grafana-data/src';
- import {
- locationDisplayNames,
- logsSupportedLocationsKusto,
- logsResourceTypes,
- resourceTypeDisplayNames,
- supportedMetricNamespaces,
- } from '../azureMetadata';
- import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types';
- import { addResources, parseResourceURI } from '../components/ResourcePicker/utils';
- import {
- AzureDataSourceJsonData,
- AzureGraphResponse,
- AzureMonitorQuery,
- AzureResourceGraphOptions,
- AzureResourceSummaryItem,
- RawAzureResourceGroupItem,
- RawAzureResourceItem,
- RawAzureSubscriptionItem,
- } 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 supportedMetricNamespacesKusto = supportedMetricNamespaces.map((v) => `"${v.toLocaleLowerCase()}"`).join(',');
- export type ResourcePickerQueryType = 'logs' | 'metrics';
- export default class ResourcePickerData extends DataSourceWithBackend<AzureMonitorQuery, AzureDataSourceJsonData> {
- private resourcePath: string;
- resultLimit = 200;
- constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) {
- super(instanceSettings);
- this.resourcePath = `${routeNames.resourceGraph}`;
- }
- async fetchInitialRows(type: ResourcePickerQueryType, currentSelection?: string): Promise<ResourceRowGroup> {
- const subscriptions = await this.getSubscriptions();
- if (!currentSelection) {
- return subscriptions;
- }
- let resources = subscriptions;
- const parsedURI = parseResourceURI(currentSelection);
- if (parsedURI) {
- const resourceGroupURI = `/subscriptions/${parsedURI.subscriptionID}/resourceGroups/${parsedURI.resourceGroup}`;
- if (parsedURI.resourceGroup) {
- const resourceGroups = await this.getResourceGroupsBySubscriptionId(parsedURI.subscriptionID, type);
- resources = addResources(resources, `/subscriptions/${parsedURI.subscriptionID}`, resourceGroups);
- }
- if (parsedURI.resource) {
- const resourcesForResourceGroup = await this.getResourcesForResourceGroup(resourceGroupURI, type);
- resources = addResources(resources, resourceGroupURI, resourcesForResourceGroup);
- }
- }
- return resources;
- }
- async fetchAndAppendNestedRow(
- rows: ResourceRowGroup,
- parentRow: ResourceRow,
- type: ResourcePickerQueryType
- ): Promise<ResourceRowGroup> {
- const nestedRows =
- parentRow.type === ResourceRowType.Subscription
- ? await this.getResourceGroupsBySubscriptionId(parentRow.id, type)
- : await this.getResourcesForResourceGroup(parentRow.id, type);
- return addResources(rows, parentRow.uri, nestedRows);
- }
- search = async (searchPhrase: string, searchType: ResourcePickerQueryType): Promise<ResourceRowGroup> => {
- let searchQuery = 'resources';
- if (searchType === 'logs') {
- searchQuery += `
- | union resourcecontainers`;
- }
- searchQuery += `
- | where id contains "${searchPhrase}"
- ${this.filterByType(searchType)}
- | order by tolower(name) asc
- | limit ${this.resultLimit}
- `;
- const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(searchQuery);
- return response.map((item) => {
- const parsedUri = parseResourceURI(item.id);
- if (!parsedUri || !(parsedUri.resource || parsedUri.resourceGroup || parsedUri.subscriptionID)) {
- throw new Error('unable to fetch resource details');
- }
- let id = parsedUri.subscriptionID;
- let type = ResourceRowType.Subscription;
- if (parsedUri.resource) {
- id = parsedUri.resource;
- type = ResourceRowType.Resource;
- } else if (parsedUri.resourceGroup) {
- id = parsedUri.resourceGroup;
- type = ResourceRowType.ResourceGroup;
- }
- return {
- name: item.name,
- id,
- uri: item.id,
- resourceGroupName: item.resourceGroup,
- type,
- typeLabel: resourceTypeDisplayNames[item.type] || item.type,
- location: locationDisplayNames[item.location] || item.location,
- };
- });
- };
- // private
- async getSubscriptions(): Promise<ResourceRowGroup> {
- const query = `
- 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[] = [];
- 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) => ({
- name: subscription.subscriptionName,
- id: subscription.subscriptionId,
- uri: `/subscriptions/${subscription.subscriptionId}`,
- typeLabel: 'Subscription',
- type: ResourceRowType.Subscription,
- children: [],
- }));
- }
- async getResourceGroupsBySubscriptionId(
- subscriptionId: string,
- type: ResourcePickerQueryType
- ): Promise<ResourceRowGroup> {
- const query = `
- resources
- | join kind=inner (
- ResourceContainers
- | where type == 'microsoft.resources/subscriptions/resourcegroups'
- | project resourceGroupURI=id, resourceGroupName=name, resourceGroup, subscriptionId
- ) on resourceGroup, subscriptionId
- ${this.filterByType(type)}
- | where subscriptionId == '${subscriptionId}'
- | summarize count() by resourceGroupName, resourceGroupURI
- | order by resourceGroupURI asc`;
- let resourceGroups: RawAzureResourceGroupItem[] = [];
- 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) => {
- const parsedUri = parseResourceURI(r.resourceGroupURI);
- if (!parsedUri || !parsedUri.resourceGroup) {
- throw new Error('unable to fetch resource groups');
- }
- return {
- name: r.resourceGroupName,
- uri: r.resourceGroupURI,
- id: parsedUri.resourceGroup,
- type: ResourceRowType.ResourceGroup,
- typeLabel: 'Resource Group',
- children: [],
- };
- });
- }
- async getResourcesForResourceGroup(
- resourceGroupId: string,
- type: ResourcePickerQueryType
- ): Promise<ResourceRowGroup> {
- const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
- resources
- | where id hasprefix "${resourceGroupId}"
- ${this.filterByType(type)} and location in (${logsSupportedLocationsKusto})
- `);
- return response.map((item) => {
- const parsedUri = parseResourceURI(item.id);
- if (!parsedUri || !parsedUri.resource) {
- throw new Error('unable to fetch resource details');
- }
- return {
- name: item.name,
- id: parsedUri.resource,
- uri: item.id,
- resourceGroupName: item.resourceGroup,
- type: ResourceRowType.Resource,
- typeLabel: resourceTypeDisplayNames[item.type] || item.type,
- location: locationDisplayNames[item.location] || item.location,
- };
- });
- }
- // used to make the select resource button that launches the resource picker show a nicer file path to users
- async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> {
- const { subscriptionID, resourceGroup, resource } = parseResourceURI(resourceURI) ?? {};
- if (!subscriptionID) {
- throw new Error('Invalid resource URI passed');
- }
- // resourceGroupURI and resourceURI could be invalid values, but that's okay because the join
- // will just silently fail as expected
- const subscriptionURI = `/subscriptions/${subscriptionID}`;
- const resourceGroupURI = `${subscriptionURI}/resourceGroups/${resourceGroup}`;
- const query = `
- resourcecontainers
- | where type == "microsoft.resources/subscriptions"
- | where id =~ "${subscriptionURI}"
- | project subscriptionName=name, subscriptionId
- | join kind=leftouter (
- resourcecontainers
- | where type == "microsoft.resources/subscriptions/resourcegroups"
- | where id =~ "${resourceGroupURI}"
- | project resourceGroupName=name, resourceGroup, subscriptionId
- ) on subscriptionId
- | join kind=leftouter (
- resources
- | where id =~ "${resourceURI}"
- | project resourceName=name, subscriptionId
- ) on subscriptionId
- | project subscriptionName, resourceGroupName, resourceName
- `;
- const { data: response } = await this.makeResourceGraphRequest<AzureResourceSummaryItem[]>(query);
- if (!response.length) {
- throw new Error('unable to fetch resource details');
- }
- const { subscriptionName, resourceGroupName, resourceName } = response[0];
- // if the name is undefined it could be because the id is undefined or because we are using a template variable.
- // Either way we can use it as a fallback. We don't really want to interpolate these variables because we want
- // to show the user when they are using template variables `$sub/$rg/$resource`
- return {
- subscriptionName: subscriptionName || subscriptionID,
- resourceGroupName: resourceGroupName || resourceGroup,
- resourceName: resourceName || resource,
- };
- }
- async getResourceURIFromWorkspace(workspace: string) {
- const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
- resources
- | where properties['customerId'] == "${workspace}"
- | project id
- `);
- if (!response.length) {
- throw new Error('unable to find resource for workspace ' + workspace);
- }
- 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 = (t: ResourcePickerQueryType) => {
- return t === 'logs'
- ? `| where type in (${logsSupportedResourceTypesKusto})`
- : `| where type in (${supportedMetricNamespacesKusto})`;
- };
- }
|