controller dropdown field to SQL MIAA and Postgres deployment. (#12217)

* saving first draft

* throw if no controllers

* cleanup

* bug fixes

* bug fixes and caching controller access

* pr comments and bug fixes.

* fixes

* fixes

* comment fix

* remove debug prints

* comment fixes

* remove debug logs

* inputValueTransformer returns string|Promise

* PR feedback

* pr fixes

* remove _ from protected fields

* anonymous to full methods

* small fixes
This commit is contained in:
Arvind Ranasaria
2020-09-15 14:47:49 -07:00
committed by GitHub
parent 92ed830564
commit 9cf80113fc
36 changed files with 754 additions and 190 deletions

View File

@@ -85,16 +85,36 @@
"cell_type": "code",
"source": [
"# Required Values\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\" in os.environ\n",
"if env_var:\n",
" controller_endpoint = os.environ[\"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_ENDPOINT was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_USERNAME\" in os.environ\n",
"if env_var:\n",
" controller_username = os.environ[\"AZDATA_NB_VAR_CONTROLLER_USERNAME\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_USERNAME was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_PASSWORD\" in os.environ\n",
"if env_var:\n",
" controller_password = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_PASSWORD was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME\" in os.environ\n",
"if env_var:\n",
" server_group_name = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\" in os.environ\n",
"if env_var:\n",
" postgres_password = os.environ[\"AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PASSWORD was not defined. Exiting\\n') \n",
"\n",
"env_var = \"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA\" in os.environ\n",
"if env_var:\n",
" postgres_storage_class_data = os.environ[\"AZDATA_NB_VAR_POSTGRES_STORAGE_CLASS_DATA\"]\n",
@@ -159,6 +179,21 @@
"azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189"
}
},
{
"cell_type": "code",
"source": [
"# Login to the data controller.\n",
"#\n",
"os.environ[\"AZDATA_PASSWORD\"] = controller_password\n",
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
"out=run_command()"
],
"metadata": {
"azdata_cell_guid": "71366399-5963-4e24-b2f2-6bb5bffba4ec"
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": [
@@ -187,4 +222,4 @@
"execution_count": null
}
]
}
}

View File

@@ -85,21 +85,42 @@
"cell_type": "code",
"source": [
"# Required Values\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\" in os.environ\n",
"if env_var:\n",
" controller_endpoint = os.environ[\"AZDATA_NB_VAR_CONTROLLER_ENDPOINT\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_ENDPOINT was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_USERNAME\" in os.environ\n",
"if env_var:\n",
" controller_username = os.environ[\"AZDATA_NB_VAR_CONTROLLER_USERNAME\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_USERNAME was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_CONTROLLER_PASSWORD\" in os.environ\n",
"if env_var:\n",
" controller_password = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_CONTROLLER_PASSWORD was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_SQL_INSTANCE_NAME\" in os.environ\n",
"if env_var:\n",
" mssql_instance_name = os.environ[\"AZDATA_NB_VAR_SQL_INSTANCE_NAME\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_INSTANCE_NAME was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_SQL_PASSWORD\" in os.environ\n",
"if env_var:\n",
" mssql_password = os.environ[\"AZDATA_NB_VAR_SQL_PASSWORD\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_PASSWORD was not defined. Exiting\\n') \n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_PASSWORD was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA\" in os.environ\n",
"if env_var:\n",
" mssql_storage_class_data = os.environ[\"AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA\"]\n",
"else:\n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA was not defined. Exiting\\n') \n",
" sys.exit(f'environment variable: AZDATA_NB_VAR_SQL_STORAGE_CLASS_DATA was not defined. Exiting\\n')\n",
"\n",
"env_var = \"AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS\" in os.environ\n",
"if env_var:\n",
" mssql_storage_class_logs = os.environ[\"AZDATA_NB_VAR_SQL_STORAGE_CLASS_LOGS\"]\n",
@@ -123,6 +144,21 @@
"azdata_cell_guid": "90b0e162-2987-463f-9ce6-12dda1267189"
}
},
{
"cell_type": "code",
"source": [
"# Login to the data controller.\n",
"#\n",
"os.environ[\"AZDATA_PASSWORD\"] = controller_password\n",
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
"out=run_command()"
],
"metadata": {
"azdata_cell_guid": "1437c536-17e8-4a7f-80c1-aa43ad02686c"
},
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"source": [
@@ -139,4 +175,4 @@
"execution_count": null
}
]
}
}

View File

@@ -599,6 +599,25 @@
{
"title": "%arc.sql.settings.section.title%",
"fields": [
{
"label": "%arc.controller%",
"variableName": "AZDATA_NB_VAR_ARC_CONTROLLER",
"type": "options",
"editable": false,
"required": true,
"options": {
"source": {
"type": "ArcControllersOptionsSource",
"variableNames": {
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
}
},
"optionsType": "dropdown"
},
"labelWidth": "100%"
},
{
"label": "%arc.sql.instance.name%",
"variableName": "AZDATA_NB_VAR_SQL_INSTANCE_NAME",
@@ -711,6 +730,25 @@
{
"title": "%arc.postgres.settings.section.title%",
"fields": [
{
"label": "%arc.controller%",
"variableName": "AZDATA_NB_VAR_ARC_CONTROLLER",
"type": "options",
"editable": false,
"required": true,
"options": {
"source": {
"type": "ArcControllersOptionsSource",
"variableNames": {
"endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT",
"username": "AZDATA_NB_VAR_CONTROLLER_USERNAME",
"password": "AZDATA_NB_VAR_CONTROLLER_PASSWORD"
}
},
"optionsType": "dropdown"
},
"labelWidth": "100%"
},
{
"label": "%arc.postgres.server.group.name%",
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME",

View File

@@ -76,6 +76,9 @@
"resource.type.picker.display.name": "Resource Type",
"sql.managed.instance.display.name": "Azure SQL managed instance - Azure Arc",
"postgres.server.group.display.name": "PostgreSQL server groups - Azure Arc",
"arc.controller": "Target Azure Arc Controller",
"arc.sql.new.dialog.title": "Deploy Azure SQL managed instance - Azure Arc (preview)",
"arc.sql.settings.section.title": "SQL Connection information",
"arc.azure.section.title": "Azure information",

View File

@@ -5,9 +5,10 @@
import * as arc from 'arc';
import * as vscode from 'vscode';
import { UserCancelledError } from './common/utils';
import { IconPathHelper, refreshActionId } from './constants';
import * as loc from './localizedConstants';
import { ConnectToControllerDialog } from './ui/dialogs/connectControllerDialog';
import { ConnectToControllerDialog, PasswordToControllerDialog } from './ui/dialogs/connectControllerDialog';
import { AzureArcTreeDataProvider } from './ui/tree/azureArcTreeDataProvider';
import { ControllerTreeNode } from './ui/tree/controllerTreeNode';
import { TreeNode } from './ui/tree/treeNode';
@@ -57,11 +58,24 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
await checkArcDeploymentExtension();
return {
getRegisteredDataControllers: async () => {
return (await treeDataProvider.getChildren())
.filter(node => node instanceof ControllerTreeNode)
.map(node => (node as ControllerTreeNode).model.info);
getRegisteredDataControllers: async () => (await treeDataProvider.getChildren())
.filter(node => node instanceof ControllerTreeNode)
.map(node => ({
label: (node as ControllerTreeNode).model.label,
info: (node as ControllerTreeNode).model.info
})),
getControllerPassword: async (controllerInfo: arc.ControllerInfo) => {
return await treeDataProvider.getPassword(controllerInfo);
},
reacquireControllerPassword: async (controllerInfo: arc.ControllerInfo) => {
let model;
const dialog = new PasswordToControllerDialog(treeDataProvider);
dialog.showDialog(controllerInfo);
model = await dialog.waitForClose();
if (!model) {
throw new UserCancelledError();
}
return model.password;
}
};
}

View File

@@ -73,6 +73,7 @@ export const indirect = localize('arc.indirect', "Indirect");
export const loading = localize('arc.loading', "Loading...");
export const refreshToEnterCredentials = localize('arc.refreshToEnterCredentials', "Refresh node to enter credentials");
export const connectToController = localize('arc.connectToController', "Connect to Existing Controller");
export const passwordToController = localize('arc.passwordToController', "Provide Password to Controller");
export const controllerUrl = localize('arc.controllerUrl', "Controller URL");
export const controllerName = localize('arc.controllerName', "Name");
export const defaultControllerName = localize('arc.defaultControllerName', "arc-dc");
@@ -81,6 +82,7 @@ export const password = localize('arc.password', "Password");
export const rememberPassword = localize('arc.rememberPassword', "Remember Password");
export const connect = localize('arc.connect', "Connect");
export const cancel = localize('arc.cancel', "Cancel");
export const ok = localize('arc.ok', "Ok");
export const notConfigured = localize('arc.notConfigured', "Not Configured");
// Database States - see https://docs.microsoft.com/sql/relational-databases/databases/database-states
@@ -156,3 +158,6 @@ export function invalidResourceDeletionName(name: string): string { return local
export function couldNotFindAzureResource(name: string): string { return localize('arc.couldNotFindAzureResource', "Could not find Azure resource for {0}", name); }
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)); }
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 function errorVerifyingPassword(error: any): string { return localize('arc.errorVerifyingPassword', "Error encountered while verifying password. {0}", getErrorMessage(error)); }

View File

@@ -35,7 +35,13 @@ declare module 'arc' {
resources: ResourceInfo[]
};
export interface DataController {
label: string,
info: ControllerInfo
}
export interface IExtension {
getRegisteredDataControllers(): Promise<ControllerInfo[]>;
getRegisteredDataControllers(): Promise<DataController[]>;
getControllerPassword(controllerInfo: ControllerInfo): Promise<string>;
reacquireControllerPassword(controllerInfo: ControllerInfo, password: string, retryCount?: number): Promise<string>;
}
}

View File

@@ -5,6 +5,7 @@
import { ControllerInfo } from 'arc';
import * as azdata from 'azdata';
import * as azdataExt from 'azdata-ext';
import { v4 as uuid } from 'uuid';
import * as vscode from 'vscode';
import { Deferred } from '../../common/promise';
@@ -12,94 +13,136 @@ import * as loc from '../../localizedConstants';
import { ControllerModel } from '../../models/controllerModel';
import { InitializingComponent } from '../components/initializingComponent';
import { AzureArcTreeDataProvider } from '../tree/azureArcTreeDataProvider';
import { getErrorMessage } from '../../common/utils';
export type ConnectToControllerDialogModel = { controllerModel: ControllerModel, password: string };
export class ConnectToControllerDialog extends InitializingComponent {
private modelBuilder!: azdata.ModelBuilder;
abstract class ControllerDialogBase extends InitializingComponent {
protected modelBuilder!: azdata.ModelBuilder;
protected dialog: azdata.window.Dialog;
private urlInputBox!: azdata.InputBoxComponent;
private nameInputBox!: azdata.InputBoxComponent;
private usernameInputBox!: azdata.InputBoxComponent;
private passwordInputBox!: azdata.InputBoxComponent;
private rememberPwCheckBox!: azdata.CheckBoxComponent;
protected urlInputBox!: azdata.InputBoxComponent;
protected nameInputBox!: azdata.InputBoxComponent;
protected usernameInputBox!: azdata.InputBoxComponent;
protected passwordInputBox!: azdata.InputBoxComponent;
private _completionPromise = new Deferred<ConnectToControllerDialogModel | undefined>();
private _id!: string;
constructor(private _treeDataProvider: AzureArcTreeDataProvider) {
super();
protected getComponents(): (azdata.FormComponent<azdata.Component> & { layout?: azdata.FormItemLayout | undefined; })[] {
return [
{
component: this.urlInputBox,
title: loc.controllerUrl,
required: true
}, {
component: this.nameInputBox,
title: loc.controllerName,
required: false
}, {
component: this.usernameInputBox,
title: loc.username,
required: true
}, {
component: this.passwordInputBox,
title: loc.password,
required: true
}
];
}
public showDialog(controllerInfo?: ControllerInfo, password?: string): azdata.window.Dialog {
this._id = controllerInfo?.id ?? uuid();
const dialog = azdata.window.createModelViewDialog(loc.connectToController);
dialog.cancelButton.onClick(() => this.handleCancel());
dialog.registerContent(async view => {
this.modelBuilder = view.modelBuilder;
protected abstract fieldToFocusOn(): azdata.Component;
protected readonlyFields(): azdata.InputBoxComponent[] { return []; }
this.urlInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.url,
// If we have a model then we're editing an existing connection so don't let them modify the URL
readOnly: !!controllerInfo
}).component();
this.nameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.name
}).component();
this.usernameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.username
}).component();
this.passwordInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
inputType: 'password',
value: password
})
.component();
this.rememberPwCheckBox = this.modelBuilder.checkBox()
.withProperties<azdata.CheckBoxProperties>({
label: loc.rememberPassword,
checked: controllerInfo?.rememberPassword
}).component();
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
this.urlInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.url,
// If we have a model then we're editing an existing connection so don't let them modify the URL
readOnly: !!controllerInfo
}).component();
this.nameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.name
}).component();
this.usernameInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
value: controllerInfo?.username
}).component();
this.passwordInputBox = this.modelBuilder.inputBox()
.withProperties<azdata.InputBoxProperties>({
inputType: 'password',
value: password
}).component();
}
protected completionPromise = new Deferred<ConnectToControllerDialogModel | undefined>();
protected id!: string;
constructor(protected treeDataProvider: AzureArcTreeDataProvider, title: string) {
super();
this.dialog = azdata.window.createModelViewDialog(title);
}
public showDialog(controllerInfo?: ControllerInfo, password: string | undefined = undefined): azdata.window.Dialog {
this.id = controllerInfo?.id ?? uuid();
this.dialog.cancelButton.onClick(() => this.handleCancel());
this.dialog.registerContent(async (view) => {
this.modelBuilder = view.modelBuilder;
this.initializeFields(controllerInfo, password);
let formModel = this.modelBuilder.formContainer()
.withFormItems([{
components: [
{
component: this.urlInputBox,
title: loc.controllerUrl,
required: true
}, {
component: this.nameInputBox,
title: loc.controllerName,
required: false
}, {
component: this.usernameInputBox,
title: loc.username,
required: true
}, {
component: this.passwordInputBox,
title: loc.password,
required: true
}, {
component: this.rememberPwCheckBox,
title: ''
}
],
components: this.getComponents(),
title: ''
}]).withLayout({ width: '100%' }).component();
await view.initializeModel(formModel);
this.urlInputBox.focus();
await this.fieldToFocusOn().focus();
this.readonlyFields().forEach(f => f.readOnly = true);
this.initialized = true;
});
dialog.registerCloseValidator(async () => await this.validate());
dialog.okButton.label = loc.connect;
dialog.cancelButton.label = loc.cancel;
azdata.window.openDialog(dialog);
return dialog;
this.dialog.registerCloseValidator(async () => await this.validate());
this.dialog.okButton.label = loc.connect;
this.dialog.cancelButton.label = loc.cancel;
azdata.window.openDialog(this.dialog);
return this.dialog;
}
public abstract async validate(): Promise<boolean>;
private handleCancel(): void {
this.completionPromise.resolve(undefined);
}
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
return this.completionPromise.promise;
}
}
export class ConnectToControllerDialog extends ControllerDialogBase {
protected rememberPwCheckBox!: azdata.CheckBoxComponent;
protected fieldToFocusOn() {
return this.urlInputBox;
}
protected getComponents() {
return [
...super.getComponents(),
{
component: this.rememberPwCheckBox,
title: ''
}];
}
protected initializeFields(controllerInfo: ControllerInfo | undefined, password: string | undefined) {
super.initializeFields(controllerInfo, password);
this.rememberPwCheckBox = this.modelBuilder.checkBox()
.withProperties<azdata.CheckBoxProperties>({
label: loc.rememberPassword,
checked: controllerInfo?.rememberPassword
}).component();
}
constructor(treeDataProvider: AzureArcTreeDataProvider) {
super(treeDataProvider, loc.connectToController);
}
public async validate(): Promise<boolean> {
@@ -120,32 +163,86 @@ export class ConnectToControllerDialog extends InitializingComponent {
url = `${url}:30080`;
}
const controllerInfo: ControllerInfo = {
id: this._id,
id: this.id,
url: url,
name: this.nameInputBox.value ?? '',
username: this.usernameInputBox.value,
rememberPassword: this.rememberPwCheckBox.checked ?? false,
resources: []
};
const controllerModel = new ControllerModel(this._treeDataProvider, controllerInfo, this.passwordInputBox.value);
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
try {
// Validate that we can connect to the controller, this also populates the controllerRegistration from the connection response.
await controllerModel.refresh(false);
// default info.name to the name of the controller instance if the user did not specify their own and to a pre-canned default if for some weird reason controller endpoint returned instanceName is also not a valid value
controllerModel.info.name = controllerModel.info.name || controllerModel.controllerConfig?.metadata.name || loc.defaultControllerName;
} catch (err) {
vscode.window.showErrorMessage(loc.connectToControllerFailed(this.urlInputBox.value, err));
this.dialog.message = {
text: loc.connectToControllerFailed(this.urlInputBox.value, err),
level: azdata.window.MessageLevel.Error
};
return false;
}
this._completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
return true;
}
}
export class PasswordToControllerDialog extends ControllerDialogBase {
constructor(treeDataProvider: AzureArcTreeDataProvider) {
super(treeDataProvider, loc.passwordToController);
}
protected fieldToFocusOn() {
return this.passwordInputBox;
}
protected readonlyFields() {
return [
this.urlInputBox,
this.nameInputBox,
this.usernameInputBox
];
}
public async validate(): Promise<boolean> {
if (!this.passwordInputBox.value) {
return false;
}
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);
} catch (e) {
if (getErrorMessage(e).match(/Wrong username or password/i)) {
this.dialog.message = {
text: loc.invalidPassword,
level: azdata.window.MessageLevel.Error
};
return false;
} else {
this.dialog.message = {
text: loc.errorVerifyingPassword(e),
level: azdata.window.MessageLevel.Error
};
return false;
}
}
const controllerInfo: ControllerInfo = {
id: this.id,
url: this.urlInputBox.value!,
name: this.nameInputBox.value!,
username: this.usernameInputBox.value!,
rememberPassword: false,
resources: []
};
const controllerModel = new ControllerModel(this.treeDataProvider, controllerInfo, this.passwordInputBox.value);
this.completionPromise.resolve({ controllerModel: controllerModel, password: this.passwordInputBox.value });
return true;
}
private handleCancel(): void {
this._completionPromise.resolve(undefined);
}
public waitForClose(): Promise<ConnectToControllerDialogModel | undefined> {
return this._completionPromise.promise;
public showDialog(controllerInfo?: ControllerInfo): azdata.window.Dialog {
const dialog = super.showDialog(controllerInfo);
dialog.okButton.label = loc.ok;
return dialog;
}
}