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": { "metadata": {
"kernelspec": { "kernelspec": {
"name": "python3", "name": "python3",
"display_name": "Python 3" "display_name": "Python 3",
"language": "python"
}, },
"language_info": { "language_info": {
"name": "python", "name": "python",

View File

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

View File

@@ -663,6 +663,8 @@
"variableNames": { "variableNames": {
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT", "endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME", "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" "password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
} }
}, },
@@ -855,6 +857,8 @@
"variableNames": { "variableNames": {
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT", "endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME", "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" "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 () => { 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); const dialog = new ConnectToControllerDialog(treeDataProvider);
dialog.showDialog(); dialog.showDialog();
const model = await dialog.waitForClose(); 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 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 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 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 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 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 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); 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 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 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 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 azdataExt from 'azdata-ext';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { UserCancelledError } from '../common/api'; import { UserCancelledError } from '../common/api';
import { getCurrentClusterContext, getKubeConfigClusterContexts } from '../common/kubeUtils';
import { Deferred } from '../common/promise';
import * as loc from '../localizedConstants'; import * as loc from '../localizedConstants';
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog'; import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
@@ -22,6 +24,7 @@ export class ControllerModel {
private _endpoints: azdataExt.DcEndpointListResult[] = []; private _endpoints: azdataExt.DcEndpointListResult[] = [];
private _registrations: Registration[] = []; private _registrations: Registration[] = [];
private _controllerConfig: azdataExt.DcConfigShowResult | undefined = undefined; private _controllerConfig: azdataExt.DcConfigShowResult | undefined = undefined;
private static _refreshInProgress: Deferred<void> | undefined = undefined;
private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.DcConfigShowResult | undefined>(); private readonly _onConfigUpdated = new vscode.EventEmitter<azdataExt.DcConfigShowResult | undefined>();
private readonly _onEndpointsUpdated = new vscode.EventEmitter<azdataExt.DcEndpointListResult[]>(); private readonly _onEndpointsUpdated = new vscode.EventEmitter<azdataExt.DcEndpointListResult[]>();
@@ -50,19 +53,41 @@ export class ControllerModel {
this._onInfoUpdated.fire(this._info); 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 * Calls azdata login to set the context to this controller
* @param promptReconnect * @param promptReconnect
*/ */
public async azdataLogin(promptReconnect: boolean = false): Promise<void> { public async azdataLogin(promptReconnect: boolean = false): Promise<void> {
// We haven't gotten our password yet or we want to prompt for a reconnect let promptForValidClusterContext: boolean = false;
if (!this._password || promptReconnect) { 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 = ''; this._password = '';
if (this.info.rememberPassword) { if (this.info.rememberPassword) {
// It should be in the credentials store, get it from there // It should be in the credentials store, get it from there
this._password = await this.treeDataProvider.getPassword(this.info); 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 // No password yet or we want to re-prompt for credentials so prompt for it from the user
const dialog = new ConnectToControllerDialog(this.treeDataProvider); const dialog = new ConnectToControllerDialog(this.treeDataProvider);
dialog.showDialog(this.info, this._password); dialog.showDialog(this.info, this._password);
@@ -70,13 +95,14 @@ export class ControllerModel {
if (model) { if (model) {
await this.treeDataProvider.addOrUpdateController(model.controllerModel, model.password, false); await this.treeDataProvider.addOrUpdateController(model.controllerModel, model.password, false);
this._password = model.password; this._password = model.password;
this._info = model.controllerModel.info;
} else { } else {
throw new UserCancelledError(loc.userCancelledError); 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> { 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); await this.azdataLogin(promptReconnect);
const newRegistrations: Registration[] = []; const newRegistrations: Registration[] = [];
await Promise.all([ await Promise.all([
@@ -108,7 +140,7 @@ export class ControllerModel {
this._onConfigUpdated.fire(this._controllerConfig); this._onConfigUpdated.fire(this._controllerConfig);
throw err; 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._endpoints = result.result;
this.endpointsLastUpdated = new Date(); this.endpointsLastUpdated = new Date();
this._onEndpointsUpdated.fire(this._endpoints); this._onEndpointsUpdated.fire(this._endpoints);
@@ -123,7 +155,7 @@ export class ControllerModel {
throw err; throw err;
}), }),
Promise.all([ 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 => { newRegistrations.push(...result.result.map(r => {
return { return {
instanceName: r.name, instanceName: r.name,
@@ -147,6 +179,8 @@ export class ControllerModel {
this._onRegistrationsUpdated.fire(this._registrations); this._onRegistrationsUpdated.fire(this._registrations);
}) })
]); ]);
ControllerModel._refreshInProgress.resolve();
ControllerModel._refreshInProgress = undefined;
} }
public get endpoints(): azdataExt.DcEndpointListResult[] { public get endpoints(): azdataExt.DcEndpointListResult[] {

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,8 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
workers?: number workers?: number
}, },
_engineVersion?: string, _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: { sql: {

View File

@@ -12,6 +12,7 @@ import * as TypeMoq from 'typemoq';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as loc from '../../localizedConstants'; import * as loc from '../../localizedConstants';
import * as kubeUtils from '../../common/kubeUtils';
import { UserCancelledError } from '../../common/api'; import { UserCancelledError } from '../../common/api';
import { ControllerModel } from '../../models/controllerModel'; import { ControllerModel } from '../../models/controllerModel';
import { ConnectToControllerDialog } from '../../ui/dialogs/connectControllerDialog'; import { ConnectToControllerDialog } from '../../ui/dialogs/connectControllerDialog';
@@ -34,6 +35,8 @@ describe('ControllerModel', function (): void {
beforeEach(function (): void { beforeEach(function (): void {
sinon.stub(ConnectToControllerDialog.prototype, 'showDialog'); 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> { 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: [] }); 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(); 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> { 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: [] }); 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(); 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> { 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); await model.azdataLogin(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called'); 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> { 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); await model.azdataLogin(true);
should(waitForCloseStub.called).be.true('waitForClose should have been called'); 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> { 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 { v4 as uuid } from 'uuid';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as azdataExt from 'azdata-ext'; import * as azdataExt from 'azdata-ext';
import * as kubeUtils from '../../../common/kubeUtils';
import { ControllerModel } from '../../../models/controllerModel'; import { ControllerModel } from '../../../models/controllerModel';
import { MiaaModel } from '../../../models/miaaModel'; import { MiaaModel } from '../../../models/miaaModel';
import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider'; import { AzureArcTreeDataProvider } from '../../../ui/tree/azureArcTreeDataProvider';
@@ -102,13 +103,14 @@ describe('AzureArcTreeDataProvider tests', function (): void {
mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi); mockArcApi.setup(x => x.azdata).returns(() => fakeAzdataApi);
sinon.stub(vscode.extensions, 'getExtension').returns(mockArcExtension.object); 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'); 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, ''); await treeDataProvider.addOrUpdateController(controllerModel, '');
const controllerNode = treeDataProvider.getControllerNode(controllerModel); const controllerNode = treeDataProvider.getControllerNode(controllerModel);
const children = await treeDataProvider.getChildren(controllerNode); 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.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.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) { 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 const fileUri = fileUris[0]; //we allow the user to select only one file in the dialog
this.filePathInputBox.value = fileUri.fsPath; this.filePathInputBox.value = fileUri.fsPath;

View File

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

View File

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

View File

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

View File

@@ -151,6 +151,7 @@ export class PostgresOverviewPage extends DashboardPage {
try { try {
const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : ''); const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : '');
if (password) { if (password) {
await this._postgresModel.controllerModel.azdataLogin();
await this._azdataApi.azdata.arc.postgres.server.edit( await this._azdataApi.azdata.arc.postgres.server.edit(
this._postgresModel.info.name, this._postgresModel.info.name,
{ {
@@ -158,7 +159,8 @@ export class PostgresOverviewPage extends DashboardPage {
noWait: true noWait: true
}, },
this._postgresModel.engineVersion, this._postgresModel.engineVersion,
{ 'AZDATA_PASSWORD': password }); { 'AZDATA_PASSWORD': password }
);
vscode.window.showInformationMessage(loc.passwordReset); vscode.window.showInformationMessage(loc.passwordReset);
} }
} catch (error) { } catch (error) {
@@ -185,8 +187,9 @@ export class PostgresOverviewPage extends DashboardPage {
title: loc.deletingInstance(this._postgresModel.info.name), title: loc.deletingInstance(this._postgresModel.info.name),
cancellable: false cancellable: false
}, },
(_progress, _token) => { async (_progress, _token) => {
return this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name); await this._postgresModel.controllerModel.azdataLogin();
return await this._azdataApi.azdata.arc.postgres.server.delete(this._postgresModel.info.name);
} }
); );
await this._controllerModel.refreshTreeNode(); 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; const azdataApi = <azdataExt.IExtension>vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
try { 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) { } catch (e) {
if (getErrorMessage(e).match(/Wrong username or password/i)) { if (getErrorMessage(e).match(/Wrong username or password/i)) {
this.dialog.message = { this.dialog.message = {
text: loc.invalidPassword, text: loc.loginFailed,
level: azdata.window.MessageLevel.Error level: azdata.window.MessageLevel.Error
}; };
return false; return false;
@@ -299,3 +307,5 @@ export class PasswordToControllerDialog extends ControllerDialogBase {
return dialog; return dialog;
} }
} }

View File

@@ -45,47 +45,47 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
return { return {
arc: { arc: {
dc: { dc: {
create: async (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string) => { create: async (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass); return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars);
}, },
endpoint: { endpoint: {
list: async () => { list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.endpoint.list(); return azdataToolService.localAzdata.arc.dc.endpoint.list(additionalEnvVars);
} }
}, },
config: { config: {
list: async () => { list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.config.list(); return azdataToolService.localAzdata.arc.dc.config.list(additionalEnvVars);
}, },
show: async () => { show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.dc.config.show(); return azdataToolService.localAzdata.arc.dc.config.show(additionalEnvVars);
} }
} }
}, },
postgres: { postgres: {
server: { server: {
delete: async (name: string) => { delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.delete(name); return azdataToolService.localAzdata.arc.postgres.server.delete(name, additionalEnvVars);
}, },
list: async () => { list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.list(); return azdataToolService.localAzdata.arc.postgres.server.list(additionalEnvVars);
}, },
show: async (name: string) => { show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.show(name); return azdataToolService.localAzdata.arc.postgres.server.show(name, additionalEnvVars);
}, },
edit: async ( edit: async (
name: string, name: string,
@@ -112,20 +112,20 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
}, },
sql: { sql: {
mi: { mi: {
delete: async (name: string) => { delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.delete(name); return azdataToolService.localAzdata.arc.sql.mi.delete(name, additionalEnvVars);
}, },
list: async () => { list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.list(); return azdataToolService.localAzdata.arc.sql.mi.list(additionalEnvVars);
}, },
show: async (name: string) => { show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.show(name); return azdataToolService.localAzdata.arc.sql.mi.show(name, additionalEnvVars);
}, },
edit: async ( edit: async (
name: string, name: string,
@@ -135,10 +135,12 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
memoryLimit?: string; memoryLimit?: string;
memoryRequest?: string; memoryRequest?: string;
noWait?: boolean; noWait?: boolean;
}) => { },
additionalEnvVars?: azdataExt.AdditionalEnvVars
) => {
await localAzdataDiscovered; await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.sql.mi.edit(name, args); return azdataToolService.localAzdata.arc.sql.mi.edit(name, args, additionalEnvVars);
} }
} }
} }
@@ -148,9 +150,9 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
throwIfNoAzdata(azdataToolService.localAzdata); throwIfNoAzdata(azdataToolService.localAzdata);
return azdataToolService.localAzdata.getPath(); return azdataToolService.localAzdata.getPath();
}, },
login: async (endpoint: string, username: string, password: string) => { login: async (endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars) => {
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.login(endpoint, username, password); return azdataToolService.localAzdata.login(endpoint, username, password, additionalEnvVars);
}, },
getSemVersion: async () => { getSemVersion: async () => {
await localAzdataDiscovered; await localAzdataDiscovered;

View File

@@ -31,7 +31,7 @@ export interface IAzdataTool extends azdataExt.IAzdataApi {
* @param args The args to pass to azdata * @param args The args to pass to azdata
* @param parseResult A function used to parse out the raw result into the desired shape * @param parseResult A function used to parse out the raw result into the desired shape
*/ */
executeCommand<R>(args: string[], additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<R>> executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>>
} }
/** /**
@@ -62,7 +62,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
public arc = { public arc = {
dc: { dc: {
create: (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string): Promise<azdataExt.AzdataOutput<void>> => { create: (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<void>> => {
const args = ['arc', 'dc', 'create', const args = ['arc', 'dc', 'create',
'--namespace', namespace, '--namespace', namespace,
'--name', name, '--name', name,
@@ -76,32 +76,32 @@ export class AzdataTool implements azdataExt.IAzdataApi {
if (storageClass) { if (storageClass) {
args.push('--storage-class', storageClass); args.push('--storage-class', storageClass);
} }
return this.executeCommand<void>(args); return this.executeCommand<void>(args, additionalEnvVars);
}, },
endpoint: { endpoint: {
list: (): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => { list: (additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list']); return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars);
} }
}, },
config: { config: {
list: (): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => { list: (additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list']); return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars);
}, },
show: (): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => { show: (additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show']); return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars);
} }
} }
}, },
postgres: { postgres: {
server: { server: {
delete: (name: string): Promise<azdataExt.AzdataOutput<void>> => { delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force']); return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force'], additionalEnvVars);
}, },
list: (): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => { list: (additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list']); return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars);
}, },
show: (name: string): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => { show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => {
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name]); return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name], additionalEnvVars);
}, },
edit: ( edit: (
name: string, name: string,
@@ -119,7 +119,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
workers?: number workers?: number
}, },
engineVersion?: string, engineVersion?: string,
additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> => { additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name]; const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
if (args.adminPassword) { argsArray.push('--admin-password'); } if (args.adminPassword) { argsArray.push('--admin-password'); }
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); } if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
@@ -139,14 +139,14 @@ export class AzdataTool implements azdataExt.IAzdataApi {
}, },
sql: { sql: {
mi: { mi: {
delete: (name: string): Promise<azdataExt.AzdataOutput<void>> => { delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name]); return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars);
}, },
list: (): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => { list: (additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list']); return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], additionalEnvVars);
}, },
show: (name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => { show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name]); return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name], additionalEnvVars);
}, },
edit: ( edit: (
name: string, name: string,
@@ -156,21 +156,23 @@ export class AzdataTool implements azdataExt.IAzdataApi {
memoryLimit?: string, memoryLimit?: string,
memoryRequest?: string, memoryRequest?: string,
noWait?: boolean, noWait?: boolean,
}): Promise<azdataExt.AzdataOutput<void>> => { },
additionalEnvVars?: azdataExt.AdditionalEnvVars
): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name]; const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name];
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); } if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); } if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); } if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); } if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); } if (args.noWait) { argsArray.push('--no-wait'); }
return this.executeCommand<void>(argsArray); return this.executeCommand<void>(argsArray, additionalEnvVars);
} }
} }
} }
}; };
public login(endpoint: string, username: string, password: string): Promise<azdataExt.AzdataOutput<void>> { public login(endpoint: string, username: string, password: string, additionalEnvVars: azdataExt.AdditionalEnvVars = {}): Promise<azdataExt.AzdataOutput<void>> {
return this.executeCommand<void>(['login', '-e', endpoint, '-u', username], { 'AZDATA_PASSWORD': password }); return this.executeCommand<void>(['login', '-e', endpoint, '-u', username], Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }));
} }
/** /**
@@ -188,7 +190,7 @@ export class AzdataTool implements azdataExt.IAzdataApi {
}; };
} }
public async executeCommand<R>(args: string[], additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<R>> { public async executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>> {
try { try {
const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout); const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout);
return { return {
@@ -609,7 +611,7 @@ async function discoverLatestStableAzdataVersionDarwin(): Promise<SemVer> {
return new SemVer(azdataPackageVersionInfo.versions.stable); return new SemVer(azdataPackageVersionInfo.versions.stable);
} }
async function executeAzdataCommand(command: string, args: string[], additionalEnvVars: { [key: string]: string } = {}): Promise<ProcessOutput> { async function executeAzdataCommand(command: string, args: string[], additionalEnvVars: azdataExt.AdditionalEnvVars = {}): Promise<ProcessOutput> {
additionalEnvVars = Object.assign(additionalEnvVars, { 'ACCEPT_EULA': 'yes' }); additionalEnvVars = Object.assign(additionalEnvVars, { 'ACCEPT_EULA': 'yes' });
const debug = vscode.workspace.getConfiguration(azdataConfigSection).get(debugConfigKey); const debug = vscode.workspace.getConfiguration(azdataConfigSection).get(debugConfigKey);
if (debug) { if (debug) {

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { AdditionalEnvVars } from 'azdata-ext';
import * as cp from 'child_process'; import * as cp from 'child_process';
import * as sudo from 'sudo-prompt'; import * as sudo from 'sudo-prompt';
import * as loc from '../localizedConstants'; import * as loc from '../localizedConstants';
@@ -47,7 +48,7 @@ export type ProcessOutput = { stdout: string, stderr: string };
* @param args Optional args to pass, every arg and arg value must be a separate item in the array * @param args Optional args to pass, every arg and arg value must be a separate item in the array
* @param additionalEnvVars Additional environment variables to add to the process environment * @param additionalEnvVars Additional environment variables to add to the process environment
*/ */
export async function executeCommand(command: string, args: string[], additionalEnvVars?: { [key: string]: string },): Promise<ProcessOutput> { export async function executeCommand(command: string, args: string[], additionalEnvVars?: AdditionalEnvVars): Promise<ProcessOutput> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Logger.log(loc.executingCommand(command, args)); Logger.log(loc.executingCommand(command, args));
const stdoutBuffers: Buffer[] = []; const stdoutBuffers: Buffer[] = [];

View File

@@ -18,9 +18,7 @@ export class AzdataToolService {
} }
/** /**
* Sets the localAzdata that was last saved * Sets the localAzdata object to be used for azdata operations
*
* @param memento The memento that stores the localAzdata object
*/ */
set localAzdata(azdata: IAzdataTool | undefined) { set localAzdata(azdata: IAzdataTool | undefined) {
this._localAzdata = azdata; this._localAzdata = azdata;

View File

@@ -16,6 +16,8 @@ declare module 'azdata-ext' {
name = 'Microsoft.azdata' name = 'Microsoft.azdata'
} }
export type AdditionalEnvVars = { [key: string]: string};
export interface ErrorWithLink extends Error { export interface ErrorWithLink extends Error {
messageWithLink: string; messageWithLink: string;
} }
@@ -233,20 +235,20 @@ declare module 'azdata-ext' {
export interface IAzdataApi { export interface IAzdataApi {
arc: { arc: {
dc: { dc: {
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string): Promise<AzdataOutput<void>>, create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<void>>,
endpoint: { endpoint: {
list(): Promise<AzdataOutput<DcEndpointListResult[]>> list(additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<DcEndpointListResult[]>>
}, },
config: { config: {
list(): Promise<AzdataOutput<DcConfigListResult[]>>, list(additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<DcConfigListResult[]>>,
show(): Promise<AzdataOutput<DcConfigShowResult>> show(additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<DcConfigShowResult>>
} }
}, },
postgres: { postgres: {
server: { server: {
delete(name: string): Promise<AzdataOutput<void>>, delete(name: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<void>>,
list(): Promise<AzdataOutput<PostgresServerListResult[]>>, list(additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<PostgresServerListResult[]>>,
show(name: string): Promise<AzdataOutput<PostgresServerShowResult>>, show(name: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<PostgresServerShowResult>>,
edit( edit(
name: string, name: string,
args: { args: {
@@ -263,14 +265,15 @@ declare module 'azdata-ext' {
workers?: number workers?: number
}, },
engineVersion?: string, engineVersion?: string,
additionalEnvVars?: { [key: string]: string }): Promise<AzdataOutput<void>> additionalEnvVars?: AdditionalEnvVars
): Promise<AzdataOutput<void>>
} }
}, },
sql: { sql: {
mi: { mi: {
delete(name: string): Promise<AzdataOutput<void>>, delete(name: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<void>>,
list(): Promise<AzdataOutput<SqlMiListResult[]>>, list(additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<SqlMiListResult[]>>,
show(name: string): Promise<AzdataOutput<SqlMiShowResult>>, show(name: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<SqlMiShowResult>>,
edit( edit(
name: string, name: string,
args: { args: {
@@ -279,13 +282,14 @@ declare module 'azdata-ext' {
memoryLimit?: string, memoryLimit?: string,
memoryRequest?: string, memoryRequest?: string,
noWait?: boolean, noWait?: boolean,
} },
additionalEnvVars?: AdditionalEnvVars
): Promise<AzdataOutput<void>> ): Promise<AzdataOutput<void>>
} }
} }
}, },
getPath(): Promise<string>, getPath(): Promise<string>,
login(endpoint: string, username: string, password: string): Promise<AzdataOutput<any>>, login(endpoint: string, username: string, password: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzdataOutput<any>>,
/** /**
* The semVersion corresponding to this installation of azdata. version() method should have been run * The semVersion corresponding to this installation of azdata. version() method should have been run
* before fetching this value to ensure that correct value is returned. This is almost always correct unless * before fetching this value to ensure that correct value is returned. This is almost always correct unless