mirror of https://github.com/grafana/grafana.git
405 lines
14 KiB
TypeScript
405 lines
14 KiB
TypeScript
import { uniq } from 'lodash';
|
|
|
|
import { DataSourceWithBackend, reportInteraction } from '@grafana/runtime';
|
|
|
|
import { logsResourceTypes } from '../azureMetadata/logsResourceTypes';
|
|
import { resourceTypeDisplayNames, resourceTypes } from '../azureMetadata/resourceTypes';
|
|
import AzureMonitorDatasource from '../azure_monitor/azure_monitor_datasource';
|
|
import AzureResourceGraphDatasource from '../azure_resource_graph/azure_resource_graph_datasource';
|
|
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types';
|
|
import {
|
|
addResources,
|
|
findRow,
|
|
parseMultipleResourceDetails,
|
|
parseResourceDetails,
|
|
parseResourceURI,
|
|
resourceToString,
|
|
} from '../components/ResourcePicker/utils';
|
|
import { AzureMonitorQuery, AzureMonitorResource } from '../types/query';
|
|
import {
|
|
AzureMonitorDataSourceInstanceSettings,
|
|
AzureMonitorDataSourceJsonData,
|
|
AzureResourceSummaryItem,
|
|
RawAzureResourceItem,
|
|
ResourceGraphFilters,
|
|
} from '../types/types';
|
|
|
|
const logsSupportedResourceTypesKusto = logsResourceTypes.map((v) => `"${v}"`).join(',');
|
|
|
|
export type ResourcePickerQueryType = 'logs' | 'metrics' | 'traces';
|
|
|
|
export default class ResourcePickerData extends DataSourceWithBackend<
|
|
AzureMonitorQuery,
|
|
AzureMonitorDataSourceJsonData
|
|
> {
|
|
resultLimit = 200;
|
|
azureMonitorDatasource;
|
|
azureResourceGraphDatasource;
|
|
supportedMetricNamespaces = '';
|
|
|
|
constructor(
|
|
instanceSettings: AzureMonitorDataSourceInstanceSettings,
|
|
azureMonitorDatasource: AzureMonitorDatasource,
|
|
azureResourceGraphDatasource: AzureResourceGraphDatasource
|
|
) {
|
|
super(instanceSettings);
|
|
this.azureMonitorDatasource = azureMonitorDatasource;
|
|
this.azureResourceGraphDatasource = azureResourceGraphDatasource;
|
|
}
|
|
|
|
async fetchInitialRows(
|
|
type: ResourcePickerQueryType,
|
|
currentSelection?: AzureMonitorResource[],
|
|
filters?: ResourceGraphFilters
|
|
): Promise<ResourceRowGroup> {
|
|
try {
|
|
const subscriptions = await this.getSubscriptions(filters);
|
|
|
|
if (!currentSelection) {
|
|
return subscriptions;
|
|
}
|
|
|
|
const rgUriOf = (s: AzureMonitorResource) => `/subscriptions/${s.subscription}/resourceGroups/${s.resourceGroup}`;
|
|
|
|
const resUriOf = (s: AzureMonitorResource) => resourceToString(s);
|
|
|
|
const hasSubAndRG = (
|
|
s: AzureMonitorResource
|
|
): s is AzureMonitorResource & { subscription: string; resourceGroup: string } =>
|
|
Boolean(s.subscription && s.resourceGroup);
|
|
|
|
const subsToFetch = uniq(
|
|
currentSelection
|
|
.filter(hasSubAndRG)
|
|
.filter((s) => !findRow(subscriptions, rgUriOf(s)))
|
|
.map(({ subscription }) => subscription)
|
|
);
|
|
|
|
const rgUrisToFetch = uniq(
|
|
currentSelection
|
|
.filter((s) => s.subscription && s.resourceGroup && s.resourceName && !findRow(subscriptions, resUriOf(s)))
|
|
.map((s) => rgUriOf(s))
|
|
);
|
|
|
|
const [groupsResults, resourcesResults] = await Promise.all([
|
|
Promise.all(
|
|
subsToFetch.map(async (sub) => [sub, await this.getResourceGroupsBySubscriptionId(sub, type)] as const)
|
|
),
|
|
Promise.all(
|
|
rgUrisToFetch.map(async (rgUri) => [rgUri, await this.getResourcesForResourceGroup(rgUri, type)] as const)
|
|
),
|
|
]);
|
|
|
|
const withGroups = groupsResults.reduce<ResourceRowGroup>(
|
|
(acc, [sub, rgs]) => addResources(acc, `/subscriptions/${sub}`, rgs),
|
|
subscriptions
|
|
);
|
|
|
|
return resourcesResults.reduce<ResourceRowGroup>(
|
|
(acc, [rgUri, res]) => addResources(acc, rgUri, res),
|
|
withGroups
|
|
);
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
if (err.message !== 'No subscriptions were found') {
|
|
throw err;
|
|
}
|
|
if (filters) {
|
|
return [];
|
|
}
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async fetchAndAppendNestedRow(
|
|
rows: ResourceRowGroup,
|
|
parentRow: ResourceRow,
|
|
type: ResourcePickerQueryType,
|
|
filters?: ResourceGraphFilters
|
|
): Promise<ResourceRowGroup> {
|
|
const nestedRows =
|
|
parentRow.type === ResourceRowType.Subscription
|
|
? await this.getResourceGroupsBySubscriptionId(parentRow.id, type, filters)
|
|
: await this.getResourcesForResourceGroup(parentRow.uri, type, filters);
|
|
|
|
return addResources(rows, parentRow.uri, nestedRows);
|
|
}
|
|
|
|
search = async (
|
|
searchPhrase: string,
|
|
searchType: ResourcePickerQueryType,
|
|
filters: ResourceGraphFilters
|
|
): Promise<ResourceRowGroup> => {
|
|
let searchQuery = 'resources';
|
|
if (searchType === 'logs') {
|
|
searchQuery += `
|
|
| union resourcecontainers`;
|
|
}
|
|
|
|
const filtersQuery = createFilter(filters);
|
|
searchQuery += `
|
|
| where id contains "${searchPhrase}"
|
|
${await this.filterByType(searchType)}
|
|
${filtersQuery}
|
|
| order by tolower(name) asc
|
|
| limit ${this.resultLimit}
|
|
`;
|
|
const response =
|
|
await this.azureResourceGraphDatasource.pagedResourceGraphRequest<RawAzureResourceItem>(searchQuery);
|
|
return response.map((item) => {
|
|
const parsedUri = parseResourceURI(item.id);
|
|
if (!parsedUri || !(parsedUri.resourceName || parsedUri.resourceGroup || parsedUri.subscription)) {
|
|
throw new Error('unable to fetch resource details');
|
|
}
|
|
let id = parsedUri.subscription ?? '';
|
|
let type = ResourceRowType.Subscription;
|
|
if (parsedUri.resourceName) {
|
|
id = parsedUri.resourceName;
|
|
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: item.location,
|
|
};
|
|
});
|
|
};
|
|
|
|
async getSubscriptions(filters?: ResourceGraphFilters): Promise<ResourceRowGroup> {
|
|
const subscriptions = await this.azureResourceGraphDatasource.getSubscriptions(filters);
|
|
|
|
if (!subscriptions.length) {
|
|
throw new Error('No subscriptions were found');
|
|
}
|
|
|
|
return subscriptions.map((subscription) => ({
|
|
name: subscription.subscriptionName,
|
|
id: subscription.subscriptionId,
|
|
uri: `/subscriptions/${subscription.subscriptionId}`,
|
|
typeLabel: 'Subscription',
|
|
type: ResourceRowType.Subscription,
|
|
children: [],
|
|
}));
|
|
}
|
|
|
|
async getResourceGroupsBySubscriptionId(
|
|
subscriptionId: string,
|
|
type: ResourcePickerQueryType,
|
|
filters?: ResourceGraphFilters
|
|
): Promise<ResourceRowGroup> {
|
|
const filter = await this.filterByType(type);
|
|
|
|
const resourceGroups = await this.azureResourceGraphDatasource.getResourceGroups(subscriptionId, filter, filters);
|
|
|
|
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: [],
|
|
};
|
|
});
|
|
}
|
|
|
|
// Refactor this one out at a later date
|
|
async getResourcesForResourceGroup(
|
|
uri: string,
|
|
type: ResourcePickerQueryType,
|
|
filters?: ResourceGraphFilters
|
|
): Promise<ResourceRowGroup> {
|
|
const resources = await this.azureResourceGraphDatasource.getResourceNames(
|
|
{ uri },
|
|
await this.filterByType(type),
|
|
filters
|
|
);
|
|
|
|
return resources.map((resource) => {
|
|
return {
|
|
name: resource.name,
|
|
id: resource.name,
|
|
uri: resource.id,
|
|
resourceGroupName: resource.resourceGroup,
|
|
type: ResourceRowType.Resource,
|
|
typeLabel: resourceTypeDisplayNames[resource.type] || resource.type,
|
|
locationDisplayName: resource.location,
|
|
location: resource.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<AzureMonitorResource> {
|
|
const { subscription, resourceGroup, resourceName } = parseResourceDetails(resourceURI) ?? {};
|
|
|
|
if (!subscription) {
|
|
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/${subscription}`;
|
|
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 response = await this.azureResourceGraphDatasource.pagedResourceGraphRequest<AzureResourceSummaryItem>(query);
|
|
|
|
if (!response.length) {
|
|
throw new Error('unable to fetch resource details');
|
|
}
|
|
|
|
const { subscriptionName, resourceGroupName, resourceName: responseResourceName } = 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 {
|
|
subscription: subscriptionName || subscription,
|
|
resourceGroup: resourceGroupName || resourceGroup,
|
|
resourceName: responseResourceName || resourceName,
|
|
};
|
|
}
|
|
|
|
async getResourceURIFromWorkspace(workspace: string) {
|
|
const response = await this.azureResourceGraphDatasource.pagedResourceGraphRequest<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;
|
|
}
|
|
|
|
private filterByType = async (t: ResourcePickerQueryType) => {
|
|
if (this.supportedMetricNamespaces === '' && t !== 'logs') {
|
|
await this.fetchAllNamespaces();
|
|
}
|
|
return t === 'logs'
|
|
? `| where type in (${logsSupportedResourceTypesKusto})`
|
|
: `| where type in (${this.supportedMetricNamespaces})`;
|
|
};
|
|
|
|
private async fetchAllNamespaces() {
|
|
const subscriptions = await this.getSubscriptions();
|
|
reportInteraction('grafana_ds_azuremonitor_subscriptions_loaded', { subscriptions: subscriptions.length });
|
|
|
|
let supportedMetricNamespaces: Set<string> = new Set();
|
|
// Include a predefined set of metric namespaces as a fallback in the case the user cannot query subscriptions
|
|
resourceTypes.forEach((namespace) => {
|
|
supportedMetricNamespaces.add(`"${namespace}"`);
|
|
});
|
|
|
|
// We make use of these three regions as they *should* contain every possible namespace
|
|
const regions = ['westeurope', 'eastus', 'japaneast'];
|
|
const getNamespacesForRegion = async (region: string) => {
|
|
const namespaces = await this.azureMonitorDatasource.getMetricNamespaces(
|
|
{
|
|
// We only need to run this request against the first available subscription
|
|
resourceUri: `/subscriptions/${subscriptions[0].id}`,
|
|
},
|
|
false,
|
|
region
|
|
);
|
|
if (namespaces) {
|
|
for (const namespace of namespaces) {
|
|
supportedMetricNamespaces.add(`"${namespace.value.toLocaleLowerCase()}"`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const promises = regions.map((region) => getNamespacesForRegion(region));
|
|
await Promise.all(promises);
|
|
|
|
if (supportedMetricNamespaces.size === 0) {
|
|
throw new Error(
|
|
'Unable to resolve a list of valid metric namespaces. Validate the datasource configuration is correct and required permissions have been granted for all subscriptions. Grafana requires at least the Reader role to be assigned.'
|
|
);
|
|
}
|
|
|
|
this.supportedMetricNamespaces = Array.from(supportedMetricNamespaces).join(',');
|
|
}
|
|
|
|
parseRows(resources: Array<string | AzureMonitorResource>): ResourceRow[] {
|
|
const resourceObjs = parseMultipleResourceDetails(resources);
|
|
const newSelectedRows: ResourceRow[] = [];
|
|
resourceObjs.forEach((resource, i) => {
|
|
let id = resource.resourceName;
|
|
let name = resource.resourceName;
|
|
let rtype = ResourceRowType.Resource;
|
|
if (!id) {
|
|
id = resource.resourceGroup;
|
|
name = resource.resourceGroup;
|
|
rtype = ResourceRowType.ResourceGroup;
|
|
if (!id) {
|
|
id = resource.subscription;
|
|
name = resource.subscription;
|
|
rtype = ResourceRowType.Subscription;
|
|
}
|
|
}
|
|
newSelectedRows.push({
|
|
id: id ?? '',
|
|
name: name ?? '',
|
|
type: rtype,
|
|
uri: resourceToString(resource),
|
|
typeLabel:
|
|
resourceTypeDisplayNames[resource.metricNamespace?.toLowerCase() ?? ''] ?? resource.metricNamespace ?? '',
|
|
location: resource.region,
|
|
});
|
|
});
|
|
return newSelectedRows;
|
|
}
|
|
}
|
|
export const createFilter = (filters: ResourceGraphFilters) => {
|
|
let filtersQuery = '';
|
|
if (filters) {
|
|
if (filters.subscriptions && filters.subscriptions.length > 0) {
|
|
filtersQuery += `| where subscriptionId in (${filters.subscriptions.map((s) => `"${s.toLowerCase()}"`).join(',')})\n`;
|
|
}
|
|
if (filters.types && filters.types.length > 0) {
|
|
filtersQuery += `| where type in (${filters.types.map((t) => `"${t.toLowerCase()}"`).join(',')})\n`;
|
|
}
|
|
if (filters.locations && filters.locations.length > 0) {
|
|
filtersQuery += `| where location in (${filters.locations.map((l) => `"${l.toLowerCase()}"`).join(',')})\n`;
|
|
}
|
|
}
|
|
|
|
return filtersQuery;
|
|
};
|