From c50067b6d29d4767a66c3540d7793f7a6d7aa35c Mon Sep 17 00:00:00 2001 From: Brian Bergeron Date: Fri, 18 Sep 2020 08:49:54 -0700 Subject: [PATCH] Arc - Enable Postgres dashboard (#12439) * get overview, conn strings, properties pages working * hook up password reset, azure link, scale configuration * fix comments * enable opening postgres dashboard from controller dashboard * minor fixes Co-authored-by: Brian Bergeron Co-authored-by: chgagnon --- extensions/arc/package.json | 2 +- extensions/arc/src/models/controllerModel.ts | 14 ----- extensions/arc/src/models/postgresModel.ts | 53 ++++++++++++++++--- .../controllerDashboardOverviewPage.ts | 27 ++++------ .../postgres/postgresConnectionStringsPage.ts | 5 +- .../postgresDiagnoseAndSolveProblemsPage.ts | 2 +- .../postgres/postgresOverviewPage.ts | 41 +++++++------- .../postgres/postgresPropertiesPage.ts | 23 ++++---- extensions/azdata/src/azdata.ts | 36 +++++++------ extensions/azdata/src/extension.ts | 34 ++++++------ extensions/azdata/src/typings/azdata-ext.d.ts | 30 ++++++----- 11 files changed, 144 insertions(+), 123 deletions(-) diff --git a/extensions/arc/package.json b/extensions/arc/package.json index 8126ef5f7f..514da55042 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -96,7 +96,7 @@ "view/item/context": [ { "command": "arc.openDashboard", - "when": "view == azureArc && viewItem && viewItem != postgresInstances)", + "when": "view == azureArc && viewItem", "group": "navigation@1" }, { diff --git a/extensions/arc/src/models/controllerModel.ts b/extensions/arc/src/models/controllerModel.ts index dc0a4110a3..3ad0a04228 100644 --- a/extensions/arc/src/models/controllerModel.ts +++ b/extensions/arc/src/models/controllerModel.ts @@ -20,7 +20,6 @@ export type Registration = { export class ControllerModel { private readonly _azdataApi: azdataExt.IExtension; private _endpoints: azdataExt.DcEndpointListResult[] = []; - private _namespace: string = ''; private _registrations: Registration[] = []; private _controllerConfig: azdataExt.DcConfigShowResult | undefined = undefined; @@ -158,10 +157,6 @@ export class ControllerModel { return this._endpoints.find(e => e.name === name); } - public get namespace(): string { - return this._namespace; - } - public get registrations(): Registration[] { return this._registrations; } @@ -176,15 +171,6 @@ export class ControllerModel { }); } - public async deleteRegistration(_type: ResourceType, _name: string) { - /* TODO chgagnon - if (r && !r.isDeleted && r.customObjectName) { - const r = this.getRegistration(type, name); - await this._registrationRouter.apiV1RegistrationNsNameIsDeletedDelete(this._namespace, r.customObjectName, true); - } - */ - } - /** * property to for use a display label for this controller */ diff --git a/extensions/arc/src/models/postgresModel.ts b/extensions/arc/src/models/postgresModel.ts index 1ba3e3366b..09931200c5 100644 --- a/extensions/arc/src/models/postgresModel.ts +++ b/extensions/arc/src/models/postgresModel.ts @@ -6,6 +6,7 @@ import { ResourceInfo } from 'arc'; import * as azdataExt from 'azdata-ext'; import * as vscode from 'vscode'; +import * as loc from '../localizedConstants'; import { ControllerModel, Registration } from './controllerModel'; import { ResourceModel } from './resourceModel'; import { parseIpAndPort } from '../common/utils'; @@ -23,20 +24,56 @@ export class PostgresModel extends ResourceModel { this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports; } + /** Returns the configuration of Postgres */ public get config(): azdataExt.PostgresServerShowResult | undefined { return this._config; } - /** Returns the IP address and port of the server */ - public get endpoint(): { ip: string, port: string } { - return this._config - ? parseIpAndPort(this._config.status.externalEndpoint) - : { ip: '', port: '' }; + /** Returns the major version of Postgres */ + public get engineVersion(): string | undefined { + const kind = this._config?.kind; + return kind + ? kind.substring(kind.lastIndexOf('-') + 1) + : undefined; } - /** Returns the server's configuration e.g. '3 nodes, 1.5 vCores, 1GiB RAM, 2GiB storage per node' */ - public get configuration(): string { - return ''; // TODO + /** Returns the IP address and port of Postgres */ + public get endpoint(): { ip: string, port: string } | undefined { + return this._config?.status.externalEndpoint + ? parseIpAndPort(this._config.status.externalEndpoint) + : undefined; + } + + /** Returns the scale configuration of Postgres e.g. '3 nodes, 1.5 vCores, 1Gi RAM, 2Gi storage per node' */ + public get scaleConfiguration(): string | undefined { + if (!this._config) { + return undefined; + } + + const cpuLimit = this._config.spec.scheduling?.default?.resources?.limits?.cpu; + const ramLimit = this._config.spec.scheduling?.default?.resources?.limits?.memory; + const cpuRequest = this._config.spec.scheduling?.default?.resources?.requests?.cpu; + const ramRequest = this._config.spec.scheduling?.default?.resources?.requests?.memory; + const storage = this._config.spec.storage?.data?.size; + const nodes = (this._config.spec.scale?.shards ?? 0) + 1; // An extra node for the coordinator + + let configuration: string[] = []; + configuration.push(`${nodes} ${nodes > 1 ? loc.nodes : loc.node}`); + + // Prefer limits if they're provided, otherwise use requests if they're provided + if (cpuLimit || cpuRequest) { + configuration.push(`${cpuLimit ?? cpuRequest!} ${loc.vCores}`); + } + + if (ramLimit || ramRequest) { + configuration.push(`${ramLimit ?? ramRequest!} ${loc.ram}`); + } + + if (storage) { + configuration.push(`${storage} ${loc.storagePerNode}`); + } + + return configuration.join(', '); } /** Refreshes the model */ diff --git a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts index f87ba9a185..100aa53a16 100644 --- a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts @@ -219,23 +219,16 @@ export class ControllerDashboardOverviewPage extends DashboardPage { iconHeight: iconSize, iconWidth: iconSize }).component(); - let nameComponent: azdata.Component; - if (r.instanceType === ResourceType.postgresInstances) { - nameComponent = this.modelView.modelBuilder.text() - .withProperties({ - value: r.instanceName || '', - CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } - }).component(); - } else { - nameComponent = this.modelView.modelBuilder.hyperlink() - .withProperties({ - label: r.instanceName || '', - url: '' - }).component(); - (nameComponent).onDidClick(async () => { - await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', r.instanceName); - }); - } + + const nameComponent = this.modelView.modelBuilder.hyperlink() + .withProperties({ + label: r.instanceName || '', + url: '' + }).component(); + + this.disposables.push(nameComponent.onDidClick(async () => { + await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', r.instanceName); + })); // TODO chgagnon return [imageComponent, nameComponent, resourceTypeToDisplayName(r.instanceType), '-'/* loc.numVCores(r.vCores) */]; diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts index 116dee9869..82760431da 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts @@ -98,7 +98,10 @@ export class PostgresConnectionStringsPage extends DashboardPage { } private getConnectionStrings(): KeyValue[] { - const endpoint: { ip: string, port: string } = this._postgresModel.endpoint; + const endpoint = this._postgresModel.endpoint; + if (!endpoint) { + return []; + } return [ new InputKeyValue(this.modelView.modelBuilder, 'ADO.NET', `Server=${endpoint.ip};Database=postgres;Port=${endpoint.port};User Id=postgres;Password={your_password_here};Ssl Mode=Require;`), diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts index 1060303bd6..cc94746b9b 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts @@ -52,7 +52,7 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage { troubleshootButton.onDidClick(() => { process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.config?.metadata.namespace; process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.info.name; - // TODO set env POSTGRES_SERVER_VERSION + process.env['POSTGRES_SERVER_VERSION'] = this._postgresModel.engineVersion; vscode.commands.executeCommand('bookTreeView.openBook', this._context.asAbsolutePath('notebooks/arcDataServices'), true, 'postgres/tsg100-troubleshoot-postgres'); })); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts index c128d821c5..64d00a62fa 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts @@ -12,6 +12,7 @@ import { DashboardPage } from '../../components/dashboardPage'; import { ControllerModel } from '../../../models/controllerModel'; import { PostgresModel } from '../../../models/postgresModel'; import { promptAndConfirmPassword, promptForInstanceDeletion } from '../../../common/utils'; +import { ResourceType } from 'arc'; export class PostgresOverviewPage extends DashboardPage { @@ -154,7 +155,13 @@ export class PostgresOverviewPage extends DashboardPage { try { const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : ''); if (password) { - // TODO: azdata arc postgres server edit --admin-password + await this._azdataApi.azdata.arc.postgres.server.edit( + this._postgresModel.info.name, + { + adminPassword: true, + noWait: true + }, + { 'AZDATA_PASSWORD': password }); vscode.window.showInformationMessage(loc.passwordReset); } } catch (error) { @@ -220,15 +227,13 @@ export class PostgresOverviewPage extends DashboardPage { this.disposables.push( openInAzurePortalButton.onDidClick(async () => { - /* - const r = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); - if (!r) { - vscode.window.showErrorMessage(loc.couldNotFindAzureResource(this._postgresModel.fullName)); - } else { + const azure = this._controllerModel.controllerConfig?.spec.settings.azure; + if (azure) { vscode.env.openExternal(vscode.Uri.parse( - `https://portal.azure.com/#resource/subscriptions/${r.subscriptionId}/resourceGroups/${r.resourceGroupName}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${r.instanceName}`)); + `https://portal.azure.com/#resource/subscriptions/${azure.subscription}/resourceGroups/${azure.resourceGroup}/providers/Microsoft.AzureData/${ResourceType.postgresInstances}/${this._postgresModel.info.name}`)); + } else { + vscode.window.showErrorMessage(loc.couldNotFindControllerRegistration); } - */ })); return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ @@ -240,22 +245,18 @@ export class PostgresOverviewPage extends DashboardPage { } private getProperties(): azdata.PropertiesContainerItem[] { - /* - const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; + const endpoint = this._postgresModel.endpoint; return [ - { displayName: loc.name, value: this._postgresModel.name }, - { displayName: loc.coordinatorEndpoint, value: `postgresql://postgres@${endpoint.ip}:${endpoint.port}` }, - { displayName: loc.status, value: this._postgresModel.service?.status?.state ?? '' }, + { displayName: loc.name, value: this._postgresModel.info.name }, + { displayName: loc.coordinatorEndpoint, value: endpoint ? `postgresql://postgres@${endpoint.ip}:${endpoint.port}` : '-' }, + { displayName: loc.status, value: this._postgresModel.config?.status.state || '-' }, { displayName: loc.postgresAdminUsername, value: 'postgres' }, - { displayName: loc.dataController, value: this._controllerModel?.namespace ?? '' }, - { displayName: loc.nodeConfiguration, value: this._postgresModel.configuration }, - { displayName: loc.subscriptionId, value: registration?.subscriptionId ?? '' }, - { displayName: loc.postgresVersion, value: this._postgresModel.service?.spec?.engine?.version?.toString() ?? '' } + { displayName: loc.dataController, value: this._controllerModel?.controllerConfig?.metadata.namespace || '-' }, + { displayName: loc.nodeConfiguration, value: this._postgresModel.scaleConfiguration || '-' }, + { displayName: loc.subscriptionId, value: this._controllerModel.controllerConfig?.spec.settings.azure.subscription ?? '' }, + { displayName: loc.postgresVersion, value: this._postgresModel.engineVersion ?? '-' } ]; - */ - return []; } private getKibanaLink(): string { diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts index 00ed6e6f48..f2ebd8c423 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as loc from '../../../localizedConstants'; import { IconPathHelper, cssStyles } from '../../../constants'; -import { KeyValueContainer, KeyValue } from '../../components/keyValueContainer'; +import { KeyValueContainer, KeyValue, InputKeyValue, TextKeyValue } from '../../components/keyValueContainer'; import { DashboardPage } from '../../components/dashboardPage'; import { ControllerModel } from '../../../models/controllerModel'; import { PostgresModel } from '../../../models/postgresModel'; @@ -91,24 +91,19 @@ export class PostgresPropertiesPage extends DashboardPage { } private getProperties(): KeyValue[] { - /* - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; - const connectionString = `postgresql://postgres@${endpoint.ip}:${endpoint.port}`; - const registration = this._controllerModel.getRegistration(ResourceType.postgresInstances, this._postgresModel.namespace, this._postgresModel.name); + const endpoint = this._postgresModel.endpoint; return [ - new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, connectionString), + new InputKeyValue(this.modelView.modelBuilder, loc.coordinatorEndpoint, endpoint ? `postgresql://postgres@${endpoint.ip}:${endpoint.port}` : ''), new InputKeyValue(this.modelView.modelBuilder, loc.postgresAdminUsername, 'postgres'), - new TextKeyValue(this.modelView.modelBuilder, loc.status, this._postgresModel.service?.status?.state ?? 'Unknown'), + new TextKeyValue(this.modelView.modelBuilder, loc.status, this._postgresModel.config?.status.state ?? loc.unknown), // TODO: Make this a LinkKeyValue that opens the controller dashboard - new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.namespace ?? ''), - new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.configuration), - new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.service?.spec?.engine?.version?.toString() ?? ''), - new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, registration?.resourceGroupName ?? ''), - new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, registration?.subscriptionId ?? '') + new TextKeyValue(this.modelView.modelBuilder, loc.dataController, this._controllerModel.controllerConfig?.metadata.namespace ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.nodeConfiguration, this._postgresModel.scaleConfiguration ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.postgresVersion, this._postgresModel.engineVersion ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.resourceGroup, this._controllerModel.controllerConfig?.spec.settings.azure.resourceGroup ?? ''), + new TextKeyValue(this.modelView.modelBuilder, loc.subscriptionId, this._controllerModel.controllerConfig?.spec.settings.azure.subscription ?? '') ]; - */ - return []; } private handleRegistrationsUpdated() { diff --git a/extensions/azdata/src/azdata.ts b/extensions/azdata/src/azdata.ts index 01b60d0c51..f56835d949 100644 --- a/extensions/azdata/src/azdata.ts +++ b/extensions/azdata/src/azdata.ts @@ -101,21 +101,23 @@ export class AzdataTool implements IAzdataTool { show: async (name: string) => { return this.executeCommand(['arc', 'postgres', 'server', 'show', '-n', name]); }, - edit: async (args: { + edit: async ( name: string, - adminPassword?: boolean, - coresLimit?: string, - coresRequest?: string, - engineSettings?: string, - extensions?: string, - memoryLimit?: string, - memoryRequest?: string, - noWait?: boolean, - port?: number, - replaceEngineSettings?: boolean, - workers?: number - }) => { - const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', args.name]; + args: { + adminPassword?: boolean, + coresLimit?: string, + coresRequest?: string, + engineSettings?: string, + extensions?: string, + memoryLimit?: string, + memoryRequest?: string, + noWait?: boolean, + port?: number, + replaceEngineSettings?: boolean, + workers?: number + }, + additionalEnvVars?: { [key: string]: string }) => { + const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name]; if (args.adminPassword) { argsArray.push('--admin-password'); } if (args.coresLimit !== undefined) { argsArray.push('--cores-limit', args.coresLimit); } if (args.coresRequest !== undefined) { argsArray.push('--cores-request', args.coresRequest); } @@ -127,7 +129,7 @@ export class AzdataTool implements IAzdataTool { if (args.port !== undefined) { argsArray.push('--port', args.port.toString()); } if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); } if (args.workers !== undefined) { argsArray.push('--workers', args.workers.toString()); } - return this.executeCommand(argsArray); + return this.executeCommand(argsArray, additionalEnvVars); } } }, @@ -182,18 +184,18 @@ export class AzdataTool implements IAzdataTool { // ERROR: { stderr: '...' } // so we also need to trim off the start that isn't a valid JSON blob err.stderr = JSON.parse(err.stderr.substring(err.stderr.indexOf('{'), err.stderr.indexOf('}') + 1)).stderr; - } catch (err) { + } catch { // it means this was probably some other generic error (such as command not being found) // check if azdata still exists if it does then rethrow the original error if not then emit a new specific error. try { await fs.promises.access(this._path); //this.path exists - throw err; // rethrow the error } catch (e) { // this.path does not exist await vscode.commands.executeCommand('setContext', azdataFound, false); throw (loc.noAzdata); } + throw err; // rethrow the original error } } diff --git a/extensions/azdata/src/extension.ts b/extensions/azdata/src/extension.ts index b874061027..7706271ebf 100644 --- a/extensions/azdata/src/extension.ts +++ b/extensions/azdata/src/extension.ts @@ -86,7 +86,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.postgres.server.delete(name); }, list: async () => { @@ -97,22 +97,24 @@ export async function activate(context: vscode.ExtensionContext): Promise { - await throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.postgres.server.edit(args); + args: { + adminPassword?: boolean, + coresLimit?: string, + coresRequest?: string, + engineSettings?: string, + extensions?: string, + memoryLimit?: string, + memoryRequest?: string, + noWait?: boolean, + port?: number, + replaceEngineSettings?: boolean, + workers?: number + }, + additionalEnvVars?: { [key: string]: string }) => { + throwIfNoAzdataOrEulaNotAccepted(); + return localAzdata!.arc.postgres.server.edit(name, args, additionalEnvVars); } } }, diff --git a/extensions/azdata/src/typings/azdata-ext.d.ts b/extensions/azdata/src/typings/azdata-ext.d.ts index 7fb577ee1d..6927975496 100644 --- a/extensions/azdata/src/typings/azdata-ext.d.ts +++ b/extensions/azdata/src/typings/azdata-ext.d.ts @@ -162,7 +162,7 @@ declare module 'azdata-ext' { name: string // "citus" }[], settings: { - default: { } // { "max_connections": "101", "work_mem": "4MB" } + default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" } } }, scale: { @@ -233,20 +233,22 @@ declare module 'azdata-ext' { delete(name: string): Promise>, list(): Promise>, show(name: string): Promise>, - edit(args: { + edit( name: string, - adminPassword?: boolean, - coresLimit?: string, - coresRequest?: string, - engineSettings?: string, - extensions?: string, - memoryLimit?: string, - memoryRequest?: string, - noWait?: boolean, - port?: number, - replaceEngineSettings?: boolean, - workers?: number - }): Promise> + args: { + adminPassword?: boolean, + coresLimit?: string, + coresRequest?: string, + engineSettings?: string, + extensions?: string, + memoryLimit?: string, + memoryRequest?: string, + noWait?: boolean, + port?: number, + replaceEngineSettings?: boolean, + workers?: number + }, + additionalEnvVars?: { [key: string]: string }): Promise> } }, sql: {