import { FetchResponse, getBackendSrv } from '@grafana/runtime'; import { getManagementApiRoute } from '../api/routes'; import { locationDisplayNames, logsSupportedLocationsKusto, logsSupportedResourceTypesKusto, resourceTypeDisplayNames, } from '../azureMetadata'; import { ResourceRowType, ResourceRow, ResourceRowGroup } from '../components/ResourcePicker/types'; import { parseResourceURI } from '../components/ResourcePicker/utils'; import { getAzureCloud } from '../credentials'; import { AzureDataSourceInstanceSettings, AzureGraphResponse, AzureResourceSummaryItem, RawAzureResourceGroupItem, RawAzureResourceItem, } from '../types'; const RESOURCE_GRAPH_URL = '/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01'; export default class ResourcePickerData { private proxyUrl: string; private cloud: string; constructor(instanceSettings: AzureDataSourceInstanceSettings) { this.proxyUrl = instanceSettings.url!; this.cloud = getAzureCloud(instanceSettings); } static readonly templateVariableGroupID = '$$grafana-templateVariables$$'; async getResourcePickerData() { const query = ` resources // Put subscription details on each row | join kind=leftouter ( ResourceContainers | where type == 'microsoft.resources/subscriptions' | project subscriptionName=name, subscriptionURI=id, subscriptionId ) on subscriptionId // Put resource group details on each row | join kind=leftouter ( ResourceContainers | where type == 'microsoft.resources/subscriptions/resourcegroups' | project resourceGroupURI=id, resourceGroupName=name, resourceGroup ) on resourceGroup | where type in (${logsSupportedResourceTypesKusto}) // Get only unique resource groups and subscriptions. Also acts like a project | summarize count() by resourceGroupName, resourceGroupURI, subscriptionName, subscriptionURI | order by subscriptionURI asc `; const { ok, data: response } = await this.makeResourceGraphRequest(query); // TODO: figure out desired error handling strategy if (!ok) { throw new Error('unable to fetch resource containers'); } return formatResourceGroupData(response.data); } async getResourcesForResourceGroup(resourceGroup: ResourceRow) { const { ok, data: response } = await this.makeResourceGraphRequest(` resources | where id hasprefix "${resourceGroup.id}" | where type in (${logsSupportedResourceTypesKusto}) and location in (${logsSupportedLocationsKusto}) `); // TODO: figure out desired error handling strategy if (!ok) { throw new Error('unable to fetch resource containers'); } return formatResourceGroupChildren(response.data); } async getResourceURIDisplayProperties(resourceURI: string): Promise { const { subscriptionID, resourceGroup } = 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 { ok, data: response } = await this.makeResourceGraphRequest(query); if (!ok || !response.data[0]) { throw new Error('unable to fetch resource details'); } return response.data[0]; } async getResourceURIFromWorkspace(workspace: string) { const { ok, data: response } = await this.makeResourceGraphRequest(` resources | where properties['customerId'] == "${workspace}" | project id `); // TODO: figure out desired error handling strategy if (!ok) { throw new Error('unable to fetch resource containers'); } if (!response.data.length) { throw new Error('unable to find resource for workspace ' + workspace); } return response.data[0].id; } async makeResourceGraphRequest( query: string, maxRetries = 1 ): Promise>> { try { return await getBackendSrv() .fetch>({ url: this.proxyUrl + '/' + getManagementApiRoute(this.cloud) + RESOURCE_GRAPH_URL, method: 'POST', data: { query: query, options: { resultFormat: 'objectArray', }, }, }) .toPromise(); } catch (error) { if (maxRetries > 0) { return this.makeResourceGraphRequest(query, maxRetries - 1); } throw error; } } transformVariablesToRow(templateVariables: string[]): ResourceRow { return { id: ResourcePickerData.templateVariableGroupID, name: 'Template variables', type: ResourceRowType.VariableGroup, typeLabel: 'Variables', children: templateVariables.map((v) => ({ id: v, name: v, type: ResourceRowType.Variable, typeLabel: 'Variable', })), }; } } function formatResourceGroupData(rawData: RawAzureResourceGroupItem[]) { // Subscriptions goes into the top level array const rows: ResourceRowGroup = []; // Array of all the resource groups, with subscription data on each row for (const row of rawData) { const resourceGroupRow: ResourceRow = { name: row.resourceGroupName, id: row.resourceGroupURI, type: ResourceRowType.ResourceGroup, typeLabel: 'Resource Group', children: [], }; const subscription = rows.find((v) => v.id === row.subscriptionURI); if (subscription) { if (!subscription.children) { subscription.children = []; } subscription.children.push(resourceGroupRow); } else { const newSubscriptionRow = { name: row.subscriptionName, id: row.subscriptionURI, typeLabel: 'Subscription', type: ResourceRowType.Subscription, children: [resourceGroupRow], }; rows.push(newSubscriptionRow); } } return rows; } function formatResourceGroupChildren(rawData: RawAzureResourceItem[]): ResourceRowGroup { return rawData.map((item) => ({ name: item.name, id: item.id, resourceGroupName: item.resourceGroup, type: ResourceRowType.Resource, typeLabel: resourceTypeDisplayNames[item.type] || item.type, location: locationDisplayNames[item.location] || item.location, })); }