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