Change azdata executions to point to kube config and cluster contexts (#13569)

This commit is contained in:
Arvind Ranasaria
2020-12-23 23:45:00 -08:00
committed by GitHub
parent cf7c506d75
commit f314a9e1e6
24 changed files with 180 additions and 114 deletions

View File

@@ -2,7 +2,8 @@
"metadata": {
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
"display_name": "Python 3",
"language": "python"
},
"language_info": {
"name": "python",

View File

@@ -2,7 +2,8 @@
"metadata": {
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
"display_name": "Python 3",
"language": "python"
},
"language_info": {
"name": "python",

View File

@@ -663,6 +663,8 @@
"variableNames": {
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
"kubeConfig": "AZDATA_NB_VAR_CONTROLLER_KUBECONFIG",
"clusterContext": "AZDATA_NB_VAR_CONTROLLER_KUBECTL_CONTEXT",
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
}
},
@@ -855,6 +857,8 @@
"variableNames": {
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
"kubeConfig": "AZDATA_NB_VAR_CONTROLLER_KUBECONFIG",
"clusterContext": "AZDATA_NB_VAR_CONTROLLER_KUBECTL_CONTEXT",
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
}
},

View File

@@ -28,14 +28,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
});
vscode.commands.registerCommand('arc.connectToController', async () => {
const nodes = await treeDataProvider.getChildren();
if (nodes.length > 0) {
const response = await vscode.window.showErrorMessage(loc.onlyOneControllerSupported, loc.yes, loc.no);
if (response !== loc.yes) {
return;
}
await treeDataProvider.removeController(nodes[0] as ControllerTreeNode);
}
const dialog = new ConnectToControllerDialog(treeDataProvider);
dialog.showDialog();
const model = await dialog.waitForClose();

View File

@@ -196,9 +196,8 @@ export function couldNotFindAzureResource(name: string): string { return localiz
export function passwordResetFailed(error: any): string { return localize('arc.passwordResetFailed', "Failed to reset password. {0}", getErrorMessage(error)); }
export function errorConnectingToController(error: any): string { return localize('arc.errorConnectingToController', "Error connecting to controller. {0}", getErrorMessage(error, true)); }
export function passwordAcquisitionFailed(error: any): string { return localize('arc.passwordAcquisitionFailed', "Failed to acquire password. {0}", getErrorMessage(error)); }
export const invalidPassword = localize('arc.invalidPassword', "The password did not work, try again.");
export const loginFailed = localize('arc.loginFailed', "Error logging into controller, try again.");
export function errorVerifyingPassword(error: any): string { return localize('arc.errorVerifyingPassword', "Error encountered while verifying password. {0}", getErrorMessage(error)); }
export const onlyOneControllerSupported = localize('arc.onlyOneControllerSupported', "Only one controller connection is currently supported at this time. Do you wish to remove the existing connection and add a new one?");
export const noControllersConnected = localize('noControllersConnected', "No Azure Arc controllers are currently connected. Please run the command: 'Connect to Existing Azure Arc Controller' and then try again");
export const variableValueFetchForUnsupportedVariable = (variableName: string) => localize('getVariableValue.unknownVariableName', "Attempt to get variable value for unknown variable:{0}", variableName);
export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName);
@@ -211,4 +210,5 @@ export const select = localize('button.label', "Select");
export const noContextFound = (configFile: string) => localize('noContextFound', "No 'contexts' found in the config file: {0}", configFile);
export const noCurrentContextFound = (configFile: string) => localize('noCurrentContextFound', "No context is marked as 'current-context' in the config file: {0}", configFile);
export const noNameInContext = (configFile: string) => localize('noNameInContext', "No name field was found in a cluster context in the config file: {0}", configFile);
export const userCancelledError = localize('userCancelledError', "User cancelled the dialog");
export const userCancelledError = localize('arc.userCancelledError', "User cancelled the dialog");
export const clusterContextConfigNoLongerValid = (configFile: string, clusterContext: string, error: any) => localize('clusterContextConfigNoLongerValid', "The cluster context information specified by config file: {0} and cluster context: {1} is no longer valid. Error is:\n\t{2}\n Do you want to update this information?", configFile, clusterContext, getErrorMessage(error));

View File

@@ -7,6 +7,8 @@ import { ControllerInfo, ResourceType } from 'arc';
import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import { UserCancelledError } from '../common/api';
import { getCurrentClusterContext, getKubeConfigClusterContexts } from '../common/kubeUtils';
import { Deferred } from '../common/promise';
import * as loc from '../localizedConstants';
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
@@ -22,6 +24,7 @@ export class ControllerModel {
private _endpoints: azdataExt.DcEndpointListResult[] = [];
private _registrations: Registration[] = [];
private _controllerConfig: azdataExt.DcConfigShowResult | undefined = undefined;
private static _refreshInProgress: Deferred<void> | undefined = undefined;
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.DcConfigShowResult | undefined>();
private readonly _onEndpointsUpdated = new vscode.EventEmitter<azdataExt.DcEndpointListResult[]>();
@@ -50,19 +53,41 @@ export class ControllerModel {
this._onInfoUpdated.fire(this._info);
}
public get azdataAdditionalEnvVars(): azdataExt.AdditionalEnvVars {
return {
'KUBECONFIG': this.info.kubeConfigFilePath,
'KUBECTL_CONTEXT': this.info.kubeClusterContext
};
}
/**
* Calls azdata login to set the context to this controller
* @param promptReconnect
*/
public async azdataLogin(promptReconnect: boolean = false): Promise<void> {
// We haven't gotten our password yet or we want to prompt for a reconnect
if (!this._password || promptReconnect) {
let promptForValidClusterContext: boolean = false;
try {
const contexts = await getKubeConfigClusterContexts(this.info.kubeConfigFilePath);
getCurrentClusterContext(contexts, this.info.kubeClusterContext, true); // this throws if this.info.kubeClusterContext is not found in 'contexts'
} catch (error) {
const response = await vscode.window.showErrorMessage(loc.clusterContextConfigNoLongerValid(this.info.kubeConfigFilePath, this.info.kubeClusterContext, error), loc.yes, loc.no);
if (response === loc.yes) {
promptForValidClusterContext = true;
} else {
if (!promptReconnect) { //throw unless we are required to prompt for reconnect anyways
throw error;
}
}
}
// We haven't gotten our password yet or we want to prompt for a reconnect or we want to prompt to reacquire valid cluster context or any and all of these.
if (!this._password || promptReconnect || promptForValidClusterContext) {
this._password = '';
if (this.info.rememberPassword) {
// It should be in the credentials store, get it from there
this._password = await this.treeDataProvider.getPassword(this.info);
}
if (promptReconnect || !this._password) {
if (promptReconnect || !this._password || promptForValidClusterContext) {
// No password yet or we want to re-prompt for credentials so prompt for it from the user
const dialog = new ConnectToControllerDialog(this.treeDataProvider);
dialog.showDialog(this.info, this._password);
@@ -70,13 +95,14 @@ export class ControllerModel {
if (model) {
await this.treeDataProvider.addOrUpdateController(model.controllerModel, model.password, false);
this._password = model.password;
this._info = model.controllerModel.info;
} else {
throw new UserCancelledError(loc.userCancelledError);
}
}
}
await this._azdataApi.azdata.login(this.info.url, this.info.username, this._password);
await this._azdataApi.azdata.login(this.info.url, this.info.username, this._password, this.azdataAdditionalEnvVars);
}
/**
@@ -91,6 +117,12 @@ export class ControllerModel {
}
}
public async refresh(showErrors: boolean = true, promptReconnect: boolean = false): Promise<void> {
//wait for any previous refresh that might be in progress to finish
if (ControllerModel._refreshInProgress) {
await ControllerModel._refreshInProgress;
}
// create a new in progress promise object
ControllerModel._refreshInProgress = new Deferred<void>();
await this.azdataLogin(promptReconnect);
const newRegistrations: Registration[] = [];
await Promise.all([
@@ -108,7 +140,7 @@ export class ControllerModel {
this._onConfigUpdated.fire(this._controllerConfig);
throw err;
}),
this._azdataApi.azdata.arc.dc.endpoint.list().then(result => {
this._azdataApi.azdata.arc.dc.endpoint.list(this.azdataAdditionalEnvVars).then(result => {
this._endpoints = result.result;
this.endpointsLastUpdated = new Date();
this._onEndpointsUpdated.fire(this._endpoints);
@@ -123,7 +155,7 @@ export class ControllerModel {
throw err;
}),
Promise.all([
this._azdataApi.azdata.arc.postgres.server.list().then(result => {
this._azdataApi.azdata.arc.postgres.server.list(this.azdataAdditionalEnvVars).then(result => {
newRegistrations.push(...result.result.map(r => {
return {
instanceName: r.name,
@@ -147,6 +179,8 @@ export class ControllerModel {
this._onRegistrationsUpdated.fire(this._registrations);
})
]);
ControllerModel._refreshInProgress.resolve();
ControllerModel._refreshInProgress = undefined;
}
public get endpoints(): azdataExt.DcEndpointListResult[] {

View File

@@ -38,8 +38,8 @@ export class MiaaModel extends ResourceModel {
private _refreshPromise: Deferred<void> | undefined = undefined;
constructor(private _controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
super(_miaaInfo, registration);
constructor(controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) {
super(controllerModel, _miaaInfo, registration);
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
}
@@ -77,7 +77,7 @@ export class MiaaModel extends ResourceModel {
}
this._refreshPromise = new Deferred();
try {
await this._controllerModel.azdataLogin();
await this.controllerModel.azdataLogin();
try {
const result = await this._azdataApi.azdata.arc.sql.mi.show(this.info.name);
this._config = result.result;
@@ -180,7 +180,7 @@ export class MiaaModel extends ResourceModel {
if (this.info.connectionId) {
try {
const credentialProvider = await azdata.credentials.getProvider(credentialNamespace);
const credentials = await credentialProvider.readCredential(createCredentialId(this._controllerModel.info.id, this.info.resourceType, this.info.name));
const credentials = await credentialProvider.readCredential(createCredentialId(this.controllerModel.info.id, this.info.resourceType, this.info.name));
if (credentials.password) {
// Try to connect to verify credentials are still valid
connectionProfile.password = credentials.password;
@@ -189,7 +189,7 @@ export class MiaaModel extends ResourceModel {
const result = await azdata.connection.connect(connectionProfile, false, false);
if (!result.connected) {
vscode.window.showErrorMessage(loc.connectToSqlFailed(connectionProfile.serverName, result.errorMessage));
const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this);
const connectToSqlDialog = new ConnectToSqlDialog(this.controllerModel, this);
connectToSqlDialog.showDialog(connectionProfile);
connectionProfile = await connectToSqlDialog.waitForClose();
}
@@ -203,7 +203,7 @@ export class MiaaModel extends ResourceModel {
if (!connectionProfile?.userName || !connectionProfile?.password) {
// Need to prompt user for password since we don't have one stored
const connectToSqlDialog = new ConnectToSqlDialog(this._controllerModel, this);
const connectToSqlDialog = new ConnectToSqlDialog(this.controllerModel, this);
connectToSqlDialog.showDialog(connectionProfile);
connectionProfile = await connectToSqlDialog.waitForClose();
}

View File

@@ -22,8 +22,8 @@ export class PostgresModel extends ResourceModel {
private _refreshPromise?: Deferred<void>;
constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration) {
super(info, registration);
constructor(controllerModel: ControllerModel, info: ResourceInfo, registration: Registration) {
super(controllerModel, info, registration);
this._azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
}
@@ -91,7 +91,7 @@ export class PostgresModel extends ResourceModel {
this._refreshPromise = new Deferred();
try {
await this._controllerModel.azdataLogin();
await this.controllerModel.azdataLogin();
this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name)).result;
this.configLastUpdated = new Date();
this._onConfigUpdated.fire(this._config);

View File

@@ -5,14 +5,14 @@
import { ResourceInfo } from 'arc';
import * as vscode from 'vscode';
import { Registration } from './controllerModel';
import { ControllerModel, Registration } from './controllerModel';
export abstract class ResourceModel {
private readonly _onRegistrationUpdated = new vscode.EventEmitter<Registration>();
public onRegistrationUpdated = this._onRegistrationUpdated.event;
constructor(public info: ResourceInfo, private _registration: Registration) { }
constructor(public readonly controllerModel: ControllerModel, public info: ResourceInfo, private _registration: Registration) { }
public get registration(): Registration {
return this._registration;

View File

@@ -35,6 +35,8 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
switch (variableName) {
case 'endpoint': return controller.info.url;
case 'username': return controller.info.username;
case 'kubeConfig': return controller.info.kubeConfigFilePath;
case 'clusterContext': return controller.info.kubeClusterContext;
case 'password': return this.getPassword(controller);
default: throw new Error(loc.variableValueFetchForUnsupportedVariable(variableName));
}
@@ -59,6 +61,8 @@ export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourcePro
switch (variableName) {
case 'endpoint': return false;
case 'username': return false;
case 'kubeConfig': return false;
case 'clusterContext': return false;
case 'password': return true;
default: throw new Error(loc.isPasswordFetchForUnsupportedVariable(variableName));
}

View File

@@ -50,7 +50,8 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
workers?: number
},
_engineVersion?: string,
_additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
_additionalEnvVars?: azdataExt.AdditionalEnvVars
): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
}
},
sql: {

View File

@@ -12,6 +12,7 @@ import * as TypeMoq from 'typemoq';
import { v4 as uuid } from 'uuid';
import * as vscode from 'vscode';
import * as loc from '../../localizedConstants';
import * as kubeUtils from '../../common/kubeUtils';
import { UserCancelledError } from '../../common/api';
import { ControllerModel } from '../../models/controllerModel';
import { ConnectToControllerDialog } from '../../ui/dialogs/connectControllerDialog';
@@ -34,6 +35,8 @@ describe('ControllerModel', function (): void {
beforeEach(function (): void {
sinon.stub(ConnectToControllerDialog.prototype, 'showDialog');
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]);
sinon.stub(vscode.window, 'showErrorMessage').resolves(<any>loc.yes);
});
it('Rejected with expected error when user cancels', async function (): Promise<void> {
@@ -61,7 +64,7 @@ describe('ControllerModel', function (): void {
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
await model.azdataLogin();
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Prompt for password when not in cred store', async function (): Promise<void> {
@@ -87,7 +90,7 @@ describe('ControllerModel', function (): void {
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
await model.azdataLogin();
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Prompt for password when rememberPassword is true but prompt reconnect is true', async function (): Promise<void> {
@@ -113,7 +116,7 @@ describe('ControllerModel', function (): void {
await model.azdataLogin(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Prompt for password when we already have a password but prompt reconnect is true', async function (): Promise<void> {
@@ -140,7 +143,7 @@ describe('ControllerModel', function (): void {
await model.azdataLogin(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called');
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password), TypeMoq.Times.once());
azdataMock.verify(x => x.login(TypeMoq.It.isAny(), TypeMoq.It.isAny(), password, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('Model values are updated correctly when modified during reconnect', async function (): Promise<void> {

View File

@@ -11,6 +11,7 @@ import * as sinon from 'sinon';
import { v4 as uuid } from 'uuid';
import * as vscode from 'vscode';
import * as azdataExt from 'azdata-ext';
import * as kubeUtils from '../../../common/kubeUtils';
import { ControllerModel } from '../../../models/controllerModel';
import { MiaaModel } from '../../../models/miaaModel';
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
@@ -102,13 +103,14 @@ describe('AzureArcTreeDataProvider tests', function (): void {
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object);
sinon.stub(kubeUtils, 'getKubeConfigClusterContexts').resolves([{ name: 'currentCluster', isCurrentContext: true }]);
const controllerModel = new ControllerModel(treeDataProvider, { id: uuid(), url: '127.0.0.1', kubeConfigFilePath: '/path/to/.kube/config', kubeClusterContext: 'currentCluster', name: 'my-arc', username: 'sa', rememberPassword: true, resources: [] }, 'mypassword');
await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel);
const children = await treeDataProvider.getChildren(controllerNode);
should(children.filter(c => c.label === fakeAzdataApi.postgresInstances[0].name).length).equal(1, 'Should have a Postgres child');
should(children.filter(c => c.label === fakeAzdataApi.miaaInstances[0].name).length).equal(1, 'Should have a MIAA child');
should(children.length).equal(2, 'Should have excatly 2 children');
should(children.length).equal(2, 'Should have exactly 2 children');
});
});

View File

@@ -44,7 +44,7 @@ export class FilePicker implements IReadOnly {
});
if (!fileUris || fileUris.length === 0) {
return; // This can happen when a user cancels out. we don't throw and the user just won't be able to move on until they select something.
return; // This can happen when a user cancels out. We don't throw and the user just won't be able to move on until they select something.
}
const fileUri = fileUris[0]; //we allow the user to select only one file in the dialog
this.filePathInputBox.value = fileUri.fsPath;

View File

@@ -130,6 +130,7 @@ export class MiaaComputeAndStoragePage extends DashboardPage {
},
async (_progress, _token): Promise<void> => {
try {
await this._miaaModel.controllerModel.azdataLogin();
await this._azdataApi.azdata.arc.sql.mi.edit(
this._miaaModel.info.name, this.saveArgs);
} catch (err) {

View File

@@ -205,8 +205,9 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
title: loc.deletingInstance(this._miaaModel.info.name),
cancellable: false
},
(_progress, _token) => {
return this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name);
async (_progress, _token) => {
await this._controllerModel.azdataLogin();
return await this._azdataApi.azdata.arc.sql.mi.delete(this._miaaModel.info.name);
}
);
await this._controllerModel.refreshTreeNode();

View File

@@ -156,10 +156,12 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
},
async (_progress, _token): Promise<void> => {
try {
await this._postgresModel.controllerModel.azdataLogin();
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
this.saveArgs,
this._postgresModel.engineVersion);
this._postgresModel.engineVersion
);
} catch (err) {
// If an error occurs while editing the instance then re-enable the save button since
// the edit wasn't successfully applied

View File

@@ -151,6 +151,7 @@ export class PostgresOverviewPage extends DashboardPage {
try {
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
if (password) {
await this._postgresModel.controllerModel.azdataLogin();
await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name,
{
@@ -158,7 +159,8 @@ export class PostgresOverviewPage extends DashboardPage {
noWait: true
},
this._postgresModel.engineVersion,
{ 'AZDATA_PASSWORD': password });
{ 'AZDATA_PASSWORD': password }
);
vscode.window.showInformationMessage(loc.passwordReset);
}
} catch (error) {
@@ -185,8 +187,9 @@ export class PostgresOverviewPage extends DashboardPage {
title: loc.deletingInstance(this._postgresModel.info.name),
cancellable: false
},
(_progress, _token) => {
return this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name);
async (_progress, _token) => {
await this._postgresModel.controllerModel.azdataLogin();
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name);
}
);
await this._controllerModel.refreshTreeNode();

View File

@@ -271,11 +271,19 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
}
const azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
try {
await azdataApi.azdata.login(this.urlInputBox.value!, this.usernameInputBox.value!, this.passwordInputBox.value);
await azdataApi.azdata.login(
this.urlInputBox.value!,
this.usernameInputBox.value!,
this.passwordInputBox.value,
{
'KUBECONFIG': this.kubeConfigInputBox.value!,
'KUBECTL_CONTEXT': this.clusterContextRadioGroup.value!
}
);
} catch (e) {
if (getErrorMessage(e).match(/Wrong username or password/i)) {
this.dialog.message = {
text: loc.invalidPassword,
text: loc.loginFailed,
level: azdata.window.MessageLevel.Error
};
return false;
@@ -299,3 +307,5 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
return dialog;
}
}