diff --git a/extensions/azurecore/src/azureResource/commands.ts b/extensions/azurecore/src/azureResource/commands.ts index fb18c3e569..d68da5bd83 100644 --- a/extensions/azurecore/src/azureResource/commands.ts +++ b/extensions/azurecore/src/azureResource/commands.ts @@ -19,40 +19,71 @@ import { AzureResourceAccountTreeNode } from './tree/accountTreeNode'; import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../azureResource/interfaces'; import { AzureResourceServiceNames } from './constants'; import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService'; +import { GetSubscriptionsResult, GetResourceGroupsResult } from '../azurecore'; +import { isArray } from 'util'; export function registerAzureResourceCommands(appContext: AppContext, tree: AzureResourceTreeProvider): void { // Resource Management commands - appContext.apiWrapper.registerCommand('azure.accounts.getSubscriptions', async (account?: azdata.Account): Promise => { - if (!account) { - return []; + appContext.apiWrapper.registerCommand('azure.accounts.getSubscriptions', async (account?: azdata.Account, ignoreErrors: boolean = false): Promise => { + const result: GetSubscriptionsResult = { subscriptions: [], errors: [] }; + if (!account?.properties?.tenants || !isArray(account.properties.tenants)) { + const error = new Error('Invalid account'); + if (!ignoreErrors) { + throw error; + } + result.errors.push(error); + return result; } - const subscriptions = []; const subscriptionService = appContext.getService(AzureResourceServiceNames.subscriptionService); const tokens = await appContext.apiWrapper.getSecurityToken(account, azdata.AzureResource.ResourceManagement); - for (const tenant of account.properties.tenants) { - const token = tokens[tenant.id].token; - const tokenType = tokens[tenant.id].tokenType; + await Promise.all(account.properties.tenants.map(async (tenant: { id: string | number; }) => { + try { + const token = tokens[tenant.id].token; + const tokenType = tokens[tenant.id].tokenType; - subscriptions.push(...await subscriptionService.getSubscriptions(account, new TokenCredentials(token, tokenType))); - } - return subscriptions; + result.subscriptions.push(...await subscriptionService.getSubscriptions(account, new TokenCredentials(token, tokenType))); + } catch (err) { + console.warn(`Error fetching subscriptions for account ${account.displayInfo.displayName} tenant ${tenant.id} : ${err}`); + if (!ignoreErrors) { + throw err; + } + result.errors.push(err); + } + return Promise.resolve(); + })); + return result; }); - appContext.apiWrapper.registerCommand('azure.accounts.getResourceGroups', async (account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription): Promise => { - if (!account || !subscription) { - return []; + appContext.apiWrapper.registerCommand('azure.accounts.getResourceGroups', async (account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise => { + const result: GetResourceGroupsResult = { resourceGroups: [], errors: [] }; + if (!account?.properties?.tenants || !isArray(account.properties.tenants) || !subscription) { + const error = new Error('Invalid account or subscription'); + if (!ignoreErrors) { + throw error; + } + result.errors.push(error); + return result; } const service = new AzureResourceGroupService(); - const resourceGroups: azureResource.AzureResourceResourceGroup[] = []; - for (const tenant of account.properties.tenants) { - const tokens = await appContext.apiWrapper.getSecurityToken(account, azdata.AzureResource.ResourceManagement); - const token = tokens[tenant.id].token; - const tokenType = tokens[tenant.id].tokenType; + await Promise.all(account.properties.tenants.map(async (tenant: { id: string | number; }) => { + try { + const tokens = await appContext.apiWrapper.getSecurityToken(account, azdata.AzureResource.ResourceManagement); + const token = tokens[tenant.id].token; + const tokenType = tokens[tenant.id].tokenType; - resourceGroups.push(...await service.getResources(subscription, new TokenCredentials(token, tokenType))); - } - return resourceGroups; + result.resourceGroups.push(...await service.getResources(subscription, new TokenCredentials(token, tokenType))); + } catch (err) { + console.warn(`Error fetching resource groups for account ${account.displayInfo.displayName} (${account.displayInfo.userId}) subscription ${subscription.id} (${subscription.name}) tenant ${tenant.id} : ${err}`); + if (!ignoreErrors) { + throw err; + } + result.errors.push(err); + } + return Promise.resolve(); + })); + + return result; }); // Resource Tree commands diff --git a/extensions/azurecore/src/azureResource/providers/resourceTreeDataProviderBase.ts b/extensions/azurecore/src/azureResource/providers/resourceTreeDataProviderBase.ts index a77e179464..7cee492294 100644 --- a/extensions/azurecore/src/azureResource/providers/resourceTreeDataProviderBase.ts +++ b/extensions/azurecore/src/azureResource/providers/resourceTreeDataProviderBase.ts @@ -85,7 +85,29 @@ export async function queryGraphResources(resourceClient: R await doQuery(response.skipToken); } }; - await doQuery(); + try { + await doQuery(); + } catch (err) { + try { + if (err.response?.body) { + // The response object contains more useful error info than the error originally comes back with + const response = JSON.parse(err.response.body); + if (response.error?.details && Array.isArray(response.error.details) && response.error.details.length > 0) { + if (response.error.details[0].message) { + err.message = `${response.error.details[0].message}\n${err.message}`; + } + if (response.error.details[0].code) { + err.message = `${err.message} (${response.error.details[0].code})`; + } + } + } + } catch (err2) { + // Just log, we still want to throw the original error if something happens parsing the error + console.log(`Unexpected error while parsing error from querying resources : ${err2}`); + } + throw err; + } + return allResources; } diff --git a/extensions/azurecore/src/azurecore.d.ts b/extensions/azurecore/src/azurecore.d.ts new file mode 100644 index 0000000000..d1378d69ad --- /dev/null +++ b/extensions/azurecore/src/azurecore.d.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { azureResource } from './azureResource/azure-resource'; + +/** + * Covers defining what the azurecore extension exports to other extensions + * + * IMPORTANT: THIS IS NOT A HARD DEFINITION unlike vscode; therefore no enums or classes should be defined here + * (const enums get evaluated when typescript -> javascript so those are fine) + */ + + +export const enum extension { + name = 'Microsoft.azurecore' +} + +export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] }; +export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] }; + diff --git a/extensions/resource-deployment/src/typings/ref.d.ts b/extensions/resource-deployment/src/typings/ref.d.ts index 73d0439254..cfdf5dd135 100644 --- a/extensions/resource-deployment/src/typings/ref.d.ts +++ b/extensions/resource-deployment/src/typings/ref.d.ts @@ -6,5 +6,4 @@ /// /// /// -/// /// diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index 2214e50482..8a1d2c74dd 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -10,6 +10,7 @@ import { DialogInfoBase, FieldType, FieldInfo, SectionInfo, LabelPosition, FontW import { Model } from './model'; import { getDateTimeString } from '../utils'; import { azureResource } from '../../../azurecore/src/azureResource/azure-resource'; +import * as azurecore from '../../../azurecore/src/azurecore'; import * as loc from '../localizedConstants'; const localize = nls.loadMessageBundle(); @@ -510,8 +511,8 @@ function handleSelectedAccountChanged( } else { locationDropdown.values = []; } - vscode.commands.executeCommand('azure.accounts.getSubscriptions', selectedAccount).then(subscriptions => { - subscriptionDropdown.values = (subscriptions).map(subscription => { + vscode.commands.executeCommand('azure.accounts.getSubscriptions', selectedAccount, true /*ignoreErrors*/).then(response => { + subscriptionDropdown.values = (response).subscriptions.map(subscription => { const displayName = `${subscription.name} (${subscription.id})`; subscriptionValueToSubscriptionMap.set(displayName, subscription); return displayName; @@ -551,8 +552,8 @@ function createAzureResourceGroupsDropdown( function handleSelectedSubscriptionChanged(selectedAccount: azdata.Account | undefined, selectedSubscription: azureResource.AzureResourceSubscription | undefined, resourceGroupDropdown: azdata.DropDownComponent): void { resourceGroupDropdown.values = []; - vscode.commands.executeCommand('azure.accounts.getResourceGroups', selectedAccount, selectedSubscription).then(resourceGroups => { - resourceGroupDropdown.values = (resourceGroups).map(resourceGroup => resourceGroup.name).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase())); + vscode.commands.executeCommand('azure.accounts.getResourceGroups', selectedAccount, selectedSubscription, true /*ignoreErrors*/).then(response => { + resourceGroupDropdown.values = (response).resourceGroups.map(resourceGroup => resourceGroup.name).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase())); }, err => { vscode.window.showErrorMessage(localize('azure.accounts.unexpectedResourceGroupsError', "Unexpected error fetching resource groups for subscription {0} ({1}): {2}", selectedSubscription?.name, selectedSubscription?.id, err.message)); }); }