Fix deployment warnings to only show if no resources found (#10878)

This commit is contained in:
Charles Gagnon
2020-06-11 17:07:19 -07:00
committed by GitHub
parent 4322234d0b
commit 8db272cea4
14 changed files with 205 additions and 123 deletions

View File

@@ -113,16 +113,6 @@
"title": "%accounts.clearTokenCache%",
"category": "Azure Accounts"
},
{
"command": "azure.accounts.getSubscriptions",
"title": "%azure.accounts.getSubscriptions.title%",
"category": "Azure Accounts"
},
{
"command": "azure.accounts.getResourceGroups",
"title": "%azure.accounts.getResourceGroups.title%",
"category": "Azure Accounts"
},
{
"command": "azure.resource.signin",
"title": "%azure.resource.signin.title%",
@@ -198,14 +188,6 @@
{
"command": "azure.resource.connectsqlserver",
"when": "false"
},
{
"command": "azure.accounts.getSubscriptions",
"when": "false"
},
{
"command": "azure.accounts.getResourceGroups",
"when": "false"
}
],
"view/title": [

View File

@@ -15,8 +15,6 @@
"azure.resource.connectsqldb.title": "Add to Servers",
"accounts.clearTokenCache": "Clear Azure Account Token Cache",
"azure.accounts.getSubscriptions.title": "Get Azure Account Subscriptions",
"azure.accounts.getResourceGroups.title": "Get Azure Account Subscription Resource Groups",
"azure.openInAzurePortal.title": "Open in Azure Portal",

View File

@@ -18,9 +18,6 @@ import { AzureResourceTreeProvider } from './tree/treeProvider';
import { AzureResourceAccountTreeNode } from './tree/accountTreeNode';
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureTerminalService } from '../azureResource/interfaces';
import { AzureResourceServiceNames } from './constants';
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
import { GetSubscriptionsResult, GetResourceGroupsResult } from '../azurecore';
import { isArray } from 'util';
import { AzureAccount, Tenant } from '../account-provider/interfaces';
export function registerAzureResourceCommands(appContext: AppContext, tree: AzureResourceTreeProvider): void {
@@ -75,79 +72,6 @@ export function registerAzureResourceCommands(appContext: AppContext, tree: Azur
}
});
// Resource Management commands
appContext.apiWrapper.registerCommand('azure.accounts.getSubscriptions', async (account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> => {
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
if (!account?.properties?.tenants || !isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.getSubscriptions.invalidParamsError', "Invalid account"));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
return result;
}
const subscriptionService = appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
const tokens = await appContext.apiWrapper.getSecurityToken(account, azdata.AzureResource.ResourceManagement);
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;
result.subscriptions.push(...await subscriptionService.getSubscriptions(account, new TokenCredentials(token, tokenType)));
} catch (err) {
const error = new Error(localize('azure.accounts.getSubscriptions.queryError', "Error fetching subscriptions for account {0} tenant {1} : {2}",
account.displayInfo.displayName,
tenant.id,
err instanceof Error ? err.message : err));
console.warn(error);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
return Promise.resolve();
}));
return result;
});
appContext.apiWrapper.registerCommand('azure.accounts.getResourceGroups', async (account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise<GetResourceGroupsResult> => {
const result: GetResourceGroupsResult = { resourceGroups: [], errors: [] };
if (!account?.properties?.tenants || !isArray(account.properties.tenants) || !subscription) {
const error = new Error(localize('azure.accounts.getResourceGroups.invalidParamsError', "Invalid account or subscription"));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
return result;
}
const service = new AzureResourceGroupService();
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;
result.resourceGroups.push(...await service.getResources(subscription, new TokenCredentials(token, tokenType), account));
} catch (err) {
const error = new Error(localize('azure.accounts.getResourceGroups.queryError', "Error fetching resource groups for account {0} ({1}) subscription {2} ({3}) tenant {4} : {5}",
account.displayInfo.displayName,
account.displayInfo.userId,
subscription.id,
subscription.name,
tenant.id,
err instanceof Error ? err.message : err));
console.warn(error);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
return Promise.resolve();
}));
return result;
});
// Resource Tree commands
appContext.apiWrapper.registerCommand('azure.resource.selectsubscriptions', async (node?: TreeNode) => {

View File

@@ -16,6 +16,7 @@ export enum AzureResourceItemType {
export enum AzureResourceServiceNames {
resourceService = 'AzureResourceService',
resourceGroupService = 'AzureResourceGroupService',
cacheService = 'AzureResourceCacheService',
accountService = 'AzureResourceAccountService',
subscriptionService = 'AzureResourceSubscriptionService',

View File

@@ -3,7 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
import { azureResource } from './azure-resource';
import { GetResourceGroupsResult, GetSubscriptionsResult } from '../azurecore';
import { isArray } from 'util';
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
import { TokenCredentials } from '@azure/ms-rest-js';
import { AppContext } from '../appContext';
import { IAzureResourceSubscriptionService } from './interfaces';
import { AzureResourceServiceNames } from './constants';
const localize = nls.loadMessageBundle();
function getErrorMessage(error: Error | string): string {
@@ -92,3 +102,73 @@ export function equals(one: any, other: any): boolean {
}
return true;
}
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 || !isArray(account.properties.tenants) || !subscription) {
const error = new Error(localize('azure.accounts.getResourceGroups.invalidParamsError', "Invalid account or subscription"));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
return result;
}
const service = appContext.getService<AzureResourceGroupService>(AzureResourceServiceNames.resourceGroupService);
await Promise.all(account.properties.tenants.map(async (tenant: { id: string | number; }) => {
try {
const tokens = await azdata.accounts.getSecurityToken(account, azdata.AzureResource.ResourceManagement);
const token = tokens[tenant.id].token;
const tokenType = tokens[tenant.id].tokenType;
result.resourceGroups.push(...await service.getResources(subscription, new TokenCredentials(token, tokenType), account));
} catch (err) {
const error = new Error(localize('azure.accounts.getResourceGroups.queryError', "Error fetching resource groups for account {0} ({1}) subscription {2} ({3}) tenant {4} : {5}",
account.displayInfo.displayName,
account.displayInfo.userId,
subscription.id,
subscription.name,
tenant.id,
err instanceof Error ? err.message : err));
console.warn(error);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
}));
return result;
}
export async function getSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
if (!account?.properties?.tenants || !isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.getSubscriptions.invalidParamsError', "Invalid account"));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
return result;
}
const subscriptionService = appContext.getService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService);
const tokens = await appContext.apiWrapper.getSecurityToken(account, azdata.AzureResource.ResourceManagement);
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;
result.subscriptions.push(...await subscriptionService.getSubscriptions(account, new TokenCredentials(token, tokenType)));
} catch (err) {
const error = new Error(localize('azure.accounts.getSubscriptions.queryError', "Error fetching subscriptions for account {0} tenant {1} : {2}",
account.displayInfo.displayName,
tenant.id,
err instanceof Error ? err.message : err));
console.warn(error);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
}));
return result;
}

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { azureResource } from './azureResource/azure-resource';
/**
@@ -62,6 +63,8 @@ export const enum AzureRegion {
}
export interface IExtension {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean): Thenable<GetSubscriptionsResult>;
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<GetResourceGroupsResult>;
/**
* Converts a region value (@see AzureRegion) into the localized Display Name
* @param region The region value

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { promises as fs } from 'fs';
import * as path from 'path';
@@ -37,8 +38,10 @@ import { PostgresServerArcProvider } from './azureResource/providers/postgresArc
import { PostgresServerArcService } from './azureResource/providers/postgresArcServer/postgresServerService';
import { azureResource } from './azureResource/azure-resource';
import * as azurecore from './azurecore';
import * as azureResourceUtils from './azureResource/utils';
import * as utils from './utils';
import * as loc from './localizedConstants';
import { AzureResourceGroupService } from './azureResource/providers/resourceGroup/resourceGroupService';
let extensionContext: vscode.ExtensionContext;
@@ -84,6 +87,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
registerAzureResourceCommands(appContext, azureResourceTree);
return {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean): Thenable<azurecore.GetSubscriptionsResult> { return azureResourceUtils.getSubscriptions(appContext, account, ignoreErrors); },
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
provideResources(): azureResource.IAzureResourceProvider[] {
const arcFeaturedEnabled = apiWrapper.getExtensionConfiguration().get('enableArcFeatures');
const providers: azureResource.IAzureResourceProvider[] = [
@@ -144,6 +149,7 @@ async function initAzureAccountProvider(extensionContext: vscode.ExtensionContex
function registerAzureServices(appContext: AppContext): void {
appContext.registerService<AzureResourceService>(AzureResourceServiceNames.resourceService, new AzureResourceService());
appContext.registerService<AzureResourceGroupService>(AzureResourceServiceNames.resourceGroupService, new AzureResourceGroupService());
appContext.registerService<IAzureResourceAccountService>(AzureResourceServiceNames.accountService, new AzureResourceAccountService(appContext.apiWrapper));
appContext.registerService<IAzureResourceCacheService>(AzureResourceServiceNames.cacheService, new AzureResourceCacheService(extensionContext));
appContext.registerService<IAzureResourceSubscriptionService>(AzureResourceServiceNames.subscriptionService, new AzureResourceSubscriptionService());

View File

@@ -5,6 +5,7 @@
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as azurecore from '../../../azurecore/src/azurecore';
/**
* Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into
@@ -137,4 +138,16 @@ export class ApiWrapper {
public registerWidget(widgetId: string, handler: (view: azdata.ModelView) => void): void {
azdata.ui.registerModelViewProvider(widgetId, handler);
}
private azurecoreApi: azurecore.IExtension | undefined;
public async getAzurecoreApi(): Promise<azurecore.IExtension> {
if (!this.azurecoreApi) {
this.azurecoreApi = await this.getExtension(azurecore.extension.name)?.activate();
if (!this.azurecoreApi) {
throw new Error('Unable to retrieve azurecore API');
}
}
return this.azurecoreApi;
}
}

View File

@@ -52,7 +52,7 @@ export class AzureModelRegistryService {
* @param account azure account
*/
public async getSubscriptions(account: azdata.Account | undefined): Promise<azureResource.AzureResourceSubscription[] | undefined> {
const data = <azureResource.GetSubscriptionsResult>await this._apiWrapper.executeCommand(constants.azureSubscriptionsCommand, account, true);
const data: azureResource.GetSubscriptionsResult = await (await this._apiWrapper.getAzurecoreApi()).getSubscriptions(account, true);
return data?.subscriptions;
}
@@ -64,7 +64,7 @@ export class AzureModelRegistryService {
public async getGroups(
account: azdata.Account | undefined,
subscription: azureResource.AzureResourceSubscription | undefined): Promise<azureResource.AzureResource[] | undefined> {
const data = <azureResource.GetResourceGroupsResult>await this._apiWrapper.executeCommand(constants.azureResourceGroupsCommand, account, subscription, true);
const data: azureResource.GetResourceGroupsResult = await (await this._apiWrapper.getAzurecoreApi()).getResourceGroups(account, subscription, true);
return data?.resourceGroups;
}

View File

@@ -18,6 +18,7 @@ import { Workspace, WorkspacesListByResourceGroupResponse } from '@azure/arm-mac
import { WorkspaceModel, AssetsQueryByIdResponse, Asset, GetArtifactContentInformation2Response } from '../../modelManagement/interfaces';
import { AzureMachineLearningWorkspaces, Workspaces } from '@azure/arm-machinelearningservices';
import { WorkspaceModels } from '../../modelManagement/workspacesModels';
import { AzurecoreApiStub } from '../stubs';
interface TestContext {
@@ -129,7 +130,9 @@ describe('AzureModelRegistryService', () => {
testContext.config.object,
testContext.httpClient.object,
testContext.outputChannel);
testContext.apiWrapper.setup(x => x.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ subscriptions: expected, errors: [] }));
const azurecoreApi = TypeMoq.Mock.ofType(AzurecoreApiStub);
azurecoreApi.setup(x => x.getSubscriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ subscriptions: expected, errors: [] }));
testContext.apiWrapper.setup(x => x.getAzurecoreApi()).returns(() => Promise.resolve(azurecoreApi.object));
let actual = await service.getSubscriptions(testContext.accounts[0]);
should.deepEqual(actual, expected);
});
@@ -142,7 +145,9 @@ describe('AzureModelRegistryService', () => {
testContext.config.object,
testContext.httpClient.object,
testContext.outputChannel);
testContext.apiWrapper.setup(x => x.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ resourceGroups: expected, errors: [] }));
const azurecoreApi = TypeMoq.Mock.ofType(AzurecoreApiStub);
azurecoreApi.setup(x => x.getResourceGroups(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ resourceGroups: expected, errors: [] }));
testContext.apiWrapper.setup(x => x.getAzurecoreApi()).returns(() => Promise.resolve(azurecoreApi.object));
let actual = await service.getGroups(testContext.accounts[0], testContext.subscriptions[0]);
should.deepEqual(actual, expected);
});

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as azurecore from '../../../azurecore/src/azurecore';
import { azureResource } from '../../../azurecore/src/azureResource/azure-resource';
export class AzurecoreApiStub implements azurecore.IExtension {
getSubscriptions(_account?: azdata.Account | undefined, _ignoreErrors?: boolean | undefined): Thenable<azurecore.GetSubscriptionsResult> {
throw new Error('Method not implemented.');
}
getResourceGroups(_account?: azdata.Account | undefined, _subscription?: azureResource.AzureResourceSubscription | undefined, _ignoreErrors?: boolean | undefined): Thenable<azurecore.GetResourceGroupsResult> {
throw new Error('Method not implemented.');
}
getRegionDisplayName(_region?: string | undefined): string {
throw new Error('Method not implemented.');
}
provideResources(): azureResource.IAzureResourceProvider[] {
throw new Error('Method not implemented.');
}
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azurecore from '../../../azurecore/src/azurecore';
import * as vscode from 'vscode';
export interface IApiService {
getAzurecoreApi(): Promise<azurecore.IExtension>;
}
class ApiService implements IApiService {
private azurecoreApi: azurecore.IExtension | undefined;
constructor() { }
public async getAzurecoreApi(): Promise<azurecore.IExtension> {
if (!this.azurecoreApi) {
this.azurecoreApi = <azurecore.IExtension>(await vscode.extensions.getExtension(azurecore.extension.name)?.activate());
if (!this.azurecoreApi) {
throw new Error('Unable to retrieve azurecore API');
}
}
return this.azurecoreApi;
}
}
export const apiService: IApiService = new ApiService();

View File

@@ -16,6 +16,7 @@ import { assert, getDateTimeString, getErrorMessage } from '../utils';
import { WizardInfoBase } from './../interfaces';
import { Model } from './model';
import { RadioGroupLoadingComponentBuilder } from './radioGroupLoadingComponentBuilder';
import { apiService } from '../services/apiService';
const localize = nls.loadMessageBundle();
@@ -839,16 +840,24 @@ async function handleSelectedAccountChanged(
}
try {
const response = await vscode.commands.executeCommand<azurecore.GetSubscriptionsResult>('azure.accounts.getSubscriptions', selectedAccount, true /*ignoreErrors*/);
const response = await (await apiService.getAzurecoreApi()).getSubscriptions(selectedAccount, true);
if (!response) {
return;
}
if (response.errors.length > 0) {
// If we got back some subscriptions then don't display the errors to the user - it's normal for users
// to not necessarily have access to all subscriptions on an account so displaying the errors
// in that case is usually just distracting and causes confusion
const errMsg = response.errors.join(EOL);
if (response.subscriptions.length === 0) {
context.container.message = {
text: response.errors.join(EOL) || '',
text: errMsg || '',
description: '',
level: azdata.window.MessageLevel.Warning
level: azdata.window.MessageLevel.Error
};
} else {
console.log(errMsg);
}
}
subscriptionDropdown.values = response.subscriptions.map(subscription => {
const displayName = `${subscription.name} (${subscription.id})`;
@@ -906,17 +915,24 @@ async function handleSelectedSubscriptionChanged(context: AzureAccountFieldConte
return;
}
try {
const response = await vscode.commands.executeCommand<azurecore.GetResourceGroupsResult>('azure.accounts.getResourceGroups', selectedAccount, selectedSubscription, true /*ignoreErrors*/);
//.then(response => {
const response = await (await apiService.getAzurecoreApi()).getResourceGroups(selectedAccount, selectedSubscription, true);
if (!response) {
return;
}
if (response.errors.length > 0) {
// If we got back some RG's then don't display the errors to the user - it's normal for users
// to not necessarily have access to all RG's on a subscription so displaying the errors
// in that case is usually just distracting and causes confusion
const errMsg = response.errors.join(EOL);
if (response.resourceGroups.length === 0) {
context.container.message = {
text: response.errors.join(EOL) || '',
text: errMsg || '',
description: '',
level: azdata.window.MessageLevel.Warning
level: azdata.window.MessageLevel.Error
};
} else {
console.log(errMsg);
}
}
resourceGroupDropdown.values = (response.resourceGroups.length !== 0)
? response.resourceGroups.map(resourceGroup => resourceGroup.name).sort((a: string, b: string) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))

2
src/sql/azdata.d.ts vendored
View File

@@ -2132,7 +2132,7 @@ declare module 'azdata' {
* AzureResource.ResourceManagement if not given)
* @return Promise to return the security token
*/
export function getSecurityToken(account: Account, resource?: AzureResource): Thenable<{}>;
export function getSecurityToken(account: Account, resource?: AzureResource): Thenable<{ [key: string]: any }>;
/**
* An [event](#Event) which fires when the accounts have changed.