Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AzureMonitor: Support querying subscriptions and resource groups in Azure Monitor Logs #34766

Merged
merged 2 commits into from May 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -17,10 +17,9 @@ function parseResourceDetails(resourceURI: string) {
}

return {
id: resourceURI,
subscriptionName: parsed.subscriptionID,
resourceGroupName: parsed.resourceGroup,
name: parsed.resource,
resourceName: parsed.resource,
};
}

Expand Down Expand Up @@ -85,7 +84,7 @@ const ResourceLabel = ({ resource, datasource }: ResourceLabelProps) => {

useEffect(() => {
if (resource && parseResourceDetails(resource)) {
datasource.resourcePickerData.getResource(resource).then(setResourceComponents);
datasource.resourcePickerData.getResourceURIDisplayProperties(resource).then(setResourceComponents);
} else {
setResourceComponents(undefined);
}
Expand Down Expand Up @@ -118,10 +117,18 @@ const FormattedResource = ({ resource }: FormattedResourceProps) => {
return (
<span>
<Icon name="layer-group" /> {resource.subscriptionName}
<Separator />
<Icon name="folder" /> {resource.resourceGroupName}
<Separator />
<Icon name="cube" /> {resource.name}
{resource.resourceGroupName && (
<>
<Separator />
<Icon name="folder" /> {resource.resourceGroupName}
</>
)}
{resource.resourceName && (
<>
<Separator />
<Icon name="cube" /> {resource.resourceName}
</>
)}
</span>
);
};
Expand Down
@@ -1,6 +1,7 @@
import { cx } from '@emotion/css';
import { Checkbox, Icon, IconButton, LoadingPlaceholder, useStyles2, useTheme2, FadeTransition } from '@grafana/ui';
import React, { useCallback, useEffect, useState } from 'react';
import { Space } from '../Space';
import getStyles from './styles';
import { ResourceRowType, ResourceRow, ResourceRowGroup } from './types';
import { findRow } from './utils';
Expand Down Expand Up @@ -163,7 +164,9 @@ const NestedEntry: React.FC<NestedEntryProps> = ({
const theme = useTheme2();
const styles = useStyles2(getStyles);
const hasChildren = !!entry.children;
const isSelectable = entry.type === ResourceRowType.Resource || entry.type === ResourceRowType.Variable;
// Subscriptions, resource groups, resources, and variables are all selectable, so
// the top-level variable group is the only thing that cannot be selected.
const isSelectable = entry.type !== ResourceRowType.VariableGroup;
joshhunt marked this conversation as resolved.
Show resolved Hide resolved

const handleToggleCollapse = useCallback(() => {
onToggleCollapse(entry);
Expand All @@ -185,20 +188,24 @@ const NestedEntry: React.FC<NestedEntryProps> = ({
of the collapse button for leaf rows that have no children to get them to align */}

<span className={styles.entryContentItem}>
{hasChildren && (
{hasChildren ? (
<IconButton
className={styles.collapseButton}
name={isOpen ? 'angle-down' : 'angle-right'}
aria-label={isOpen ? 'Collapse' : 'Expand'}
onClick={handleToggleCollapse}
id={entry.id}
/>
) : (
<Space layout="inline" h={2} />
)}
</span>

{isSelectable && (
{isSelectable && (
<span className={styles.entryContentItem}>
<Checkbox id={checkboxId} onChange={handleSelectedChanged} disabled={isDisabled} value={isSelected} />
)}
</span>
</span>
)}

<span className={styles.entryContentItem}>
<EntryIcon entry={entry} isOpen={isOpen} />
Expand Down
Expand Up @@ -42,7 +42,14 @@ const ResourcePicker = ({
// Map the selected item into an array of rows
const selectedResourceRows = useMemo(() => {
const found = internalSelected && findRow(rows, internalSelected);
return found ? [found] : [];
return found
? [
{
...found,
children: undefined,
},
]
: [];
}, [internalSelected, rows]);

// Request resources for a expanded resource group
Expand Down
Expand Up @@ -29,13 +29,13 @@ const getStyles = (theme: GrafanaTheme2) => ({
}),

cell: css({
padding: theme.spacing(1, 0),
padding: theme.spacing(1, 1, 1, 0),
width: '25%',
overflow: 'hidden',
textOverflow: 'ellipsis',
'&:first-of-type': {
width: '50%',
padding: theme.spacing(1, 0, 1, 2),
padding: theme.spacing(1, 1, 1, 2),
},
}),

Expand Down
@@ -0,0 +1,36 @@
import { parseResourceURI } from './utils';

describe('AzureMonitor ResourcePicker utils', () => {
describe('parseResourceURI', () => {
it('should parse subscription URIs', () => {
expect(parseResourceURI('/subscriptions/44693801-6ee6-49de-9b2d-9106972f9572')).toEqual({
subscriptionID: '44693801-6ee6-49de-9b2d-9106972f9572',
});
});

it('should parse resource group URIs', () => {
expect(
parseResourceURI('/subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/cloud-datasources')
).toEqual({
subscriptionID: '44693801-6ee6-49de-9b2d-9106972f9572',
resourceGroup: 'cloud-datasources',
});
});

it('should parse resource URIs', () => {
expect(
parseResourceURI(
'/subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/cloud-datasources/providers/Microsoft.Compute/virtualMachines/GithubTestDataVM'
)
).toEqual({
subscriptionID: '44693801-6ee6-49de-9b2d-9106972f9572',
resourceGroup: 'cloud-datasources',
resource: 'GithubTestDataVM',
});
});

it('returns undefined for invalid input', () => {
expect(parseResourceURI('44693801-6ee6-49de-9b2d-9106972f9572')).toBeUndefined();
});
});
});
@@ -1,16 +1,23 @@
import produce from 'immer';
import { ResourceRow, ResourceRowGroup } from './types';

const RESOURCE_URI_REGEX = /\/subscriptions\/(?<subscriptionID>.+)\/resourceGroups\/(?<resourceGroup>.+)\/providers.+\/(?<resource>[\w-_]+)/;
// This regex matches URIs representing:
// - subscriptions: /subscriptions/44693801-6ee6-49de-9b2d-9106972f9572
// - resource groups: /subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/cloud-datasources
// - resources: /subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/cloud-datasources/providers/Microsoft.Compute/virtualMachines/GithubTestDataVM
const RESOURCE_URI_REGEX = /\/subscriptions\/(?<subscriptionID>[^/]+)(?:\/resourceGroups\/(?<resourceGroup>[^/]+)(?:\/providers.+\/(?<resource>[^/]+))?)?/;

type RegexGroups = Record<string, string | undefined>;

export function parseResourceURI(resourceURI: string) {
const matches = RESOURCE_URI_REGEX.exec(resourceURI);
const groups: RegexGroups = matches?.groups ?? {};
const { subscriptionID, resourceGroup, resource } = groups;

if (!matches?.groups?.subscriptionID || !matches?.groups?.resourceGroup) {
if (!subscriptionID) {
return undefined;
}

const { subscriptionID, resourceGroup, resource } = matches.groups;
return { subscriptionID, resourceGroup, resource };
}

Expand Down
@@ -1,6 +1,7 @@
import { FetchResponse, getBackendSrv } from '@grafana/runtime';
import { getLogAnalyticsResourcePickerApiRoute } from '../api/routes';
import { ResourceRowType, ResourceRow, ResourceRowGroup } from '../components/ResourcePicker/types';
import { parseResourceURI } from '../components/ResourcePicker/utils';
import { getAzureCloud } from '../credentials';
import {
AzureDataSourceInstanceSettings,
Expand Down Expand Up @@ -73,21 +74,38 @@ export default class ResourcePickerData {
return formatResourceGroupChildren(response.data);
}

async getResource(resourceURI: string) {
async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> {
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 = `
resources
| join (
resourcecontainers
| where type == "microsoft.resources/subscriptions"
| project subscriptionName=name, subscriptionId
) on subscriptionId
| join (
resourcecontainers
| where type == "microsoft.resources/subscriptions/resourcegroups"
| project resourceGroupName=name, resourceGroup
) on resourceGroup
| where id == "${resourceURI}"
| project id, name, subscriptionName, resourceGroupName
resourcecontainers
| where type == "microsoft.resources/subscriptions"
Comment on lines +90 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice if microsoft developed a js library for building queries :P

| 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<AzureResourceSummaryItem[]>(query);
Expand Down
Expand Up @@ -230,10 +230,9 @@ export interface AzureQueryEditorFieldProps {
}

export interface AzureResourceSummaryItem {
id: string;
name: string;
subscriptionName: string;
resourceGroupName: string;
resourceGroupName: string | undefined;
resourceName: string | undefined;
}

export interface RawAzureResourceGroupItem {
Expand Down