From b390052c86ca7a0a0e8326eeebea85a1c83f6a8f Mon Sep 17 00:00:00 2001 From: Aasim Khan Date: Wed, 3 Feb 2021 09:31:59 -0800 Subject: [PATCH] Exposing Azure HTTP requests method in Azurecore extension for external consumption (#14135) * Exposing azure HTTP request in azurecore Moving migration specific request from azurecore to migration Created a migrations typing file * Deleting the typings file in sql migration * Fixed typos in comments * Adding default host for azure https requests * Fixed operator in service url modification * Changed path and host logic * Made chagned requested in the PR * Fixed variable names * Adding / check for path. * Changing error code check * Fixed status logic in azure rest request Fixed comment logic Converting error array to string while throwing * Fixed status check * Fixed typos and cleaning up --- .../src/azureResource/azure-resource.d.ts | 21 ----- .../azurecore/src/azureResource/utils.ts | 94 ++++++++----------- extensions/azurecore/src/azurecore.d.ts | 29 ++++-- extensions/azurecore/src/extension.ts | 29 ++---- extensions/machine-learning/src/test/stubs.ts | 8 +- extensions/sql-migration/src/api/azure.ts | 68 ++++++++++++-- .../sql-migration/src/models/stateMachine.ts | 8 +- 7 files changed, 127 insertions(+), 130 deletions(-) diff --git a/extensions/azurecore/src/azureResource/azure-resource.d.ts b/extensions/azurecore/src/azureResource/azure-resource.d.ts index cf3ae839b1..46a080a9fa 100644 --- a/extensions/azurecore/src/azureResource/azure-resource.d.ts +++ b/extensions/azurecore/src/azureResource/azure-resource.d.ts @@ -83,26 +83,5 @@ declare module 'azureResource' { export interface BlobContainer extends AzureResource { } export interface FileShare extends AzureResource { } - - export interface MigrationControllerProperties { - name: string; - subscriptionId: string; - resourceGroup: string; - location: string; - provisioningState: string; - integrationRuntimeState?: string; - isProvisioned?: boolean; - } - - export interface MigrationController { - properties: MigrationControllerProperties; - location: string; - id: string; - name: string; - error: { - code: string, - message: string - } - } } } diff --git a/extensions/azurecore/src/azureResource/utils.ts b/extensions/azurecore/src/azureResource/utils.ts index d30b5726f9..248c59525e 100644 --- a/extensions/azurecore/src/azureResource/utils.ts +++ b/extensions/azurecore/src/azureResource/utils.ts @@ -7,7 +7,7 @@ import { ResourceGraphClient } from '@azure/arm-resourcegraph'; import { TokenCredentials } from '@azure/ms-rest-js'; import axios, { AxiosRequestConfig } from 'axios'; import * as azdata from 'azdata'; -import { HttpRequestResult, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, GetMigrationControllerResult, CreateMigrationControllerResult, GetMigrationControllerAuthKeysResult } from 'azurecore'; +import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod } from 'azurecore'; import { azureResource } from 'azureResource'; import { EOL } from 'os'; import * as nls from 'vscode-nls'; @@ -288,18 +288,19 @@ export async function getSelectedSubscriptions(appContext: AppContext, account?: return result; } -enum HttpRequestType { - GET, - POST, - PUT, - DELETE -} - /** - * Make a HTTP request to Azure REST apis. + * Makes Azure REST requests to create, retrieve, update or delete access to azure service's resources. + * For reference to different service URLs, See https://docs.microsoft.com/rest/api/?view=Azure + * @param account The azure account used to acquire access token + * @param subscription The subscription under azure account where the service will perform operations. + * @param path The path for the service starting from '/subscription/..'. See https://docs.microsoft.com/rest/api/azure/. + * @param requestType Http request method. Currently GET, PUT, POST and DELETE methods are supported. + * @param requestBody Optional request body to be used in PUT and POST requests. + * @param ignoreErrors When this flag is set the method will not throw any runtime or service errors and will return the errors in errors array. + * @param host Use this to override the host. The default host is https://management.azure.com */ -export async function makeHttpRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false, url: string, requestType: HttpRequestType, requestBody?: any): Promise { - const result: HttpRequestResult = { response: {}, errors: [] }; +export async function makeHttpRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, path: string, requestType: HttpRequestMethod, requestBody?: any, ignoreErrors: boolean = false, host: string = 'https://management.azure.com'): Promise { + const result: AzureRestResponse = { response: {}, errors: [] }; if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) { const error = new Error(invalidAzureAccount); @@ -347,24 +348,35 @@ export async function makeHttpRequest(account: azdata.Account, subscription: azu validateStatus: () => true // Never throw }; - let response; + // Adding '/' if path does not begin with it. + if (path.indexOf('/') !== 0) { + path = `/${path}`; + } + let requestUrl: string; + if (host) { + requestUrl = `${host}${path}`; + } else { + requestUrl = `https://management.azure.com${path}`; + } + + let response; switch (requestType) { - case HttpRequestType.GET: - response = await axios.get(url, config); + case HttpRequestMethod.GET: + response = await axios.get(requestUrl, config); break; - case HttpRequestType.POST: - response = await axios.post(url, requestBody, config); + case HttpRequestMethod.POST: + response = await axios.post(requestUrl, requestBody, config); break; - case HttpRequestType.PUT: - response = await axios.put(url, requestBody, config); + case HttpRequestMethod.PUT: + response = await axios.put(requestUrl, requestBody, config); break; - case HttpRequestType.DELETE: - response = await axios.delete(url, config); + case HttpRequestMethod.DELETE: + response = await axios.delete(requestUrl, config); break; } - if (response.status !== 200) { + if (response.status < 200 || response.status > 299) { let errorMessage: string[] = []; errorMessage.push(response.status.toString()); errorMessage.push(response.statusText); @@ -384,8 +396,8 @@ export async function makeHttpRequest(account: azdata.Account, subscription: azu } export async function getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise { - const apiEndpoint = `https://management.azure.com/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/blobServices/default/containers?api-version=2019-06-01`; - const response = await makeHttpRequest(account, subscription, ignoreErrors, apiEndpoint, HttpRequestType.GET); + const path = `/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/blobServices/default/containers?api-version=2019-06-01`; + const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors); return { blobContainers: response?.response?.data?.value ?? [], errors: response.errors ? response.errors : [] @@ -393,42 +405,10 @@ export async function getBlobContainers(account: azdata.Account, subscription: a } export async function getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise { - const apiEndpoint = `https://management.azure.com/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/fileServices/default/shares?api-version=2019-06-01`; - const response = await makeHttpRequest(account, subscription, ignoreErrors, apiEndpoint, HttpRequestType.GET); + const path = `/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/fileServices/default/shares?api-version=2019-06-01`; + const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors); return { fileShares: response?.response?.data?.value ?? [], errors: response.errors ? response.errors : [] }; } - -export async function getMigrationControllers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors: boolean): Promise { - const apiEndpoint = `https://${regionName}.management.azure.com/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`; - const response = await makeHttpRequest(account, subscription, false, apiEndpoint, HttpRequestType.GET); - return { - controller: response?.response?.data ?? undefined, - errors: response.errors ? response.errors : [] - }; -} - -export async function createMigrationController(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors: boolean): Promise { - const apiEndpoint = `https://${regionName}.management.azure.com/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`; - - const requestBody = { - 'location': regionName - }; - const response = await makeHttpRequest(account, subscription, ignoreErrors, apiEndpoint, HttpRequestType.PUT, requestBody); - return { - controller: response?.response?.data ?? undefined, - errors: response.errors ? response.errors : [] - }; -} - -export async function getMigrationControllerAuthKeys(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors: boolean): Promise { - const apiEndpoint = `https://${regionName}.management.azure.com/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}/ListAuthKeys?api-version=2020-09-01-preview`; - const response = await makeHttpRequest(account, subscription, ignoreErrors, apiEndpoint, HttpRequestType.POST); - return { - keyName1: response?.response?.data?.keyName1 ?? '', - keyName2: response?.response?.data?.keyName2 ?? '', - errors: response.errors ? response.errors : [] - }; -} diff --git a/extensions/azurecore/src/azurecore.d.ts b/extensions/azurecore/src/azurecore.d.ts index 39a54c1efa..9af2876cdf 100644 --- a/extensions/azurecore/src/azurecore.d.ts +++ b/extensions/azurecore/src/azurecore.d.ts @@ -66,6 +66,13 @@ declare module 'azurecore' { westus2 = 'westus2', } + export const enum HttpRequestMethod { + GET, + PUT, + POST, + DELETE + } + export interface IExtension { getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Promise; getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise; @@ -75,10 +82,18 @@ declare module 'azurecore' { getStorageAccounts(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise; getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise; getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise; - getMigrationController(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors?: boolean): Promise; - createMigrationController(account:azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors?:boolean): Promise; - getMigrationControllerAuthKeys(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, resourceGroupName: string, regionName: string, controllerName: string, ignoreErrors?: boolean): Promise; - + /** + * Makes Azure REST requests to create, retrieve, update or delete access to azure service's resources. + * For reference to different service URLs, See https://docs.microsoft.com/rest/api/?view=Azure + * @param account The azure account used to acquire access token + * @param subscription The subscription under azure account where the service will perform operations. + * @param path The path for the service starting from '/subscription/..'. See https://docs.microsoft.com/rest/api/azure/. + * @param requestType Http request method. Currently GET, PUT, POST and DELETE methods are supported. + * @param requestBody Optional request body to be used in PUT and POST requests. + * @param ignoreErrors When this flag is set the method will not throw any runtime or service errors and will return the errors in errors array. + * @param host Use this to override the host. The default host is https://management.azure.com + */ + makeAzureRestRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, path: string, requestType: HttpRequestMethod, requestBody?: any, ignoreErrors?: boolean, host?: string): Promise; /** * Converts a region value (@see AzureRegion) into the localized Display Name * @param region The region value @@ -97,10 +112,6 @@ declare module 'azurecore' { export type GetStorageAccountResult = { resources: azureResource.AzureGraphResource[], errors: Error[] }; export type GetBlobContainersResult = { blobContainers: azureResource.BlobContainer[], errors: Error[] }; export type GetFileSharesResult = { fileShares: azureResource.FileShare[], errors: Error[] }; - export type GetMigrationControllerResult = { controller: azureResource.MigrationController | undefined, errors: Error[] }; - export type CreateMigrationControllerResult = { controller: azureResource.MigrationController | undefined, errors: Error[] }; - export type GetMigrationControllerAuthKeysResult = { keyName1: string, keyName2: string, errors: Error[] }; - export type ResourceQueryResult = { resources: T[], errors: Error[] }; - export type HttpRequestResult = { response: any, errors: Error[] }; + export type AzureRestResponse = { response: any, errors: Error[] }; } diff --git a/extensions/azurecore/src/extension.ts b/extensions/azurecore/src/extension.ts index aed8018e6f..2c86cb2112 100644 --- a/extensions/azurecore/src/extension.ts +++ b/extensions/azurecore/src/extension.ts @@ -196,29 +196,14 @@ export async function activate(context: vscode.ExtensionContext): Promise { return azureResourceUtils.getFileShares(account, subscription, storageAccount, ignoreErrors); }, - getMigrationController(account: azdata.Account, + makeAzureRestRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, - resourceGroupName: string, - regionName: string, - controllerName: string, - ignoreErrors: boolean): Promise { - return azureResourceUtils.getMigrationControllers(account, subscription, resourceGroupName, regionName, controllerName, ignoreErrors); - }, - createMigrationController(account: azdata.Account, - subscription: azureResource.AzureResourceSubscription, - resourceGroupName: string, - regionName: string, - controllerName: string, - ignoreErrors: boolean): Promise { - return azureResourceUtils.createMigrationController(account, subscription, resourceGroupName, regionName, controllerName, ignoreErrors); - }, - getMigrationControllerAuthKeys(account: azdata.Account, - subscription: azureResource.AzureResourceSubscription, - resourceGroupName: string, - regionName: string, - controllerName: string, - ignoreErrors: boolean): Promise { - return azureResourceUtils.getMigrationControllerAuthKeys(account, subscription, resourceGroupName, regionName, controllerName, ignoreErrors); + path: string, + requestType: azurecore.HttpRequestMethod, + requestBody: any, + ignoreErrors: boolean, + host: string = 'https://management.azure.com'): Promise { + return azureResourceUtils.makeHttpRequest(account, subscription, path, requestType, requestBody, ignoreErrors, host); }, getRegionDisplayName: utils.getRegionDisplayName, runGraphQuery(account: azdata.Account, diff --git a/extensions/machine-learning/src/test/stubs.ts b/extensions/machine-learning/src/test/stubs.ts index 0927b9c461..97b4eeb92a 100644 --- a/extensions/machine-learning/src/test/stubs.ts +++ b/extensions/machine-learning/src/test/stubs.ts @@ -8,13 +8,7 @@ import * as azurecore from 'azurecore'; import { azureResource } from 'azureResource'; export class AzurecoreApiStub implements azurecore.IExtension { - getMigrationControllerAuthKeys(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _resourceGroupName: string, _regionName: string, _controllerName: string, _ignoreErrors?: boolean): Promise { - throw new Error('Method not implemented.'); - } - createMigrationController(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _resourceGroupName: string, _regionName: string, _controllerName: string, _ignoreErrors?: boolean): Promise { - throw new Error('Method not implemented.'); - } - getMigrationController(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _resourceGroupName: string, _regionName: string, _controllerName: string, _ignoreErrors?: boolean): Promise { + makeAzureRestRequest(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _serviceUrl: string, _requestType: azurecore.HttpRequestMethod, _requestBody?: any, _ignoreErrors?: boolean): Promise { throw new Error('Method not implemented.'); } getFileShares(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _storageAccount: azureResource.AzureGraphResource, _ignoreErrors?: boolean): Promise { diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 2beed209cf..27a0da4226 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -82,22 +82,44 @@ export async function getBlobContainers(account: azdata.Account, subscription: S return blobContainers!; } -export async function getMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise { +export async function getMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise { const api = await getAzureCoreAPI(); - let result = await api.getMigrationController(account, subscription, resourceGroupName, regionName, controllerName, true); - return result.controller!; + const host = `https://${regionName}.management.azure.com`; + const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); + if (response.errors.length > 0) { + throw response.errors.toString(); + } + + return response.response.data; } -export async function createMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise { +export async function createMigrationController(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise { const api = await getAzureCoreAPI(); - let result = await api.createMigrationController(account, subscription, resourceGroupName, regionName, controllerName, true); - return result.controller!; + const host = `https://${regionName}.management.azure.com`; + const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}?api-version=2020-09-01-preview`; + const requestBody = { + 'location': regionName + }; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host); + if (response.errors.length > 0) { + throw response.errors.toString(); + } + return response.response.data; } -export async function getMigrationControllerAuthKeys(accounts: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise { +export async function getMigrationControllerAuthKeys(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, controllerName: string): Promise { const api = await getAzureCoreAPI(); - let result = await api.getMigrationControllerAuthKeys(accounts, subscription, resourceGroupName, regionName, controllerName, true); - return result; + const host = `https://${regionName}.management.azure.com`; + const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/Controllers/${controllerName}/ListAuthKeys?api-version=2020-09-01-preview`; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, host); + if (response.errors.length > 0) { + throw response.errors.toString(); + } + return { + keyName1: response?.response?.data?.keyName1 ?? '', + keyName2: response?.response?.data?.keyName2 ?? '' + }; } /** @@ -112,7 +134,7 @@ export function getMigrationControllerRegions(): azdata.CategoryValue[] { ]; } -type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.MigrationController | azureResource.AzureResourceSubscription; +type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.AzureResourceSubscription; function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void { if (!resourceArray) { return; @@ -127,3 +149,29 @@ function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void return 0; }); } + +export interface MigrationControllerProperties { + name: string; + subscriptionId: string; + resourceGroup: string; + location: string; + provisioningState: string; + integrationRuntimeState?: string; + isProvisioned?: boolean; +} + +export interface MigrationController { + properties: MigrationControllerProperties; + location: string; + id: string; + name: string; + error: { + code: string, + message: string + } +} + +export interface GetMigrationControllerAuthKeysResult { + keyName1: string, + keyName2: string +} diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 6158b86f08..3f74cd6221 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -6,8 +6,8 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as mssql from '../../../mssql'; +import { MigrationController } from '../api/azure'; import { SKURecommendations } from './externalContract'; -import { azureResource } from 'azureResource'; export enum State { INIT, @@ -87,7 +87,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { private _assessmentResults: mssql.SqlMigrationAssessmentResultItem[] | undefined; private _azureAccount!: azdata.Account; private _databaseBackup!: DatabaseBackupModel; - private _migrationController!: azureResource.MigrationController | undefined; + private _migrationController!: MigrationController | undefined; constructor( private readonly _extensionContext: vscode.ExtensionContext, @@ -158,11 +158,11 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._stateChangeEventEmitter.event; } - public set migrationController(controller: azureResource.MigrationController | undefined) { + public set migrationController(controller: MigrationController | undefined) { this._migrationController = controller; } - public get migrationController(): azureResource.MigrationController | undefined { + public get migrationController(): MigrationController | undefined { return this._migrationController; }