mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AzureMonitor: Managed Identity configuration UI (#34170)
* Basic UI for Managed Identity * Credentials logic * Fix datasource validation * Do not offer Managed Identity for Log Analytics * Logic fixes * Show Log Analytics credentials only for App Registration * Fix tests * Datasource validation refactoring
This commit is contained in:
@@ -3,7 +3,7 @@ import { getBackendSrv, getTemplateSrv, DataSourceWithBackend } from '@grafana/r
|
||||
import { isString } from 'lodash';
|
||||
|
||||
import TimegrainConverter from '../time_grain_converter';
|
||||
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from '../types';
|
||||
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types';
|
||||
import ResponseParser from './response_parser';
|
||||
import { getAzureCloud } from '../credentials';
|
||||
import { getAppInsightsApiRoute } from '../api/routes';
|
||||
@@ -132,10 +132,10 @@ export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMo
|
||||
return null;
|
||||
}
|
||||
|
||||
testDatasource() {
|
||||
testDatasource(): Promise<DatasourceValidationResult> {
|
||||
const url = `${this.baseUrl}/metrics/metadata`;
|
||||
return this.doRequest(url)
|
||||
.then((response: any) => {
|
||||
.then<DatasourceValidationResult>((response: any) => {
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
status: 'success',
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { map } from 'lodash';
|
||||
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
|
||||
import ResponseParser, { transformMetadataToKustoSchema } from './response_parser';
|
||||
import { AzureMonitorQuery, AzureDataSourceJsonData, AzureLogsVariable, AzureQueryType } from '../types';
|
||||
import {
|
||||
AzureMonitorQuery,
|
||||
AzureDataSourceJsonData,
|
||||
AzureLogsVariable,
|
||||
AzureQueryType,
|
||||
DatasourceValidationResult,
|
||||
} from '../types';
|
||||
import {
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
@@ -12,7 +18,7 @@ import {
|
||||
import { getBackendSrv, getTemplateSrv, DataSourceWithBackend, FetchResponse } from '@grafana/runtime';
|
||||
import { Observable, from } from 'rxjs';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
import { getAzureCloud } from '../credentials';
|
||||
import { getAuthType, getAzureCloud } from '../credentials';
|
||||
import { getLogAnalyticsApiRoute, getLogAnalyticsManagementApiRoute } from '../api/routes';
|
||||
import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata';
|
||||
|
||||
@@ -349,8 +355,8 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
}
|
||||
|
||||
// TODO: update to be resource-centric
|
||||
testDatasource(): Promise<any> {
|
||||
const validationError = this.isValidConfig();
|
||||
testDatasource(): Promise<DatasourceValidationResult> {
|
||||
const validationError = this.validateDatasource();
|
||||
if (validationError) {
|
||||
return Promise.resolve(validationError);
|
||||
}
|
||||
@@ -361,7 +367,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
|
||||
return this.doRequest(url);
|
||||
})
|
||||
.then((response: any) => {
|
||||
.then<DatasourceValidationResult>((response: any) => {
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
status: 'success',
|
||||
@@ -404,36 +410,36 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
return message;
|
||||
}
|
||||
|
||||
isValidConfig() {
|
||||
if (this.instanceSettings.jsonData.azureLogAnalyticsSameAs) {
|
||||
return undefined;
|
||||
private validateDatasource(): DatasourceValidationResult | undefined {
|
||||
const authType = getAuthType(this.instanceSettings);
|
||||
|
||||
if (authType === 'clientsecret') {
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsTenantId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Tenant Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsClientId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Client Id field is required.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsSubscriptionId)) {
|
||||
if (!this.isValidConfigField(this.subscriptionId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Subscription Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsTenantId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Tenant Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsClientId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Client Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
isValidConfigField(field: string | undefined) {
|
||||
return field && field.length > 0;
|
||||
private isValidConfigField(field: string | undefined): boolean {
|
||||
return typeof field === 'string' && field.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
AzureQueryType,
|
||||
AzureMonitorMetricsMetadataResponse,
|
||||
AzureMetricQuery,
|
||||
DatasourceValidationResult,
|
||||
} from '../types';
|
||||
import {
|
||||
DataSourceInstanceSettings,
|
||||
@@ -25,7 +26,7 @@ import { from, Observable } from 'rxjs';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { getAzureCloud } from '../credentials';
|
||||
import { getAuthType, getAzureCloud } from '../credentials';
|
||||
import { getManagementApiRoute } from '../api/routes';
|
||||
|
||||
const defaultDropdownValue = 'select';
|
||||
@@ -458,24 +459,15 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
});
|
||||
}
|
||||
|
||||
testDatasource(): Promise<any> {
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.tenantId)) {
|
||||
return Promise.resolve({
|
||||
status: 'error',
|
||||
message: 'The Tenant Id field is required.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.clientId)) {
|
||||
return Promise.resolve({
|
||||
status: 'error',
|
||||
message: 'The Client Id field is required.',
|
||||
});
|
||||
testDatasource(): Promise<DatasourceValidationResult> {
|
||||
const validationError = this.validateDatasource();
|
||||
if (validationError) {
|
||||
return Promise.resolve(validationError);
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}?api-version=2019-03-01`;
|
||||
return this.doRequest(url)
|
||||
.then((response: any) => {
|
||||
.then<DatasourceValidationResult>((response: any) => {
|
||||
if (response.status === 200) {
|
||||
return {
|
||||
status: 'success',
|
||||
@@ -509,8 +501,37 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
});
|
||||
}
|
||||
|
||||
isValidConfigField(field?: string) {
|
||||
return field && field.length > 0;
|
||||
private validateDatasource(): DatasourceValidationResult | undefined {
|
||||
const authType = getAuthType(this.instanceSettings);
|
||||
|
||||
if (authType === 'clientsecret') {
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.tenantId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Tenant Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.instanceSettings.jsonData.clientId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Client Id field is required.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isValidConfigField(this.subscriptionId)) {
|
||||
return {
|
||||
status: 'error',
|
||||
message: 'The Subscription Id field is required.',
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private isValidConfigField(field?: string): boolean {
|
||||
return typeof field === 'string' && field.length > 0;
|
||||
}
|
||||
|
||||
doRequest<T = any>(url: string, maxRetries = 1): Promise<FetchResponse<T>> {
|
||||
|
||||
@@ -27,6 +27,8 @@ export const AnalyticsConfig: FunctionComponent<Props> = (props: Props) => {
|
||||
? props.options.jsonData.logAnalyticsSubscriptionId
|
||||
: props.options.jsonData.subscriptionId;
|
||||
|
||||
const credentialsEnabled = primaryCredentials.authType === 'clientsecret';
|
||||
|
||||
const hasRequiredFields =
|
||||
subscriptionId &&
|
||||
(logAnalyticsCredentials
|
||||
@@ -127,32 +129,41 @@ export const AnalyticsConfig: FunctionComponent<Props> = (props: Props) => {
|
||||
}),
|
||||
};
|
||||
|
||||
const showSameAsHelpMsg = sameAsSwitched && !primaryCredentials.clientSecret;
|
||||
const showSameAsHelpMsg =
|
||||
credentialsEnabled &&
|
||||
sameAsSwitched &&
|
||||
primaryCredentials.authType === 'clientsecret' &&
|
||||
!primaryCredentials.clientSecret;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="page-heading">Azure Monitor Logs Details</h3>
|
||||
<Switch
|
||||
label="Same details as Azure Monitor API"
|
||||
checked={!logAnalyticsCredentials}
|
||||
onChange={onLogAnalyticsSameAsChange}
|
||||
{...tooltipAttribute}
|
||||
/>
|
||||
{showSameAsHelpMsg && (
|
||||
<div className="grafana-info-box m-t-2">
|
||||
<div className="alert-body">
|
||||
<p>Re-enter your Azure Monitor Client Secret to use this setting.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{logAnalyticsCredentials && (
|
||||
<AzureCredentialsForm
|
||||
credentials={logAnalyticsCredentials}
|
||||
defaultSubscription={subscriptionId}
|
||||
onCredentialsChange={onCredentialsChange}
|
||||
onDefaultSubscriptionChange={onLogAnalyticsDefaultSubscriptionChange}
|
||||
getSubscriptions={getSubscriptions}
|
||||
/>
|
||||
<h3 className="page-heading">Azure Monitor Logs</h3>
|
||||
{credentialsEnabled && (
|
||||
<>
|
||||
<Switch
|
||||
label="Same details as Azure Monitor API"
|
||||
checked={!logAnalyticsCredentials}
|
||||
onChange={onLogAnalyticsSameAsChange}
|
||||
{...tooltipAttribute}
|
||||
/>
|
||||
{showSameAsHelpMsg && (
|
||||
<div className="grafana-info-box m-t-2">
|
||||
<div className="alert-body">
|
||||
<p>Re-enter your Azure Monitor Client Secret to use this setting.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{logAnalyticsCredentials && (
|
||||
<AzureCredentialsForm
|
||||
managedIdentityEnabled={false}
|
||||
credentials={logAnalyticsCredentials}
|
||||
defaultSubscription={subscriptionId}
|
||||
onCredentialsChange={onCredentialsChange}
|
||||
onDefaultSubscriptionChange={onLogAnalyticsDefaultSubscriptionChange}
|
||||
getSubscriptions={getSubscriptions}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="gf-form-group">
|
||||
<div className="gf-form-inline">
|
||||
|
||||
@@ -4,7 +4,9 @@ import AzureCredentialsForm, { Props } from './AzureCredentialsForm';
|
||||
|
||||
const setup = (propsFunc?: (props: Props) => Props) => {
|
||||
let props: Props = {
|
||||
managedIdentityEnabled: false,
|
||||
credentials: {
|
||||
authType: 'clientsecret',
|
||||
azureCloud: 'azuremonitor',
|
||||
tenantId: 'e7f3f661-a933-3h3f-0294-31c4f962ec48',
|
||||
clientId: '34509fad-c0r9-45df-9e25-f1ee34af6900',
|
||||
@@ -39,6 +41,7 @@ describe('Render', () => {
|
||||
const wrapper = setup((props) => ({
|
||||
...props,
|
||||
credentials: {
|
||||
authType: 'clientsecret',
|
||||
azureCloud: 'azuremonitor',
|
||||
tenantId: 'e7f3f661-a933-3h3f-0294-31c4f962ec48',
|
||||
clientId: '34509fad-c0r9-45df-9e25-f1ee34af6900',
|
||||
@@ -52,6 +55,7 @@ describe('Render', () => {
|
||||
const wrapper = setup((props) => ({
|
||||
...props,
|
||||
credentials: {
|
||||
authType: 'clientsecret',
|
||||
azureCloud: 'azuremonitor',
|
||||
tenantId: 'e7f3f661-a933-3h3f-0294-31c4f962ec48',
|
||||
clientId: '34509fad-c0r9-45df-9e25-f1ee34af6900',
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { ChangeEvent, FunctionComponent, useEffect, useReducer, useState } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { InlineFormLabel, LegacyForms, Button } from '@grafana/ui';
|
||||
import { AzureCredentials } from '../types';
|
||||
import { AzureAuthType, AzureCredentials } from '../types';
|
||||
import { isCredentialsComplete } from '../credentials';
|
||||
const { Select, Input } = LegacyForms;
|
||||
|
||||
export interface Props {
|
||||
managedIdentityEnabled: boolean;
|
||||
credentials: AzureCredentials;
|
||||
defaultSubscription?: string;
|
||||
azureCloudOptions?: SelectableValue[];
|
||||
@@ -14,6 +15,17 @@ export interface Props {
|
||||
getSubscriptions?: () => Promise<SelectableValue[]>;
|
||||
}
|
||||
|
||||
const authTypeOptions: Array<SelectableValue<AzureAuthType>> = [
|
||||
{
|
||||
value: 'msi',
|
||||
label: 'Managed Identity',
|
||||
},
|
||||
{
|
||||
value: 'clientsecret',
|
||||
label: 'App Registration',
|
||||
},
|
||||
];
|
||||
|
||||
export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) => {
|
||||
const {
|
||||
credentials,
|
||||
@@ -61,8 +73,18 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
|
||||
}
|
||||
};
|
||||
|
||||
const onAzureCloudChange = (selected: SelectableValue<string>) => {
|
||||
const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
|
||||
if (onCredentialsChange) {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
authType: selected.value || 'msi',
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onAzureCloudChange = (selected: SelectableValue<string>) => {
|
||||
if (onCredentialsChange && credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
azureCloud: selected.value,
|
||||
@@ -72,7 +94,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
|
||||
};
|
||||
|
||||
const onTenantIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (onCredentialsChange) {
|
||||
if (onCredentialsChange && credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
tenantId: event.target.value,
|
||||
@@ -82,7 +104,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
|
||||
};
|
||||
|
||||
const onClientIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (onCredentialsChange) {
|
||||
if (onCredentialsChange && credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientId: event.target.value,
|
||||
@@ -92,7 +114,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
|
||||
};
|
||||
|
||||
const onClientSecretChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (onCredentialsChange) {
|
||||
if (onCredentialsChange && credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientSecret: event.target.value,
|
||||
@@ -102,7 +124,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
|
||||
};
|
||||
|
||||
const onClientSecretReset = () => {
|
||||
if (onCredentialsChange) {
|
||||
if (onCredentialsChange && credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientSecret: '',
|
||||
@@ -118,111 +140,128 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-group">
|
||||
{azureCloudOptions && (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12" tooltip="Choose an Azure Cloud.">
|
||||
Azure Cloud
|
||||
</InlineFormLabel>
|
||||
<Select
|
||||
className="width-15"
|
||||
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
|
||||
options={azureCloudOptions}
|
||||
onChange={onAzureCloudChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="gf-form-group">
|
||||
{props.managedIdentityEnabled && (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12">Directory (tenant) ID</InlineFormLabel>
|
||||
<div className="width-15">
|
||||
<Input
|
||||
className="width-30"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.tenantId || ''}
|
||||
onChange={onTenantIdChange}
|
||||
/>
|
||||
</div>
|
||||
<InlineFormLabel className="width-12" tooltip="Choose the type of authentication to Azure services">
|
||||
Authentication
|
||||
</InlineFormLabel>
|
||||
<Select
|
||||
className="width-15"
|
||||
value={authTypeOptions.find((opt) => opt.value === credentials.authType)}
|
||||
options={authTypeOptions}
|
||||
onChange={onAuthTypeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12">Application (client) ID</InlineFormLabel>
|
||||
<div className="width-15">
|
||||
<Input
|
||||
className="width-30"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.clientId || ''}
|
||||
onChange={onClientIdChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{typeof credentials.clientSecret === 'symbol' ? (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
|
||||
<Input className="width-25" placeholder="configured" disabled={true} />
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<div className="max-width-30 gf-form-inline">
|
||||
<Button variant="secondary" type="button" onClick={onClientSecretReset}>
|
||||
reset
|
||||
</Button>
|
||||
)}
|
||||
{credentials.authType === 'clientsecret' && (
|
||||
<>
|
||||
{azureCloudOptions && (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12" tooltip="Choose an Azure Cloud">
|
||||
Azure Cloud
|
||||
</InlineFormLabel>
|
||||
<Select
|
||||
className="width-15"
|
||||
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
|
||||
options={azureCloudOptions}
|
||||
onChange={onAzureCloudChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
|
||||
<InlineFormLabel className="width-12">Directory (tenant) ID</InlineFormLabel>
|
||||
<div className="width-15">
|
||||
<Input
|
||||
className="width-30"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.clientSecret || ''}
|
||||
onChange={onClientSecretChange}
|
||||
value={credentials.tenantId || ''}
|
||||
onChange={onTenantIdChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{getSubscriptions && onDefaultSubscriptionChange && (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12">Default Subscription</InlineFormLabel>
|
||||
<div className="width-25">
|
||||
<Select
|
||||
value={subscriptions.find((opt) => opt.value === defaultSubscription)}
|
||||
options={subscriptions}
|
||||
onChange={onSubscriptionChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12">Application (client) ID</InlineFormLabel>
|
||||
<div className="width-15">
|
||||
<Input
|
||||
className="width-30"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.clientId || ''}
|
||||
onChange={onClientIdChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{typeof credentials.clientSecret === 'symbol' ? (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
|
||||
<Input className="width-25" placeholder="configured" disabled={true} />
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<div className="max-width-30 gf-form-inline">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={onLoadSubscriptions}
|
||||
disabled={!hasRequiredFields}
|
||||
>
|
||||
Load Subscriptions
|
||||
<Button variant="secondary" type="button" onClick={onClientSecretReset}>
|
||||
reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
|
||||
<div className="width-15">
|
||||
<Input
|
||||
className="width-30"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.clientSecret || ''}
|
||||
onChange={onClientSecretChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{getSubscriptions && onDefaultSubscriptionChange && (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12">Default Subscription</InlineFormLabel>
|
||||
<div className="width-25">
|
||||
<Select
|
||||
value={subscriptions.find((opt) => opt.value === defaultSubscription)}
|
||||
options={subscriptions}
|
||||
onChange={onSubscriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<div className="max-width-30 gf-form-inline">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={onLoadSubscriptions}
|
||||
disabled={!hasRequiredFields}
|
||||
>
|
||||
Load Subscriptions
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export class InsightsConfig extends PureComponent<Props> {
|
||||
const { options, onUpdateJsonDataOption, onUpdateSecureJsonDataOption } = this.props;
|
||||
return (
|
||||
<>
|
||||
<h3 className="page-heading">Azure Application Insights Details</h3>
|
||||
<h3 className="page-heading">Azure Application Insights</h3>
|
||||
<div className="gf-form-group">
|
||||
{options.secureJsonFields.appInsightsApiKey ? (
|
||||
<div className="gf-form-inline">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { FunctionComponent, useMemo } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AzureCredentialsForm } from './AzureCredentialsForm';
|
||||
import { AzureDataSourceSettings, AzureCredentials } from '../types';
|
||||
import { getCredentials, updateCredentials, isLogAnalyticsSameAs } from '../credentials';
|
||||
@@ -50,8 +51,9 @@ export const MonitorConfig: FunctionComponent<Props> = (props: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="page-heading">Azure Monitor Metrics Details</h3>
|
||||
<h3 className="page-heading">Authentication</h3>
|
||||
<AzureCredentialsForm
|
||||
managedIdentityEnabled={config.azure.managedIdentityEnabled}
|
||||
credentials={credentials}
|
||||
defaultSubscription={subscriptionId}
|
||||
azureCloudOptions={azureClouds}
|
||||
|
||||
@@ -5,7 +5,7 @@ exports[`Render should disable log analytics credentials form 1`] = `
|
||||
<h3
|
||||
className="page-heading"
|
||||
>
|
||||
Azure Monitor Logs Details
|
||||
Azure Monitor Logs
|
||||
</h3>
|
||||
<Switch
|
||||
checked={true}
|
||||
@@ -89,7 +89,7 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
|
||||
<h3
|
||||
className="page-heading"
|
||||
>
|
||||
Azure Monitor Logs Details
|
||||
Azure Monitor Logs
|
||||
</h3>
|
||||
<Switch
|
||||
checked={false}
|
||||
@@ -99,6 +99,7 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
|
||||
<AzureCredentialsForm
|
||||
credentials={
|
||||
Object {
|
||||
"authType": "clientsecret",
|
||||
"azureCloud": "azuremonitor",
|
||||
"clientId": "44693801-6ee6-49de-9b2d-9106972f9572",
|
||||
"clientSecret": undefined,
|
||||
@@ -107,6 +108,7 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
|
||||
}
|
||||
defaultSubscription="e3fe4fde-ad5e-4d60-9974-e2f3562ffdf2"
|
||||
getSubscriptions={[MockFunction]}
|
||||
managedIdentityEnabled={false}
|
||||
onCredentialsChange={[Function]}
|
||||
onDefaultSubscriptionChange={[Function]}
|
||||
/>
|
||||
@@ -186,7 +188,7 @@ exports[`Render should render component 1`] = `
|
||||
<h3
|
||||
className="page-heading"
|
||||
>
|
||||
Azure Monitor Logs Details
|
||||
Azure Monitor Logs
|
||||
</h3>
|
||||
<Switch
|
||||
checked={false}
|
||||
@@ -196,6 +198,7 @@ exports[`Render should render component 1`] = `
|
||||
<AzureCredentialsForm
|
||||
credentials={
|
||||
Object {
|
||||
"authType": "clientsecret",
|
||||
"azureCloud": "azuremonitor",
|
||||
"clientId": undefined,
|
||||
"clientSecret": undefined,
|
||||
@@ -203,6 +206,7 @@ exports[`Render should render component 1`] = `
|
||||
}
|
||||
}
|
||||
getSubscriptions={[MockFunction]}
|
||||
managedIdentityEnabled={false}
|
||||
onCredentialsChange={[Function]}
|
||||
onDefaultSubscriptionChange={[Function]}
|
||||
/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ exports[`Render should disable insights api key input 1`] = `
|
||||
<h3
|
||||
className="page-heading"
|
||||
>
|
||||
Azure Application Insights Details
|
||||
Azure Application Insights
|
||||
</h3>
|
||||
<div
|
||||
className="gf-form-group"
|
||||
@@ -73,7 +73,7 @@ exports[`Render should enable insights api key input 1`] = `
|
||||
<h3
|
||||
className="page-heading"
|
||||
>
|
||||
Azure Application Insights Details
|
||||
Azure Application Insights
|
||||
</h3>
|
||||
<div
|
||||
className="gf-form-group"
|
||||
@@ -130,7 +130,7 @@ exports[`Render should render component 1`] = `
|
||||
<h3
|
||||
className="page-heading"
|
||||
>
|
||||
Azure Application Insights Details
|
||||
Azure Application Insights
|
||||
</h3>
|
||||
<div
|
||||
className="gf-form-group"
|
||||
|
||||
@@ -1,9 +1,57 @@
|
||||
import { AzureCredentials, AzureDataSourceInstanceSettings, AzureDataSourceSettings, ConcealedSecret } from './types';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
AzureAuthType,
|
||||
AzureCloud,
|
||||
AzureCredentials,
|
||||
AzureDataSourceInstanceSettings,
|
||||
AzureDataSourceSettings,
|
||||
ConcealedSecret,
|
||||
} from './types';
|
||||
|
||||
const concealed: ConcealedSecret = Symbol('Concealed client secret');
|
||||
|
||||
export function getAuthType(options: AzureDataSourceSettings | AzureDataSourceInstanceSettings): AzureAuthType {
|
||||
if (!options.jsonData.azureAuthType) {
|
||||
// If authentication type isn't explicitly specified and datasource has client credentials,
|
||||
// then this is existing datasource which is configured for app registration (client secret)
|
||||
if (options.jsonData.tenantId && options.jsonData.clientId) {
|
||||
return 'clientsecret';
|
||||
}
|
||||
|
||||
// For newly created datasource with no configuration, managed identity is the default authentication type
|
||||
// if they are enabled in Grafana config
|
||||
return config.azure.managedIdentityEnabled ? 'msi' : 'clientsecret';
|
||||
}
|
||||
|
||||
return options.jsonData.azureAuthType;
|
||||
}
|
||||
|
||||
function getDefaultAzureCloud(): string {
|
||||
switch (config.azure.cloud) {
|
||||
case AzureCloud.Public:
|
||||
case AzureCloud.None:
|
||||
case undefined:
|
||||
return 'azuremonitor';
|
||||
case AzureCloud.China:
|
||||
return 'chinaazuremonitor';
|
||||
case AzureCloud.USGovernment:
|
||||
return 'govazuremonitor';
|
||||
case AzureCloud.Germany:
|
||||
return 'germanyazuremonitor';
|
||||
default:
|
||||
throw new Error(`The cloud '${config.azure.cloud}' not supported.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAzureCloud(options: AzureDataSourceSettings | AzureDataSourceInstanceSettings): string {
|
||||
return options.jsonData.cloudName || 'azuremonitor';
|
||||
const authType = getAuthType(options);
|
||||
switch (authType) {
|
||||
case 'msi':
|
||||
// In case of managed identity, the cloud is always same as where Grafana is hosted
|
||||
return getDefaultAzureCloud();
|
||||
case 'clientsecret':
|
||||
return options.jsonData.cloudName || getDefaultAzureCloud();
|
||||
}
|
||||
}
|
||||
|
||||
function getSecret(options: AzureDataSourceSettings): undefined | string | ConcealedSecret {
|
||||
@@ -26,30 +74,62 @@ function getLogAnalyticsSecret(options: AzureDataSourceSettings): undefined | st
|
||||
}
|
||||
}
|
||||
|
||||
export function isLogAnalyticsSameAs(options: AzureDataSourceSettings): boolean {
|
||||
export function isLogAnalyticsSameAs(options: AzureDataSourceSettings | AzureDataSourceInstanceSettings): boolean {
|
||||
return typeof options.jsonData.azureLogAnalyticsSameAs !== 'boolean' || options.jsonData.azureLogAnalyticsSameAs;
|
||||
}
|
||||
|
||||
export function isCredentialsComplete(credentials: AzureCredentials) {
|
||||
return !!(credentials.tenantId && credentials.clientId && credentials.clientSecret);
|
||||
export function isCredentialsComplete(credentials: AzureCredentials): boolean {
|
||||
switch (credentials.authType) {
|
||||
case 'msi':
|
||||
return true;
|
||||
case 'clientsecret':
|
||||
return !!(credentials.azureCloud && credentials.tenantId && credentials.clientId && credentials.clientSecret);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCredentials(options: AzureDataSourceSettings): AzureCredentials {
|
||||
return {
|
||||
azureCloud: getAzureCloud(options),
|
||||
tenantId: options.jsonData.tenantId,
|
||||
clientId: options.jsonData.clientId,
|
||||
clientSecret: getSecret(options),
|
||||
};
|
||||
const authType = getAuthType(options);
|
||||
switch (authType) {
|
||||
case 'msi':
|
||||
if (config.azure.managedIdentityEnabled) {
|
||||
return {
|
||||
authType: 'msi',
|
||||
};
|
||||
} else {
|
||||
// If authentication type is managed identity but managed identities were disabled in Grafana config,
|
||||
// then we should fallback to an empty app registration (client secret) configuration
|
||||
return {
|
||||
authType: 'clientsecret',
|
||||
azureCloud: getDefaultAzureCloud(),
|
||||
};
|
||||
}
|
||||
case 'clientsecret':
|
||||
return {
|
||||
authType: 'clientsecret',
|
||||
azureCloud: options.jsonData.cloudName || getDefaultAzureCloud(),
|
||||
tenantId: options.jsonData.tenantId,
|
||||
clientId: options.jsonData.clientId,
|
||||
clientSecret: getSecret(options),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getLogAnalyticsCredentials(options: AzureDataSourceSettings): AzureCredentials | undefined {
|
||||
const authType = getAuthType(options);
|
||||
|
||||
if (authType !== 'clientsecret') {
|
||||
// Only app registration (client secret) authentication supports different credentials for Log Analytics
|
||||
// for backward compatibility
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isLogAnalyticsSameAs(options)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
azureCloud: getAzureCloud(options),
|
||||
authType: 'clientsecret',
|
||||
azureCloud: options.jsonData.cloudName || getDefaultAzureCloud(),
|
||||
tenantId: options.jsonData.logAnalyticsTenantId,
|
||||
clientId: options.jsonData.logAnalyticsClientId,
|
||||
clientSecret: getLogAnalyticsSecret(options),
|
||||
@@ -60,57 +140,83 @@ export function updateCredentials(
|
||||
options: AzureDataSourceSettings,
|
||||
credentials: AzureCredentials
|
||||
): AzureDataSourceSettings {
|
||||
options = {
|
||||
...options,
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
cloudName: credentials.azureCloud || 'azuremonitor',
|
||||
tenantId: credentials.tenantId,
|
||||
clientId: credentials.clientId,
|
||||
},
|
||||
secureJsonData: {
|
||||
...options.secureJsonData,
|
||||
clientSecret:
|
||||
typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
|
||||
? credentials.clientSecret
|
||||
: undefined,
|
||||
},
|
||||
secureJsonFields: {
|
||||
...options.secureJsonFields,
|
||||
clientSecret: typeof credentials.clientSecret === 'symbol',
|
||||
},
|
||||
};
|
||||
switch (credentials.authType) {
|
||||
case 'msi':
|
||||
if (!config.azure.managedIdentityEnabled) {
|
||||
throw new Error('Managed Identity authentication is not enabled in Grafana config.');
|
||||
}
|
||||
|
||||
if (isLogAnalyticsSameAs(options)) {
|
||||
options = updateLogAnalyticsCredentials(options, credentials);
|
||||
options = {
|
||||
...options,
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
azureAuthType: 'msi',
|
||||
},
|
||||
};
|
||||
|
||||
if (!isLogAnalyticsSameAs(options)) {
|
||||
options = updateLogAnalyticsSameAs(options, true);
|
||||
}
|
||||
|
||||
return options;
|
||||
|
||||
case 'clientsecret':
|
||||
options = {
|
||||
...options,
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
azureAuthType: 'clientsecret',
|
||||
cloudName: credentials.azureCloud || getDefaultAzureCloud(),
|
||||
tenantId: credentials.tenantId,
|
||||
clientId: credentials.clientId,
|
||||
},
|
||||
secureJsonData: {
|
||||
...options.secureJsonData,
|
||||
clientSecret:
|
||||
typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
|
||||
? credentials.clientSecret
|
||||
: undefined,
|
||||
},
|
||||
secureJsonFields: {
|
||||
...options.secureJsonFields,
|
||||
clientSecret: typeof credentials.clientSecret === 'object',
|
||||
},
|
||||
};
|
||||
|
||||
if (isLogAnalyticsSameAs(options)) {
|
||||
options = updateLogAnalyticsCredentials(options, credentials);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function updateLogAnalyticsCredentials(
|
||||
options: AzureDataSourceSettings,
|
||||
credentials: AzureCredentials
|
||||
): AzureDataSourceSettings {
|
||||
options = {
|
||||
...options,
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
logAnalyticsTenantId: credentials.tenantId,
|
||||
logAnalyticsClientId: credentials.clientId,
|
||||
},
|
||||
secureJsonData: {
|
||||
...options.secureJsonData,
|
||||
logAnalyticsClientSecret:
|
||||
typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
|
||||
? credentials.clientSecret
|
||||
: undefined,
|
||||
},
|
||||
secureJsonFields: {
|
||||
...options.secureJsonFields,
|
||||
logAnalyticsClientSecret: typeof credentials.clientSecret === 'symbol',
|
||||
},
|
||||
};
|
||||
// Log Analytics credentials only used if primary credentials are App Registration (client secret)
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
options = {
|
||||
...options,
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
logAnalyticsTenantId: credentials.tenantId,
|
||||
logAnalyticsClientId: credentials.clientId,
|
||||
},
|
||||
secureJsonData: {
|
||||
...options.secureJsonData,
|
||||
logAnalyticsClientSecret:
|
||||
typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
|
||||
? credentials.clientSecret
|
||||
: undefined,
|
||||
},
|
||||
secureJsonFields: {
|
||||
...options.secureJsonFields,
|
||||
logAnalyticsClientSecret: typeof credentials.clientSecret === 'symbol',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
@@ -130,16 +236,19 @@ export function updateLogAnalyticsSameAs(options: AzureDataSourceSettings, sameA
|
||||
// Get the primary credentials
|
||||
let credentials = getCredentials(options);
|
||||
|
||||
// Check whether the client secret is concealed
|
||||
if (typeof credentials.clientSecret === 'symbol') {
|
||||
// Log Analytics credentials need to be synchronized but the client secret is concealed,
|
||||
// so we have to reset the primary client secret to ensure that user enters a new secret
|
||||
credentials.clientSecret = undefined;
|
||||
options = updateCredentials(options, credentials);
|
||||
}
|
||||
// Log Analytics credentials only used if primary credentials are App Registration (client secret)
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
// Check whether the client secret is concealed
|
||||
if (typeof credentials.clientSecret === 'symbol') {
|
||||
// Log Analytics credentials need to be synchronized but the client secret is concealed,
|
||||
// so we have to reset the primary client secret to ensure that user enters a new secret
|
||||
credentials.clientSecret = undefined;
|
||||
options = updateCredentials(options, credentials);
|
||||
}
|
||||
|
||||
// Synchronize the Log Analytics credentials with primary credentials
|
||||
options = updateLogAnalyticsCredentials(options, credentials);
|
||||
// Synchronize the Log Analytics credentials with primary credentials
|
||||
options = updateLogAnalyticsCredentials(options, credentials);
|
||||
}
|
||||
|
||||
// Synchronize default subscription
|
||||
options = {
|
||||
|
||||
@@ -3,7 +3,13 @@ import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource';
|
||||
import AppInsightsDatasource from './app_insights/app_insights_datasource';
|
||||
import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
|
||||
import ResourcePickerData from './resourcePicker/resourcePickerData';
|
||||
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType, InsightsAnalyticsQuery } from './types';
|
||||
import {
|
||||
AzureDataSourceJsonData,
|
||||
AzureMonitorQuery,
|
||||
AzureQueryType,
|
||||
DatasourceValidationResult,
|
||||
InsightsAnalyticsQuery,
|
||||
} from './types';
|
||||
import {
|
||||
DataFrame,
|
||||
DataQueryRequest,
|
||||
@@ -141,8 +147,8 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async testDatasource() {
|
||||
const promises: any[] = [];
|
||||
async testDatasource(): Promise<DatasourceValidationResult> {
|
||||
const promises: Array<Promise<DatasourceValidationResult>> = [];
|
||||
|
||||
if (this.azureMonitorDatasource.isConfigured()) {
|
||||
promises.push(this.azureMonitorDatasource.testDatasource());
|
||||
@@ -164,8 +170,8 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.all(promises).then((results) => {
|
||||
let status = 'success';
|
||||
return await Promise.all(promises).then((results) => {
|
||||
let status: 'success' | 'error' = 'success';
|
||||
let message = '';
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
|
||||
@@ -10,6 +10,12 @@ import Datasource from '../datasource';
|
||||
export type AzureDataSourceSettings = DataSourceSettings<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
|
||||
export type AzureDataSourceInstanceSettings = DataSourceInstanceSettings<AzureDataSourceJsonData>;
|
||||
|
||||
export interface DatasourceValidationResult {
|
||||
status: 'success' | 'error';
|
||||
message: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export type AzureResultFormat = 'time_series' | 'table';
|
||||
|
||||
export enum AzureQueryType {
|
||||
@@ -33,9 +39,29 @@ export interface AzureMonitorQuery extends DataQuery {
|
||||
azureResourceGraph: AzureResourceGraphQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Azure clouds known to Azure Monitor.
|
||||
*/
|
||||
export enum AzureCloud {
|
||||
Public = 'AzureCloud',
|
||||
China = 'AzureChinaCloud',
|
||||
USGovernment = 'AzureUSGovernment',
|
||||
Germany = 'AzureGermanCloud',
|
||||
None = '',
|
||||
}
|
||||
|
||||
export type AzureAuthType = 'msi' | 'clientsecret';
|
||||
|
||||
export type ConcealedSecret = symbol;
|
||||
|
||||
export interface AzureCredentials {
|
||||
export type AzureCredentials = AzureManagedIdentityCredentials | AzureClientSecretCredentials;
|
||||
|
||||
export interface AzureManagedIdentityCredentials {
|
||||
authType: 'msi';
|
||||
}
|
||||
|
||||
export interface AzureClientSecretCredentials {
|
||||
authType: 'clientsecret';
|
||||
azureCloud?: string;
|
||||
tenantId?: string;
|
||||
clientId?: string;
|
||||
@@ -44,6 +70,7 @@ export interface AzureCredentials {
|
||||
|
||||
export interface AzureDataSourceJsonData extends DataSourceJsonData {
|
||||
cloudName: string;
|
||||
azureAuthType?: AzureAuthType;
|
||||
|
||||
// monitor
|
||||
tenantId?: string;
|
||||
|
||||
Reference in New Issue
Block a user