1import { DataSourceWithBackend } from '@grafana/runtime'; 2import { DataSourceInstanceSettings } from '../../../../../../packages/grafana-data/src'; 3import { 4 locationDisplayNames, 5 logsSupportedLocationsKusto, 6 logsSupportedResourceTypesKusto, 7 resourceTypeDisplayNames, 8} from '../azureMetadata'; 9import { ResourceRowType, ResourceRow, ResourceRowGroup } from '../components/ResourcePicker/types'; 10import { parseResourceURI } from '../components/ResourcePicker/utils'; 11import { 12 AzureDataSourceJsonData, 13 AzureGraphResponse, 14 AzureMonitorQuery, 15 AzureResourceGraphOptions, 16 AzureResourceSummaryItem, 17 RawAzureResourceGroupItem, 18 RawAzureResourceItem, 19} from '../types'; 20import { routeNames } from '../utils/common'; 21 22const RESOURCE_GRAPH_URL = '/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01'; 23 24export default class ResourcePickerData extends DataSourceWithBackend<AzureMonitorQuery, AzureDataSourceJsonData> { 25 private resourcePath: string; 26 27 constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) { 28 super(instanceSettings); 29 this.resourcePath = `${routeNames.resourceGraph}`; 30 } 31 32 static readonly templateVariableGroupID = '$$grafana-templateVariables$$'; 33 34 async getResourcePickerData() { 35 const query = ` 36 resources 37 // Put subscription details on each row 38 | join kind=leftouter ( 39 ResourceContainers 40 | where type == 'microsoft.resources/subscriptions' 41 | project subscriptionName=name, subscriptionURI=id, subscriptionId 42 ) on subscriptionId 43 44 // Put resource group details on each row 45 | join kind=leftouter ( 46 ResourceContainers 47 | where type == 'microsoft.resources/subscriptions/resourcegroups' 48 | project resourceGroupURI=id, resourceGroupName=name, resourceGroup, subscriptionId 49 ) on resourceGroup, subscriptionId 50 51 | where type in (${logsSupportedResourceTypesKusto}) 52 53 // Get only unique resource groups and subscriptions. Also acts like a project 54 | summarize count() by resourceGroupName, resourceGroupURI, subscriptionName, subscriptionURI 55 | order by subscriptionURI asc 56 `; 57 58 let resources: RawAzureResourceGroupItem[] = []; 59 let allFetched = false; 60 let $skipToken = undefined; 61 while (!allFetched) { 62 // The response may include several pages 63 let options: Partial<AzureResourceGraphOptions> = {}; 64 if ($skipToken) { 65 options = { 66 $skipToken, 67 }; 68 } 69 const resourceResponse = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(query, 1, options); 70 if (!resourceResponse.data.length) { 71 throw new Error('unable to fetch resource details'); 72 } 73 resources = resources.concat(resourceResponse.data); 74 $skipToken = resourceResponse.$skipToken; 75 allFetched = !$skipToken; 76 } 77 78 return formatResourceGroupData(resources); 79 } 80 81 async getResourcesForResourceGroup(resourceGroup: ResourceRow) { 82 const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(` 83 resources 84 | where id hasprefix "${resourceGroup.id}" 85 | where type in (${logsSupportedResourceTypesKusto}) and location in (${logsSupportedLocationsKusto}) 86 `); 87 88 return formatResourceGroupChildren(response); 89 } 90 91 async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> { 92 const { subscriptionID, resourceGroup } = parseResourceURI(resourceURI) ?? {}; 93 94 if (!subscriptionID) { 95 throw new Error('Invalid resource URI passed'); 96 } 97 98 // resourceGroupURI and resourceURI could be invalid values, but that's okay because the join 99 // will just silently fail as expected 100 const subscriptionURI = `/subscriptions/${subscriptionID}`; 101 const resourceGroupURI = `${subscriptionURI}/resourceGroups/${resourceGroup}`; 102 103 const query = ` 104 resourcecontainers 105 | where type == "microsoft.resources/subscriptions" 106 | where id =~ "${subscriptionURI}" 107 | project subscriptionName=name, subscriptionId 108 109 | join kind=leftouter ( 110 resourcecontainers 111 | where type == "microsoft.resources/subscriptions/resourcegroups" 112 | where id =~ "${resourceGroupURI}" 113 | project resourceGroupName=name, resourceGroup, subscriptionId 114 ) on subscriptionId 115 116 | join kind=leftouter ( 117 resources 118 | where id =~ "${resourceURI}" 119 | project resourceName=name, subscriptionId 120 ) on subscriptionId 121 122 | project subscriptionName, resourceGroupName, resourceName 123 `; 124 125 const { data: response } = await this.makeResourceGraphRequest<AzureResourceSummaryItem[]>(query); 126 127 if (!response.length) { 128 throw new Error('unable to fetch resource details'); 129 } 130 131 return response[0]; 132 } 133 134 async getResourceURIFromWorkspace(workspace: string) { 135 const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(` 136 resources 137 | where properties['customerId'] == "${workspace}" 138 | project id 139 `); 140 141 if (!response.length) { 142 throw new Error('unable to find resource for workspace ' + workspace); 143 } 144 145 return response[0].id; 146 } 147 148 async makeResourceGraphRequest<T = unknown>( 149 query: string, 150 maxRetries = 1, 151 reqOptions?: Partial<AzureResourceGraphOptions> 152 ): Promise<AzureGraphResponse<T>> { 153 try { 154 return await this.postResource(this.resourcePath + RESOURCE_GRAPH_URL, { 155 query: query, 156 options: { 157 resultFormat: 'objectArray', 158 ...reqOptions, 159 }, 160 }); 161 } catch (error) { 162 if (maxRetries > 0) { 163 return this.makeResourceGraphRequest(query, maxRetries - 1); 164 } 165 166 throw error; 167 } 168 } 169 170 transformVariablesToRow(templateVariables: string[]): ResourceRow { 171 return { 172 id: ResourcePickerData.templateVariableGroupID, 173 name: 'Template variables', 174 type: ResourceRowType.VariableGroup, 175 typeLabel: 'Variables', 176 children: templateVariables.map((v) => ({ 177 id: v, 178 name: v, 179 type: ResourceRowType.Variable, 180 typeLabel: 'Variable', 181 })), 182 }; 183 } 184} 185 186function formatResourceGroupData(rawData: RawAzureResourceGroupItem[]) { 187 // Subscriptions goes into the top level array 188 const rows: ResourceRowGroup = []; 189 190 // Array of all the resource groups, with subscription data on each row 191 for (const row of rawData) { 192 const resourceGroupRow: ResourceRow = { 193 name: row.resourceGroupName, 194 id: row.resourceGroupURI, 195 type: ResourceRowType.ResourceGroup, 196 typeLabel: 'Resource Group', 197 children: [], 198 }; 199 200 const subscription = rows.find((v) => v.id === row.subscriptionURI); 201 202 if (subscription) { 203 if (!subscription.children) { 204 subscription.children = []; 205 } 206 207 subscription.children.push(resourceGroupRow); 208 } else { 209 const newSubscriptionRow = { 210 name: row.subscriptionName, 211 id: row.subscriptionURI, 212 typeLabel: 'Subscription', 213 type: ResourceRowType.Subscription, 214 children: [resourceGroupRow], 215 }; 216 217 rows.push(newSubscriptionRow); 218 } 219 } 220 221 return rows; 222} 223 224function formatResourceGroupChildren(rawData: RawAzureResourceItem[]): ResourceRowGroup { 225 return rawData.map((item) => ({ 226 name: item.name, 227 id: item.id, 228 resourceGroupName: item.resourceGroup, 229 type: ResourceRowType.Resource, 230 typeLabel: resourceTypeDisplayNames[item.type] || item.type, 231 location: locationDisplayNames[item.location] || item.location, 232 })); 233} 234