Adding database backup and accounts page to migration wizard (#13764)

* Added localized strings
Created a db backup page
added radio buttons

* created components for database backup page

* Added account selection page

* Added accounts page

* Some more work done

- Added page validations
- Almost done with db backup except for a few api calls.

* Some more progress
added graph api for storage account

* Finished hooking up all the endpoints on db page.

* Some code fixed and refactoring

* Fixed a ton of validation bugs

* Added common localized strings to the constants file

* some code cleanup

* changed method name to makeHttpGetRequest

* change http result class name

* Added return types and return values to the functions

* removed void returns

* Added more return types and values

* Storing accounts in the map with ids as key
Fixed a bug in case of no subscriptions found

* cleaning up the code

* Fixed localized strings

* Added comments to get request api
Added validation logic to database backup page
removed unnecessary page validations.

* Added some get resource functions in azure core

* Changed thenable to promise

* Added arm calls for file shares and blob storage

* Added field specific validation error message

* Added examples in validation error message.

* Fixed some typings and localized string

* Added live validations to dropdowns

* Fixed method name to getSQLVMservers

* Using older storage package

* Update typings/namings (#13767)

* Update typings

* more typings fixes

* switched fileshares and blobcontainers api to http requests

* removed the extra line

* Adding resource graph documentation link as a comment

* remove makeHttpRequest api from azurecore

Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
This commit is contained in:
Aasim Khan
2020-12-11 11:54:19 -08:00
committed by GitHub
parent e82f49d390
commit d2a525bbbe
13 changed files with 1160 additions and 21 deletions

View File

@@ -8,6 +8,10 @@ declare module 'azureResource' {
import { DataProvider, Account, TreeItem } from 'azdata';
export namespace azureResource {
/**
* AzureCore core extension supports following resource types of Azure Resource Graph.
* To add more resources, please refer this guide: https://docs.microsoft.com/en-us/azure/governance/resource-graph/reference/supported-tables-resources
*/
export const enum AzureResourceType {
resourceGroup = 'microsoft.resources/subscriptions/resourcegroups',
sqlServer = 'microsoft.sql/servers',
@@ -18,7 +22,8 @@ declare module 'azureResource' {
kustoClusters = 'microsoft.kusto/clusters',
azureArcPostgresServer = 'microsoft.azuredata/postgresinstances',
postgresServer = 'microsoft.dbforpostgresql/servers',
azureArcService = 'microsoft.azuredata/datacontrollers'
azureArcService = 'microsoft.azuredata/datacontrollers',
storageAccount = 'microsoft.storage/storageaccounts',
}
export interface IAzureResourceProvider extends DataProvider {
@@ -75,7 +80,8 @@ declare module 'azureResource' {
fullName: string;
defaultDatabaseName: string;
}
export interface BlobContainer extends AzureResource { }
export interface FileShare extends AzureResource { }
}
}

View File

@@ -5,11 +5,14 @@
import { ResourceGraphClient } from '@azure/arm-resourcegraph';
import { TokenCredentials } from '@azure/ms-rest-js';
import axios, { AxiosRequestConfig } from 'axios';
import * as azdata from 'azdata';
import { GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult } from 'azurecore';
import { HttpGetRequestResult, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult } from 'azurecore';
import { azureResource } from 'azureResource';
import { EOL } from 'os';
import * as nls from 'vscode-nls';
import { AppContext } from '../appContext';
import { invalidAzureAccount, invalidTenant, unableToFetchTokenError } from '../localizedConstants';
import { AzureResourceServiceNames } from './constants';
import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionService } from './interfaces';
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
@@ -106,7 +109,7 @@ export function equals(one: any, other: any): boolean {
export async function getResourceGroups(appContext: AppContext, account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise<GetResourceGroupsResult> {
const result: GetResourceGroupsResult = { resourceGroups: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants) || !subscription) {
const error = new Error(localize('azure.accounts.getResourceGroups.invalidParamsError', "Invalid account or subscription"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -146,7 +149,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
query: string): Promise<ResourceQueryResult<T>> {
const result: ResourceQueryResult<T> = { resources: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.runResourceQuery.errors.invalidAccount', "Invalid account"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -157,7 +160,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
// Check our subscriptions to ensure we have valid ones
subscriptions.forEach(subscription => {
if (!subscription.tenant) {
const error = new Error(localize('azure.accounts.runResourceQuery.errors.noTenantSpecifiedForSubscription', "Invalid tenant for subscription"));
const error = new Error(invalidTenant);
if (!ignoreErrors) {
throw error;
}
@@ -188,7 +191,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
resourceClient = new ResourceGraphClient(credential, { baseUri: account.properties.providerSettings.settings.armResource.endpoint });
} catch (err) {
console.error(err);
const error = new Error(localize('azure.accounts.runResourceQuery.errors.unableToFetchToken', "Unable to get token for tenant {0}", tenant.id));
const error = new Error(unableToFetchTokenError(tenant.id));
result.errors.push(error);
continue;
}
@@ -227,7 +230,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
export async function getSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.getSubscriptions.invalidParamsError', "Invalid account"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -261,7 +264,7 @@ export async function getSubscriptions(appContext: AppContext, account?: azdata.
export async function getSelectedSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.getSelectedSubscriptions.invalidParamsError', "Invalid account"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -284,3 +287,102 @@ export async function getSelectedSubscriptions(appContext: AppContext, account?:
}
return result;
}
/**
* makes a GET request to Azure REST apis. Currently, it only supports GET ARM queries.
*/
export async function makeHttpGetRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false, url: string): Promise<HttpGetRequestResult> {
const result: HttpGetRequestResult = { response: {}, errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (!subscription.tenant) {
const error = new Error(invalidTenant);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (result.errors.length > 0) {
return result;
}
let securityToken: { token: string, tokenType?: string };
try {
securityToken = await azdata.accounts.getAccountSecurityToken(
account,
subscription.tenant!,
azdata.AzureResource.ResourceManagement
);
} catch (err) {
console.error(err);
const error = new Error(unableToFetchTokenError(subscription.tenant));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (result.errors.length > 0) {
return result;
}
const config: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${securityToken.token}`
},
validateStatus: () => true // Never throw
};
const response = await axios.get(url, config);
if (response.status !== 200) {
let errorMessage: string[] = [];
errorMessage.push(response.status.toString());
errorMessage.push(response.statusText);
if (response.data && response.data.error) {
errorMessage.push(`${response.data.error.code} : ${response.data.error.message}`);
}
const error = new Error(errorMessage.join(EOL));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
result.response = response;
return result;
}
export async function getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetBlobContainersResult> {
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 makeHttpGetRequest(account, subscription, ignoreErrors, apiEndpoint);
return {
blobContainers: response.response.data.value,
errors: response.errors ? response.errors : []
};
}
export async function getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetFileSharesResult> {
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 makeHttpGetRequest(account, subscription, ignoreErrors, apiEndpoint);
return {
fileShares: response.response.data.value,
errors: response.errors ? response.errors : []
};
}

View File

@@ -6,6 +6,7 @@
declare module 'azurecore' {
import * as azdata from 'azdata';
import { azureResource } from 'azureResource';
/**
* Covers defining what the azurecore extension exports to other extensions
*
@@ -66,8 +67,14 @@ declare module 'azurecore' {
}
export interface IExtension {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Thenable<GetSubscriptionsResult>;
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<GetResourceGroupsResult>;
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Promise<GetSubscriptionsResult>;
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<GetResourceGroupsResult>;
getSqlManagedInstances(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlManagedInstancesResult>;
getSqlServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlServersResult>;
getSqlVMServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlVMServersResult>;
getStorageAccounts(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetStorageAccountResult>;
getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<GetBlobContainersResult>;
getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<GetFileSharesResult>;
/**
* Converts a region value (@see AzureRegion) into the localized Display Name
* @param region The region value
@@ -80,6 +87,13 @@ declare module 'azurecore' {
export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] };
export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] };
export type GetSqlManagedInstancesResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
export type GetSqlServersResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
export type GetSqlVMServersResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
export type GetStorageAccountResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
export type GetBlobContainersResult = {blobContainers: azureResource.BlobContainer[] | undefined, errors: Error[]};
export type GetFileSharesResult = {fileShares: azureResource.FileShare[] | undefined, errors: Error[]};
export type ResourceQueryResult<T extends azureResource.AzureGraphResource> = { resources: T[], errors: Error[] };
export type HttpGetRequestResult = { response: any, errors: Error[] };
}

View File

@@ -141,12 +141,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
});
return {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Thenable<azurecore.GetSubscriptionsResult> {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Promise<azurecore.GetSubscriptionsResult> {
return selectedOnly
? azureResourceUtils.getSelectedSubscriptions(appContext, account, ignoreErrors)
: azureResourceUtils.getSubscriptions(appContext, account, ignoreErrors);
},
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
provideResources(): azureResource.IAzureResourceProvider[] {
const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures');
const providers: azureResource.IAzureResourceProvider[] = [
@@ -164,6 +164,38 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
}
return providers;
},
getSqlManagedInstances(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetSqlManagedInstancesResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`);
},
getSqlServers(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetSqlServersResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlServer}"`);
},
getSqlVMServers(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetSqlVMServersResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.virtualMachines}" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"`);
},
getStorageAccounts(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetStorageAccountResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.storageAccount}"`);
},
getBlobContainers(account: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
storageAccount: azureResource.AzureGraphResource,
ignoreErrors: boolean): Promise<azurecore.GetBlobContainersResult> {
return azureResourceUtils.getBlobContainers(account, subscription, storageAccount, ignoreErrors);
},
getFileShares(account: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
storageAccount: azureResource.AzureGraphResource,
ignoreErrors: boolean): Promise<azurecore.GetFileSharesResult> {
return azureResourceUtils.getFileShares(account, subscription, storageAccount, ignoreErrors);
},
getRegionDisplayName: utils.getRegionDisplayName,
runGraphQuery<T extends azureResource.AzureGraphResource>(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],

View File

@@ -74,3 +74,10 @@ export const azureArcPostgresServer = localize('azurecore.azureArcPostgres', "Az
export const unableToOpenAzureLink = localize('azure.unableToOpenAzureLink', "Unable to open link, missing required values");
export const azureResourcesGridTitle = localize('azure.azureResourcesGridTitle', "Azure Resources (Preview)");
// Azure Request Errors
export const invalidAzureAccount = localize('azurecore.invalidAzureAccount', "Invalid account");
export const invalidTenant = localize('azurecore.invalidTenant', "Invalid tenant for subscription");
export function unableToFetchTokenError(tenant: string): string {
return localize('azurecore.unableToFetchToken', "Unable to get token for tenant {0}", tenant);
}