resourcePickerData.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import { DataSourceWithBackend } from '@grafana/runtime';
  2. import { DataSourceInstanceSettings } from '../../../../../../packages/grafana-data/src';
  3. import {
  4. locationDisplayNames,
  5. logsSupportedLocationsKusto,
  6. logsResourceTypes,
  7. resourceTypeDisplayNames,
  8. supportedMetricNamespaces,
  9. } from '../azureMetadata';
  10. import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types';
  11. import { addResources, parseResourceURI } from '../components/ResourcePicker/utils';
  12. import {
  13. AzureDataSourceJsonData,
  14. AzureGraphResponse,
  15. AzureMonitorQuery,
  16. AzureResourceGraphOptions,
  17. AzureResourceSummaryItem,
  18. RawAzureResourceGroupItem,
  19. RawAzureResourceItem,
  20. RawAzureSubscriptionItem,
  21. } from '../types';
  22. import { routeNames } from '../utils/common';
  23. const RESOURCE_GRAPH_URL = '/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01';
  24. const logsSupportedResourceTypesKusto = logsResourceTypes.map((v) => `"${v}"`).join(',');
  25. const supportedMetricNamespacesKusto = supportedMetricNamespaces.map((v) => `"${v.toLocaleLowerCase()}"`).join(',');
  26. export type ResourcePickerQueryType = 'logs' | 'metrics';
  27. export default class ResourcePickerData extends DataSourceWithBackend<AzureMonitorQuery, AzureDataSourceJsonData> {
  28. private resourcePath: string;
  29. resultLimit = 200;
  30. constructor(instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>) {
  31. super(instanceSettings);
  32. this.resourcePath = `${routeNames.resourceGraph}`;
  33. }
  34. async fetchInitialRows(type: ResourcePickerQueryType, currentSelection?: string): Promise<ResourceRowGroup> {
  35. const subscriptions = await this.getSubscriptions();
  36. if (!currentSelection) {
  37. return subscriptions;
  38. }
  39. let resources = subscriptions;
  40. const parsedURI = parseResourceURI(currentSelection);
  41. if (parsedURI) {
  42. const resourceGroupURI = `/subscriptions/${parsedURI.subscriptionID}/resourceGroups/${parsedURI.resourceGroup}`;
  43. if (parsedURI.resourceGroup) {
  44. const resourceGroups = await this.getResourceGroupsBySubscriptionId(parsedURI.subscriptionID, type);
  45. resources = addResources(resources, `/subscriptions/${parsedURI.subscriptionID}`, resourceGroups);
  46. }
  47. if (parsedURI.resource) {
  48. const resourcesForResourceGroup = await this.getResourcesForResourceGroup(resourceGroupURI, type);
  49. resources = addResources(resources, resourceGroupURI, resourcesForResourceGroup);
  50. }
  51. }
  52. return resources;
  53. }
  54. async fetchAndAppendNestedRow(
  55. rows: ResourceRowGroup,
  56. parentRow: ResourceRow,
  57. type: ResourcePickerQueryType
  58. ): Promise<ResourceRowGroup> {
  59. const nestedRows =
  60. parentRow.type === ResourceRowType.Subscription
  61. ? await this.getResourceGroupsBySubscriptionId(parentRow.id, type)
  62. : await this.getResourcesForResourceGroup(parentRow.id, type);
  63. return addResources(rows, parentRow.uri, nestedRows);
  64. }
  65. search = async (searchPhrase: string, searchType: ResourcePickerQueryType): Promise<ResourceRowGroup> => {
  66. let searchQuery = 'resources';
  67. if (searchType === 'logs') {
  68. searchQuery += `
  69. | union resourcecontainers`;
  70. }
  71. searchQuery += `
  72. | where id contains "${searchPhrase}"
  73. ${this.filterByType(searchType)}
  74. | order by tolower(name) asc
  75. | limit ${this.resultLimit}
  76. `;
  77. const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(searchQuery);
  78. return response.map((item) => {
  79. const parsedUri = parseResourceURI(item.id);
  80. if (!parsedUri || !(parsedUri.resource || parsedUri.resourceGroup || parsedUri.subscriptionID)) {
  81. throw new Error('unable to fetch resource details');
  82. }
  83. let id = parsedUri.subscriptionID;
  84. let type = ResourceRowType.Subscription;
  85. if (parsedUri.resource) {
  86. id = parsedUri.resource;
  87. type = ResourceRowType.Resource;
  88. } else if (parsedUri.resourceGroup) {
  89. id = parsedUri.resourceGroup;
  90. type = ResourceRowType.ResourceGroup;
  91. }
  92. return {
  93. name: item.name,
  94. id,
  95. uri: item.id,
  96. resourceGroupName: item.resourceGroup,
  97. type,
  98. typeLabel: resourceTypeDisplayNames[item.type] || item.type,
  99. location: locationDisplayNames[item.location] || item.location,
  100. };
  101. });
  102. };
  103. // private
  104. async getSubscriptions(): Promise<ResourceRowGroup> {
  105. const query = `
  106. resources
  107. | join kind=inner (
  108. ResourceContainers
  109. | where type == 'microsoft.resources/subscriptions'
  110. | project subscriptionName=name, subscriptionURI=id, subscriptionId
  111. ) on subscriptionId
  112. | summarize count() by subscriptionName, subscriptionURI, subscriptionId
  113. | order by subscriptionName desc
  114. `;
  115. let resources: RawAzureSubscriptionItem[] = [];
  116. let allFetched = false;
  117. let $skipToken = undefined;
  118. while (!allFetched) {
  119. // The response may include several pages
  120. let options: Partial<AzureResourceGraphOptions> = {};
  121. if ($skipToken) {
  122. options = {
  123. $skipToken,
  124. };
  125. }
  126. const resourceResponse = await this.makeResourceGraphRequest<RawAzureSubscriptionItem[]>(query, 1, options);
  127. if (!resourceResponse.data.length) {
  128. throw new Error('No subscriptions were found');
  129. }
  130. resources = resources.concat(resourceResponse.data);
  131. $skipToken = resourceResponse.$skipToken;
  132. allFetched = !$skipToken;
  133. }
  134. return resources.map((subscription) => ({
  135. name: subscription.subscriptionName,
  136. id: subscription.subscriptionId,
  137. uri: `/subscriptions/${subscription.subscriptionId}`,
  138. typeLabel: 'Subscription',
  139. type: ResourceRowType.Subscription,
  140. children: [],
  141. }));
  142. }
  143. async getResourceGroupsBySubscriptionId(
  144. subscriptionId: string,
  145. type: ResourcePickerQueryType
  146. ): Promise<ResourceRowGroup> {
  147. const query = `
  148. resources
  149. | join kind=inner (
  150. ResourceContainers
  151. | where type == 'microsoft.resources/subscriptions/resourcegroups'
  152. | project resourceGroupURI=id, resourceGroupName=name, resourceGroup, subscriptionId
  153. ) on resourceGroup, subscriptionId
  154. ${this.filterByType(type)}
  155. | where subscriptionId == '${subscriptionId}'
  156. | summarize count() by resourceGroupName, resourceGroupURI
  157. | order by resourceGroupURI asc`;
  158. let resourceGroups: RawAzureResourceGroupItem[] = [];
  159. let allFetched = false;
  160. let $skipToken = undefined;
  161. while (!allFetched) {
  162. // The response may include several pages
  163. let options: Partial<AzureResourceGraphOptions> = {};
  164. if ($skipToken) {
  165. options = {
  166. $skipToken,
  167. };
  168. }
  169. const resourceResponse = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(query, 1, options);
  170. resourceGroups = resourceGroups.concat(resourceResponse.data);
  171. $skipToken = resourceResponse.$skipToken;
  172. allFetched = !$skipToken;
  173. }
  174. return resourceGroups.map((r) => {
  175. const parsedUri = parseResourceURI(r.resourceGroupURI);
  176. if (!parsedUri || !parsedUri.resourceGroup) {
  177. throw new Error('unable to fetch resource groups');
  178. }
  179. return {
  180. name: r.resourceGroupName,
  181. uri: r.resourceGroupURI,
  182. id: parsedUri.resourceGroup,
  183. type: ResourceRowType.ResourceGroup,
  184. typeLabel: 'Resource Group',
  185. children: [],
  186. };
  187. });
  188. }
  189. async getResourcesForResourceGroup(
  190. resourceGroupId: string,
  191. type: ResourcePickerQueryType
  192. ): Promise<ResourceRowGroup> {
  193. const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
  194. resources
  195. | where id hasprefix "${resourceGroupId}"
  196. ${this.filterByType(type)} and location in (${logsSupportedLocationsKusto})
  197. `);
  198. return response.map((item) => {
  199. const parsedUri = parseResourceURI(item.id);
  200. if (!parsedUri || !parsedUri.resource) {
  201. throw new Error('unable to fetch resource details');
  202. }
  203. return {
  204. name: item.name,
  205. id: parsedUri.resource,
  206. uri: item.id,
  207. resourceGroupName: item.resourceGroup,
  208. type: ResourceRowType.Resource,
  209. typeLabel: resourceTypeDisplayNames[item.type] || item.type,
  210. location: locationDisplayNames[item.location] || item.location,
  211. };
  212. });
  213. }
  214. // used to make the select resource button that launches the resource picker show a nicer file path to users
  215. async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> {
  216. const { subscriptionID, resourceGroup, resource } = parseResourceURI(resourceURI) ?? {};
  217. if (!subscriptionID) {
  218. throw new Error('Invalid resource URI passed');
  219. }
  220. // resourceGroupURI and resourceURI could be invalid values, but that's okay because the join
  221. // will just silently fail as expected
  222. const subscriptionURI = `/subscriptions/${subscriptionID}`;
  223. const resourceGroupURI = `${subscriptionURI}/resourceGroups/${resourceGroup}`;
  224. const query = `
  225. resourcecontainers
  226. | where type == "microsoft.resources/subscriptions"
  227. | where id =~ "${subscriptionURI}"
  228. | project subscriptionName=name, subscriptionId
  229. | join kind=leftouter (
  230. resourcecontainers
  231. | where type == "microsoft.resources/subscriptions/resourcegroups"
  232. | where id =~ "${resourceGroupURI}"
  233. | project resourceGroupName=name, resourceGroup, subscriptionId
  234. ) on subscriptionId
  235. | join kind=leftouter (
  236. resources
  237. | where id =~ "${resourceURI}"
  238. | project resourceName=name, subscriptionId
  239. ) on subscriptionId
  240. | project subscriptionName, resourceGroupName, resourceName
  241. `;
  242. const { data: response } = await this.makeResourceGraphRequest<AzureResourceSummaryItem[]>(query);
  243. if (!response.length) {
  244. throw new Error('unable to fetch resource details');
  245. }
  246. const { subscriptionName, resourceGroupName, resourceName } = response[0];
  247. // if the name is undefined it could be because the id is undefined or because we are using a template variable.
  248. // Either way we can use it as a fallback. We don't really want to interpolate these variables because we want
  249. // to show the user when they are using template variables `$sub/$rg/$resource`
  250. return {
  251. subscriptionName: subscriptionName || subscriptionID,
  252. resourceGroupName: resourceGroupName || resourceGroup,
  253. resourceName: resourceName || resource,
  254. };
  255. }
  256. async getResourceURIFromWorkspace(workspace: string) {
  257. const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
  258. resources
  259. | where properties['customerId'] == "${workspace}"
  260. | project id
  261. `);
  262. if (!response.length) {
  263. throw new Error('unable to find resource for workspace ' + workspace);
  264. }
  265. return response[0].id;
  266. }
  267. async makeResourceGraphRequest<T = unknown>(
  268. query: string,
  269. maxRetries = 1,
  270. reqOptions?: Partial<AzureResourceGraphOptions>
  271. ): Promise<AzureGraphResponse<T>> {
  272. try {
  273. return await this.postResource(this.resourcePath + RESOURCE_GRAPH_URL, {
  274. query: query,
  275. options: {
  276. resultFormat: 'objectArray',
  277. ...reqOptions,
  278. },
  279. });
  280. } catch (error) {
  281. if (maxRetries > 0) {
  282. return this.makeResourceGraphRequest(query, maxRetries - 1);
  283. }
  284. throw error;
  285. }
  286. }
  287. private filterByType = (t: ResourcePickerQueryType) => {
  288. return t === 'logs'
  289. ? `| where type in (${logsSupportedResourceTypesKusto})`
  290. : `| where type in (${supportedMetricNamespacesKusto})`;
  291. };
  292. }