Add azurecore HTTP typings (#22828)

* Add azurecore HTTP typings

* undo + spelling fix
This commit is contained in:
Charles Gagnon
2023-04-24 10:39:02 -07:00
committed by GitHub
parent e5e8824d34
commit 7f388dd420
7 changed files with 158 additions and 118 deletions

View File

@@ -3,10 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AzureNetworkResponse } from 'azurecore';
import * as http from 'http';
import * as https from 'https';
import { TextEncoder } from 'util';
import { getNetworkResponse, NetworkRequestOptions, NetworkResponse, urlToHttpOptions } from './networkUtils';
import { NetworkRequestOptions, urlToHttpOptions } from './networkUtils';
/**
* http methods
@@ -59,7 +60,7 @@ export class HttpClient {
url: string,
options?: NetworkRequestOptions,
cancellationToken?: number | undefined
): Promise<NetworkResponse<T>> {
): Promise<AzureNetworkResponse<T>> {
if (this.proxyUrl) {
return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.GET, options, this.customAgentOptions as http.AgentOptions, cancellationToken);
} else {
@@ -76,7 +77,7 @@ export class HttpClient {
url: string,
options?: NetworkRequestOptions,
cancellationToken?: number
): Promise<NetworkResponse<T>> {
): Promise<AzureNetworkResponse<T>> {
if (this.proxyUrl) {
return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.POST, options, this.customAgentOptions as http.AgentOptions, cancellationToken);
} else {
@@ -93,7 +94,7 @@ export class HttpClient {
url: string,
options?: NetworkRequestOptions,
cancellationToken?: number
): Promise<NetworkResponse<T>> {
): Promise<AzureNetworkResponse<T>> {
if (this.proxyUrl) {
return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.PUT, options, this.customAgentOptions as http.AgentOptions, cancellationToken);
} else {
@@ -109,7 +110,7 @@ export class HttpClient {
async sendDeleteRequestAsync<T>(
url: string,
options?: NetworkRequestOptions
): Promise<NetworkResponse<T>> {
): Promise<AzureNetworkResponse<T>> {
if (this.proxyUrl) {
return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.DELETE, options, this.customAgentOptions as http.AgentOptions);
} else {
@@ -126,7 +127,7 @@ const networkRequestViaProxy = <T>(
options?: NetworkRequestOptions,
agentOptions?: http.AgentOptions,
timeout?: number
): Promise<NetworkResponse<T>> => {
): Promise<AzureNetworkResponse<T>> => {
const destinationUrl = new URL(destinationUrlString);
const proxyUrl = new URL(proxyUrlString);
@@ -164,7 +165,7 @@ const networkRequestViaProxy = <T>(
postRequestStringContent +
'\r\n';
return new Promise<NetworkResponse<T>>(((resolve, reject) => {
return new Promise<AzureNetworkResponse<T>>(((resolve, reject) => {
const request = http.request(tunnelRequestOptions);
if (tunnelRequestOptions.timeout) {
@@ -246,11 +247,11 @@ const networkRequestViaProxy = <T>(
});
const parsedHeaders = Object.fromEntries(entries) as Record<string, string>;
const networkResponse = getNetworkResponse(
parsedHeaders,
parseBody(httpStatusCode, statusMessage, parsedHeaders, body) as T,
httpStatusCode
);
const networkResponse: AzureNetworkResponse<T> = {
headers: parsedHeaders,
data: parseBody(httpStatusCode, statusMessage, parsedHeaders, body) as T,
status: httpStatusCode
};
if (((httpStatusCode < HttpStatus.SUCCESS_RANGE_START) || (httpStatusCode > HttpStatus.SUCCESS_RANGE_END)) &&
@@ -282,7 +283,7 @@ const networkRequestViaHttps = <T>(
options?: NetworkRequestOptions,
agentOptions?: https.AgentOptions,
timeout?: number
): Promise<NetworkResponse<T>> => {
): Promise<AzureNetworkResponse<T>> => {
const isPostRequest = httpMethod === HttpMethod.POST;
const isPutRequest = httpMethod === HttpMethod.PUT;
// Note: Text Encoder is necessary here because otherwise it was not able to handle Chinese characters in table names.
@@ -311,13 +312,13 @@ const networkRequestViaHttps = <T>(
};
}
return new Promise<NetworkResponse<T>>((resolve, reject) => {
return new Promise<AzureNetworkResponse<T>>((resolve, reject) => {
const request = https.request(customOptions);
if (timeout) {
request.on('timeout', () => {
request.destroy();
reject(new Error('Request time out'));
reject(new Error('Request timed out'));
});
}
@@ -342,11 +343,11 @@ const networkRequestViaHttps = <T>(
const dataBody = Buffer.concat([...data]).toString();
const parsedHeaders = headers as Record<string, string>;
const networkResponse = getNetworkResponse(
parsedHeaders,
parseBody(statusCode, statusMessage, parsedHeaders, dataBody) as T,
statusCode
);
const networkResponse: AzureNetworkResponse<T> = {
headers: parsedHeaders,
data: parseBody(statusCode, statusMessage, parsedHeaders, dataBody) as T,
status: statusCode
};
if (((statusCode < HttpStatus.SUCCESS_RANGE_START) || (statusCode > HttpStatus.SUCCESS_RANGE_END)) &&
// do not destroy the request for the device code flow
@@ -401,7 +402,7 @@ const parseBody = (statusCode: number, statusMessage: string | undefined, header
parsedBody = {
error: errorType,
error_description: `${errorDescriptionHelper} error occured.\nHttp status code: ${statusCode}\nHttp status message: ${statusMessage || 'Unknown'}\nHeaders: ${JSON.stringify(headers)}`
error_description: `${errorDescriptionHelper} error occurred.\nHttp status code: ${statusCode}\nHttp status message: ${statusMessage || 'Unknown'}\nHeaders: ${JSON.stringify(headers)}`
};
}

View File

@@ -6,20 +6,6 @@
import * as azdata from 'azdata';
import * as https from 'https';
export function getNetworkResponse<Body>(headers: Record<string, string>, body: Body, statusCode: number): NetworkResponse<Body> {
return {
headers: headers,
data: body,
status: statusCode
};
}
export declare type NetworkResponse<T> = {
headers: Record<string, string>;
data: T;
status: number;
};
export declare type NetworkRequestOptions = {
headers?: Record<string, string>;
body?: string;

View File

@@ -6,7 +6,7 @@
import { ResourceGraphClient } from '@azure/arm-resourcegraph';
import { TokenCredentials } from '@azure/ms-rest-js';
import * as azdata from 'azdata';
import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod, GetLocationsResult, GetManagedDatabasesResult, CreateResourceGroupResult, GetBlobsResult, GetStorageAccountAccessKeyResult, AzureAccount, azureResource, AzureAccountProviderMetadata } from 'azurecore';
import { AzureRestResponse, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult, HttpRequestMethod, GetLocationsResult, GetManagedDatabasesResult, CreateResourceGroupResult, GetBlobsResult, GetStorageAccountAccessKeyResult, AzureAccount, azureResource, AzureAccountProviderMetadata, AzureNetworkResponse, HttpClientResponse } from 'azurecore';
import { EOL } from 'os';
import * as nls from 'vscode-nls';
import { AppContext } from '../appContext';
@@ -23,11 +23,31 @@ import { NetworkRequestOptions } from '@azure/msal-common';
const localize = nls.loadMessageBundle();
export interface HttpClientResponse {
body: any;
headers: any;
status: Number;
error: any;
/**
* Shape of list operation responses
* e.g. https://learn.microsoft.com/en-us/rest/api/storagerp/srp_json_list_operations#response-body
*/
declare type AzureListOperationResponse<T> = {
value: T;
}
/**
* An access key for the storage account.
* https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/list-keys?tabs=HTTP#storageaccountkey
*/
declare type StorageAccountKey = {
creationTime: string;
keyName: string;
// permissions: KeyPermission Can add this if ever needed
value: string;
}
/**
* The response from the ListKeys operation.
* https://learn.microsoft.com/en-us/rest/api/storagerp/storage-accounts/list-keys?tabs=HTTP#storageaccountlistkeysresult
*/
declare type StorageAccountListKeysResult = {
keys: StorageAccountKey[];
}
function getErrorMessage(error: Error | string): string {
@@ -170,8 +190,8 @@ export async function getLocations(appContext: AppContext, account?: AzureAccoun
try {
const path = `/subscriptions/${subscription.id}/locations?api-version=2020-01-01`;
const host = getProviderMetadataForAccount(account).settings.armResource.endpoint;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors, host);
result.locations.push(...response.response.data.value);
const response = await makeHttpRequest<AzureListOperationResponse<azureResource.AzureLocation[]>>(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors, host);
result.locations.push(...response.response?.data.value || []);
result.errors.push(...response.errors);
} catch (err) {
const error = new Error(localize('azure.accounts.getLocations.queryError', "Error fetching locations for account {0} ({1}) subscription {2} ({3}) tenant {4} : {5}",
@@ -348,8 +368,17 @@ export async function getSelectedSubscriptions(appContext: AppContext, account?:
* @param host Use this to override the host. The default host is https://management.azure.com
* @param requestHeaders Provide additional request headers
*/
export async function makeHttpRequest(account: AzureAccount, subscription: azureResource.AzureResourceSubscription, path: string, requestType: HttpRequestMethod, requestBody?: any, ignoreErrors: boolean = false, host: string = 'https://management.azure.com', requestHeaders: { [key: string]: string } = {}): Promise<AzureRestResponse> {
const result: AzureRestResponse = { response: {}, errors: [] };
export async function makeHttpRequest<B>(
account: AzureAccount,
subscription: azureResource.AzureResourceSubscription,
path: string,
requestType: HttpRequestMethod,
requestBody?: any,
ignoreErrors: boolean = false,
host: string = 'https://management.azure.com',
requestHeaders: Record<string, string> = {}
): Promise<AzureRestResponse<B>> {
const result: AzureRestResponse<B> = { response: undefined, errors: [] };
const httpClient: HttpClient = getProxyEnabledHttpClient();
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
@@ -414,21 +443,21 @@ export async function makeHttpRequest(account: AzureAccount, subscription: azure
requestUrl = `${account.properties.providerSettings.settings.armResource.endpoint}${path}`;
}
let response;
let response: AzureNetworkResponse<HttpClientResponse<B>> | undefined = undefined;
switch (requestType) {
case HttpRequestMethod.GET:
response = await httpClient.sendGetRequestAsync<any>(requestUrl, {
response = await httpClient.sendGetRequestAsync<HttpClientResponse<B>>(requestUrl, {
headers: reqHeaders
});
break;
case HttpRequestMethod.POST:
response = await httpClient.sendPostRequestAsync<HttpClientResponse>(requestUrl, networkRequestOptions);
response = await httpClient.sendPostRequestAsync<HttpClientResponse<B>>(requestUrl, networkRequestOptions);
break;
case HttpRequestMethod.PUT:
response = await httpClient.sendPutRequestAsync<HttpClientResponse>(requestUrl, networkRequestOptions);
response = await httpClient.sendPutRequestAsync<HttpClientResponse<B>>(requestUrl, networkRequestOptions);
break;
case HttpRequestMethod.DELETE:
response = await httpClient.sendDeleteRequestAsync<any>(requestUrl, {
response = await httpClient.sendDeleteRequestAsync<HttpClientResponse<B>>(requestUrl, {
headers: reqHeaders
});
break;
@@ -459,7 +488,13 @@ export async function makeHttpRequest(account: AzureAccount, subscription: azure
result.errors.push(error);
}
result.response = response;
if (response) {
result.response = {
headers: response.headers,
data: response.data.body,
status: response.status
};
}
return result;
}
@@ -467,7 +502,7 @@ export async function makeHttpRequest(account: AzureAccount, subscription: azure
export async function getManagedDatabases(account: AzureAccount, subscription: azureResource.AzureResourceSubscription, managedInstance: azureResource.AzureSqlManagedInstance, ignoreErrors: boolean): Promise<GetManagedDatabasesResult> {
const path = `/subscriptions/${subscription.id}/resourceGroups/${managedInstance.resourceGroup}/providers/Microsoft.Sql/managedInstances/${managedInstance.name}/databases?api-version=2020-02-02-preview`;
const host = getProviderMetadataForAccount(account).settings.armResource.endpoint;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors, host);
const response = await makeHttpRequest<AzureListOperationResponse<azureResource.ManagedDatabase[]>>(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors, host);
return {
databases: response?.response?.data?.value ?? [],
errors: response.errors ? response.errors : []
@@ -477,7 +512,7 @@ export async function getManagedDatabases(account: AzureAccount, subscription: a
export async function getBlobContainers(account: AzureAccount, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetBlobContainersResult> {
const path = `/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/blobServices/default/containers?api-version=2019-06-01`;
const host = getProviderMetadataForAccount(account).settings.armResource.endpoint;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors, host);
const response = await makeHttpRequest<AzureListOperationResponse<azureResource.BlobContainer[]>>(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors, host);
return {
blobContainers: response?.response?.data?.value ?? [],
errors: response.errors ? response.errors : []
@@ -487,7 +522,7 @@ export async function getBlobContainers(account: AzureAccount, subscription: azu
export async function getFileShares(account: AzureAccount, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetFileSharesResult> {
const path = `/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/fileServices/default/shares?api-version=2019-06-01`;
const host = getProviderMetadataForAccount(account).settings.armResource.endpoint;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors, host);
const response = await makeHttpRequest<AzureListOperationResponse<azureResource.FileShare[]>>(account, subscription, path, HttpRequestMethod.GET, undefined, ignoreErrors, host);
return {
fileShares: response?.response?.data?.value ?? [],
errors: response.errors ? response.errors : []
@@ -500,7 +535,7 @@ export async function createResourceGroup(account: AzureAccount, subscription: a
location: location
};
const host = getProviderMetadataForAccount(account).settings.armResource.endpoint;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.PUT, requestBody, ignoreErrors, host);
const response = await makeHttpRequest<azureResource.AzureResourceResourceGroup>(account, subscription, path, HttpRequestMethod.PUT, requestBody, ignoreErrors, host);
return {
resourceGroup: response?.response?.data,
errors: response.errors ? response.errors : []
@@ -510,7 +545,7 @@ export async function createResourceGroup(account: AzureAccount, subscription: a
export async function getStorageAccountAccessKey(account: AzureAccount, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetStorageAccountAccessKeyResult> {
const path = `/subscriptions/${subscription.id}/resourceGroups/${storageAccount.resourceGroup}/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}/listKeys?api-version=2019-06-01`;
const host = getProviderMetadataForAccount(account).settings.armResource.endpoint;
const response = await makeHttpRequest(account, subscription, path, HttpRequestMethod.POST, undefined, ignoreErrors, host);
const response = await makeHttpRequest<StorageAccountListKeysResult>(account, subscription, path, HttpRequestMethod.POST, undefined, ignoreErrors, host);
return {
keyName1: response?.response?.data?.keys[0].value ?? '',
keyName2: response?.response?.data?.keys[0].value ?? '',

View File

@@ -276,6 +276,24 @@ declare module 'azurecore' {
DELETE
}
/**
* Custom version of NetworkResponse from @azure\msal-common\dist\network\NetworkManager.d.ts
* with body renamed to data to avoid breaking changes with extensions. See
* https://github.com/microsoft/azuredatastudio/pull/22761 for details.
*/
export type AzureNetworkResponse<T> = {
headers: Record<string, string>;
data: T;
status: number;
};
export interface HttpClientResponse<B> {
body: B;
headers: any;
status: Number;
error: any;
}
export interface IExtension {
/**
* Gets the list of subscriptions for the specified AzureAccount
@@ -308,7 +326,7 @@ declare module 'azurecore' {
* @param host Use this to override the host. The default host is https://management.azure.com
* @param requestHeaders Provide additional request headers
*/
makeAzureRestRequest(account: AzureAccount, subscription: azureResource.AzureResourceSubscription, path: string, requestType: HttpRequestMethod, requestBody?: any, ignoreErrors?: boolean, host?: string, requestHeaders?: { [key: string]: string }): Promise<AzureRestResponse>;
makeAzureRestRequest<B>(account: AzureAccount, subscription: azureResource.AzureResourceSubscription, path: string, requestType: HttpRequestMethod, requestBody?: any, ignoreErrors?: boolean, host?: string, requestHeaders?: Record<string, string>): Promise<AzureRestResponse<B>>;
/**
* Converts a region value (@see AzureRegion) into the localized Display Name
* @param region The region value
@@ -339,9 +357,9 @@ 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 CreateResourceGroupResult = { resourceGroup: azureResource.AzureResourceResourceGroup, errors: Error[] };
export type CreateResourceGroupResult = { resourceGroup: azureResource.AzureResourceResourceGroup | undefined, errors: Error[] };
export type ResourceQueryResult<T extends azureResource.AzureGraphResource> = { resources: T[], errors: Error[] };
export type AzureRestResponse = { response: any, errors: Error[] };
export type AzureRestResponse<B> = { response: AzureNetworkResponse<B> | undefined, errors: Error[] };
export type GetBlobsResult = { blobs: azureResource.Blob[], errors: Error[] };
export type GetStorageAccountAccessKeyResult = { keyName1: string, keyName2: string, errors: Error[] };
export type CacheEncryptionKeys = { key: string; iv: string; }

View File

@@ -230,14 +230,14 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
ignoreErrors: boolean): Promise<azurecore.CreateResourceGroupResult> {
return azureResourceUtils.createResourceGroup(account, subscription, resourceGroupName, location, ignoreErrors);
},
makeAzureRestRequest(account: azurecore.AzureAccount,
makeAzureRestRequest<B>(account: azurecore.AzureAccount,
subscription: azurecore.azureResource.AzureResourceSubscription,
path: string,
requestType: azurecore.HttpRequestMethod,
requestBody: any,
ignoreErrors: boolean,
host: string = 'https://management.azure.com',
requestHeaders: { [key: string]: string } = {}): Promise<azurecore.AzureRestResponse> {
requestHeaders: Record<string, string> = {}): Promise<azurecore.AzureRestResponse<B>> {
return azureResourceUtils.makeHttpRequest(account, subscription, path, requestType, requestBody, ignoreErrors, host, requestHeaders);
},
getRegionDisplayName: utils.getRegionDisplayName,