AzureMonitor: Retrieve Azure clouds from SDK (#87944)

* AzureMonitor: get the list of azure clouds from the SDK instead of from a hard coded list

* add azure-sdk to yarn.lock

* merge legacy with custom options

* Normalize legacy cloud names

* Workaround: send new credential format to backend while we wait to migrate to the new format

* Lint fix

* resolveLegacyCloudName should not return undefined

* re-add undefined handling in resolveLegacyCloudName so that we fallback to getDefaultAzureCloud when not defined

---------

Co-authored-by: Jeremy Angel <jeremyangel@microsoft.com>
Co-authored-by: Andreas Christou <andreas.christou@grafana.com>
This commit is contained in:
Jon Cole 2024-05-30 15:40:05 -07:00 committed by GitHub
parent 1c339db7ad
commit ba4c1fcf76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 61 additions and 59 deletions

View File

@ -4,7 +4,7 @@ import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import ResponseParser from '../azure_monitor/response_parser';
import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
import { getAuthType } from '../credentials';
import {
AzureAPIResponse,
AzureDataSourceJsonData,
@ -24,7 +24,6 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
AzureDataSourceJsonData
> {
resourcePath: string;
azurePortalUrl: string;
declare applicationId: string;
defaultSubscriptionId?: string;
@ -40,8 +39,6 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
this.resourcePath = `${routeNames.logAnalytics}`;
this.azureMonitorPath = `${routeNames.azureMonitor}/subscriptions`;
const cloud = getAzureCloud(instanceSettings);
this.azurePortalUrl = getAzurePortalUrl(cloud);
this.defaultSubscriptionId = this.instanceSettings.jsonData.subscriptionId || '';
}

View File

@ -4,7 +4,7 @@ import { find, startsWith } from 'lodash';
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
import { getAuthType } from '../credentials';
import TimegrainConverter from '../time_grain_converter';
import {
AzureDataSourceJsonData,
@ -46,7 +46,6 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
defaultSubscriptionId?: string;
basicLogsEnabled?: boolean;
resourcePath: string;
azurePortalUrl: string;
declare resourceGroup: string;
declare resourceName: string;
@ -59,9 +58,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId;
this.basicLogsEnabled = instanceSettings.jsonData.basicLogsEnabled;
const cloud = getAzureCloud(instanceSettings);
this.resourcePath = routeNames.azureMonitor;
this.azurePortalUrl = getAzurePortalUrl(cloud);
}
isConfigured(): boolean {

View File

@ -15,11 +15,6 @@ const setup = (propsFunc?: (props: Props) => Props) => {
clientId: '34509fad-c0r9-45df-9e25-f1ee34af6900',
clientSecret: undefined,
},
legacyAzureCloudOptions: [
{ value: 'azuremonitor', label: 'Azure' },
{ value: 'govazuremonitor', label: 'Azure US Government' },
{ value: 'chinaazuremonitor', label: 'Azure China' },
],
onCredentialsChange: jest.fn(),
};

View File

@ -1,5 +1,6 @@
import React, { useMemo } from 'react';
import { getAzureClouds } from '@grafana/azure-sdk';
import { SelectableValue } from '@grafana/data';
import { ConfigSection } from '@grafana/experimental';
import { Select, Field } from '@grafana/ui';
@ -16,17 +17,23 @@ export interface Props {
userIdentityEnabled: boolean;
credentials: AzureCredentials;
azureCloudOptions?: SelectableValue[];
legacyAzureCloudOptions?: SelectableValue[];
onCredentialsChange: (updatedCredentials: AzureCredentials) => void;
disabled?: boolean;
children?: JSX.Element;
}
export function getAzureCloudOptions(): Array<SelectableValue<string>> {
const cloudInfo = getAzureClouds();
return cloudInfo.map((cloud) => ({
value: cloud.name,
label: cloud.displayName,
}));
}
export const AzureCredentialsForm = (props: Props) => {
const {
credentials,
azureCloudOptions,
legacyAzureCloudOptions,
onCredentialsChange,
disabled,
managedIdentityEnabled,
@ -102,7 +109,7 @@ export const AzureCredentialsForm = (props: Props) => {
{credentials.authType === 'clientsecret' && (
<AppRegistrationCredentials
credentials={credentials}
azureCloudOptions={legacyAzureCloudOptions}
azureCloudOptions={getAzureCloudOptions()}
onCredentialsChange={onCredentialsChange}
disabled={disabled}
/>
@ -111,7 +118,7 @@ export const AzureCredentialsForm = (props: Props) => {
{credentials.authType === 'currentuser' && (
<CurrentUserFallbackCredentials
credentials={credentials}
azureCloudOptions={azureCloudOptions}
azureCloudOptions={getAzureCloudOptions()}
onCredentialsChange={onCredentialsChange}
disabled={disabled}
managedIdentityEnabled={managedIdentityEnabled}

View File

@ -7,23 +7,10 @@ import { config } from '@grafana/runtime';
import { getCredentials, updateCredentials } from '../../credentials';
import { AzureDataSourceSettings, AzureCredentials } from '../../types';
import { AzureCredentialsForm } from './AzureCredentialsForm';
import { AzureCredentialsForm, getAzureCloudOptions } from './AzureCredentialsForm';
import { BasicLogsToggle } from './BasicLogsToggle';
import { DefaultSubscription } from './DefaultSubscription';
const legacyAzureClouds: SelectableValue[] = [
{ value: 'azuremonitor', label: 'Azure' },
{ value: 'govazuremonitor', label: 'Azure US Government' },
{ value: 'chinaazuremonitor', label: 'Azure China' },
];
// This will be pulled from the azure-sdk in future
const azureClouds: SelectableValue[] = [
{ value: 'AzureCloud', label: 'Azure' },
{ value: 'AzureUSGovernment', label: 'Azure US Government' },
{ value: 'AzureChinaCloud', label: 'Azure China' },
];
export interface Props {
options: AzureDataSourceSettings;
updateOptions: (optionsFunc: (options: AzureDataSourceSettings) => AzureDataSourceSettings) => void;
@ -67,8 +54,7 @@ export const MonitorConfig = (props: Props) => {
workloadIdentityEnabled={config.azure.workloadIdentityEnabled}
userIdentityEnabled={config.azure.userIdentityEnabled}
credentials={credentials}
azureCloudOptions={azureClouds}
legacyAzureCloudOptions={legacyAzureClouds}
azureCloudOptions={getAzureCloudOptions()}
onCredentialsChange={onCredentialsChange}
disabled={props.options.readOnly}
>

View File

@ -1,3 +1,4 @@
import { getAzureClouds } from '@grafana/azure-sdk';
import { config } from '@grafana/runtime';
import {
@ -29,31 +30,43 @@ export function getAuthType(options: AzureDataSourceSettings | AzureDataSourceIn
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';
function resolveLegacyCloudName(cloudName: string | undefined): string | undefined {
if (!cloudName) {
// if undefined, allow the code to fallback to calling getDefaultAzureCloud() since that has the complete logic for handling an empty cloud name
return undefined;
}
switch (cloudName) {
case 'azuremonitor':
return AzureCloud.Public;
case 'chinaazuremonitor':
return AzureCloud.China;
case 'govazuremonitor':
return AzureCloud.USGovernment;
default:
throw new Error(`The cloud '${config.azure.cloud}' not supported.`);
return cloudName;
}
}
export function getAzurePortalUrl(azureCloud: string): string {
switch (azureCloud) {
case 'azuremonitor':
return 'https://portal.azure.com';
case 'chinaazuremonitor':
return 'https://portal.azure.cn';
case 'govazuremonitor':
return 'https://portal.azure.us';
function getDefaultAzureCloud(): string {
const cloudName = resolveLegacyCloudName(config.azure.cloud);
switch (cloudName) {
case AzureCloud.Public:
case AzureCloud.None:
return AzureCloud.Public;
case AzureCloud.China:
return AzureCloud.China;
case AzureCloud.USGovernment:
return AzureCloud.USGovernment;
default:
throw new Error('The cloud not supported.');
const cloudInfo = getAzureClouds();
for (const cloud of cloudInfo) {
if (cloud.name === config.azure.cloud) {
return cloud.name;
}
}
throw new Error(`The cloud '${config.azure.cloud}' is unsupported.`);
}
}
@ -66,7 +79,7 @@ export function getAzureCloud(options: AzureDataSourceSettings | AzureDataSource
return getDefaultAzureCloud();
case 'clientsecret':
case 'currentuser':
return options.jsonData.cloudName || getDefaultAzureCloud();
return resolveLegacyCloudName(options.jsonData.cloudName) || getDefaultAzureCloud();
}
}
@ -131,7 +144,7 @@ export function getCredentials(options: AzureDataSourceSettings): AzureCredentia
case 'clientsecret':
return {
authType,
azureCloud: options.jsonData.cloudName || getDefaultAzureCloud(),
azureCloud: resolveLegacyCloudName(options.jsonData.cloudName) || getDefaultAzureCloud(),
tenantId: options.jsonData.tenantId,
clientId: options.jsonData.clientId,
clientSecret: getSecret(options),
@ -177,7 +190,9 @@ export function updateCredentials(
jsonData: {
...options.jsonData,
azureAuthType: credentials.authType,
azureCredentials: undefined,
azureCredentials: {
authType: credentials.authType,
},
},
};
@ -189,10 +204,15 @@ export function updateCredentials(
jsonData: {
...options.jsonData,
azureAuthType: credentials.authType,
cloudName: credentials.azureCloud || getDefaultAzureCloud(),
cloudName: resolveLegacyCloudName(credentials.azureCloud) || getDefaultAzureCloud(),
tenantId: credentials.tenantId,
clientId: credentials.clientId,
azureCredentials: undefined,
azureCredentials: {
authType: credentials.authType,
azureCloud: resolveLegacyCloudName(credentials.azureCloud) || getDefaultAzureCloud(),
tenantId: credentials.tenantId,
clientId: credentials.clientId,
},
},
secureJsonData: {
...options.secureJsonData,