diff --git a/extensions/arc/src/common/utils.ts b/extensions/arc/src/common/utils.ts index 071cb67e65..dc54765962 100644 --- a/extensions/arc/src/common/utils.ts +++ b/extensions/arc/src/common/utils.ts @@ -194,23 +194,6 @@ export function getErrorMessage(error: any): string { return error.message ?? error; } -/** - * Parses an instance name from the controller. An instance name will either be just its name - * e.g. myinstance or namespace_name e.g. mynamespace_my-instance. - * @param instanceName The instance name in one of the formats described - */ -export function parseInstanceName(instanceName: string | undefined): string { - instanceName = instanceName ?? ''; - const parts: string[] = instanceName.split('_'); - if (parts.length === 2) { - instanceName = parts[1]; - } - else if (parts.length > 2) { - throw new Error(`Cannot parse resource '${instanceName}'. Acceptable formats are 'namespace_name' or 'name'.`); - } - return instanceName; -} - /** * Parses an address into its separate ip and port values. Address must be in the form : * @param address The address to parse diff --git a/extensions/arc/src/models/controllerModel.ts b/extensions/arc/src/models/controllerModel.ts index 34441f6a1f..c68774d17a 100644 --- a/extensions/arc/src/models/controllerModel.ts +++ b/extensions/arc/src/models/controllerModel.ts @@ -6,7 +6,7 @@ import { ControllerInfo, ResourceType } from 'arc'; import * as azdataExt from 'azdata-ext'; import * as vscode from 'vscode'; -import { parseInstanceName, UserCancelledError } from '../common/utils'; +import { UserCancelledError } from '../common/utils'; import * as loc from '../localizedConstants'; import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; @@ -171,7 +171,7 @@ export class ControllerModel { public getRegistration(type: ResourceType, name: string): Registration | undefined { return this._registrations.find(r => { - return r.instanceType === type && parseInstanceName(r.instanceName) === name; + return r.instanceType === type && r.instanceName === name; }); } diff --git a/extensions/arc/src/models/postgresModel.ts b/extensions/arc/src/models/postgresModel.ts index 4c051a629f..1ba3e3366b 100644 --- a/extensions/arc/src/models/postgresModel.ts +++ b/extensions/arc/src/models/postgresModel.ts @@ -4,278 +4,46 @@ *--------------------------------------------------------------------------------------------*/ import { ResourceInfo } from 'arc'; +import * as azdataExt from 'azdata-ext'; import * as vscode from 'vscode'; -import * as loc from '../localizedConstants'; -import { Registration } from './controllerModel'; +import { ControllerModel, Registration } from './controllerModel'; import { ResourceModel } from './resourceModel'; - -export enum PodRole { - Monitor, - Router, - Shard -} - -export interface V1Pod { - 'apiVersion'?: string; - 'kind'?: string; - 'metadata'?: any; // V1ObjectMeta; - 'spec'?: any; // V1PodSpec; - 'status'?: V1PodStatus; -} - -export interface V1PodStatus { - 'conditions'?: any[]; // Array; - 'containerStatuses'?: Array; - 'ephemeralContainerStatuses'?: any[]; // Array; - 'hostIP'?: string; - 'initContainerStatuses'?: any[]; // Array; - 'message'?: string; - 'nominatedNodeName'?: string; - 'phase'?: string; - 'podIP'?: string; - 'podIPs'?: any[]; // Array; - 'qosClass'?: string; - 'reason'?: string; - 'startTime'?: Date | null; -} - -export interface V1ContainerStatus { - 'containerID'?: string; - 'image'?: string; - 'imageID'?: string; - 'lastState'?: any; // V1ContainerState; - 'name'?: string; - 'ready'?: boolean; - 'restartCount'?: number; - 'started'?: boolean | null; - 'state'?: any; // V1ContainerState; -} - -export interface DuskyObjectModelsDatabaseService { - 'apiVersion'?: string; - 'kind'?: string; - 'metadata'?: any; // V1ObjectMeta; - 'spec'?: any; // DuskyObjectModelsDatabaseServiceSpec; - 'status'?: any; // DuskyObjectModelsDatabaseServiceStatus; - 'arc'?: any; // DuskyObjectModelsDatabaseServiceArcPayload; -} - -export interface V1Status { - 'apiVersion'?: string; - 'code'?: number | null; - 'details'?: any; // V1StatusDetails; - 'kind'?: string; - 'message'?: string; - 'metadata'?: any; // V1ListMeta; - 'reason'?: string; - 'status'?: string; - 'hasObject'?: boolean; -} - -export interface DuskyObjectModelsDatabase { - 'name'?: string; - 'owner'?: string; - 'sharded'?: boolean | null; -} +import { parseIpAndPort } from '../common/utils'; export class PostgresModel extends ResourceModel { - private _service?: DuskyObjectModelsDatabaseService; - private _pods?: V1Pod[]; - private readonly _onServiceUpdated = new vscode.EventEmitter(); - private readonly _onPodsUpdated = new vscode.EventEmitter(); - public onServiceUpdated = this._onServiceUpdated.event; - public onPodsUpdated = this._onPodsUpdated.event; - public serviceLastUpdated?: Date; - public podsLastUpdated?: Date; + private _config?: azdataExt.PostgresServerShowResult; + private readonly _azdataApi: azdataExt.IExtension; - constructor(info: ResourceInfo, registration: Registration) { + private readonly _onConfigUpdated = new vscode.EventEmitter(); + public onConfigUpdated = this._onConfigUpdated.event; + public configLastUpdated?: Date; + + constructor(private _controllerModel: ControllerModel, info: ResourceInfo, registration: Registration) { super(info, registration); + this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports; } - /** Returns the service's Kubernetes namespace */ - public get namespace(): string | undefined { - return ''; // TODO chgagnon return this.info.namespace; + public get config(): azdataExt.PostgresServerShowResult | undefined { + return this._config; } - /** Returns the service's name */ - public get name(): string { - return this.info.name; + /** 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 service's fully qualified name in the format namespace.name */ - public get fullName(): string { - return `${this.namespace}.${this.name}`; - } - - /** Returns the service's spec */ - public get service(): DuskyObjectModelsDatabaseService | undefined { - return this._service; - } - - /** Returns the service's pods */ - public get pods(): V1Pod[] | undefined { - return this._pods; + /** Returns the server's configuration e.g. '3 nodes, 1.5 vCores, 1GiB RAM, 2GiB storage per node' */ + public get configuration(): string { + return ''; // TODO } /** Refreshes the model */ public async refresh() { - await Promise.all([ - /* TODO enable - this._databaseRouter.getDuskyDatabaseService(this.info.namespace || 'test', this.info.name).then(response => { - this._service = response.body; - this.serviceLastUpdated = new Date(); - this._onServiceUpdated.fire(this._service); - }), - this._databaseRouter.getDuskyPods(this.info.namespace || 'test', this.info.name).then(response => { - this._pods = response.body; - this.podsLastUpdated = new Date(); - this._onPodsUpdated.fire(this._pods!); - }) - */ - ]); - } - - /** - * Updates the service - * @param func A function of modifications to apply to the service - */ - public async update(_func: (service: DuskyObjectModelsDatabaseService) => void): Promise { - return undefined; - /* - // Get the latest spec of the service in case it has changed - const service = (await this._databaseRouter.getDuskyDatabaseService(this.info.namespace || 'test', this.info.name)).body; - service.status = undefined; // can't update the status - func(service); - - return await this._databaseRouter.updateDuskyDatabaseService(this.namespace || 'test', this.name, service).then(r => { - this._service = r.body; - return this._service; - }); - */ - } - - /** Deletes the service */ - public async delete(): Promise { - return undefined; - // return (await this._databaseRouter.deleteDuskyDatabaseService(this.info.namespace || 'test', this.info.name)).body; - } - - /** Creates a SQL database in the service */ - public async createDatabase(_db: DuskyObjectModelsDatabase): Promise { - return undefined; - // return (await this._databaseRouter.createDuskyDatabase(this.namespace || 'test', this.name, db)).body; - } - - /** - * Returns the IP address and port of the service, preferring external IP over - * internal IP. If either field is not available it will be set to undefined. - */ - public get endpoint(): { ip?: string, port?: number } { - const externalIp = this._service?.status?.externalIP; - const internalIp = this._service?.status?.internalIP; - const externalPort = this._service?.status?.externalPort; - const internalPort = this._service?.status?.internalPort; - - return externalIp ? { ip: externalIp, port: externalPort ?? undefined } - : internalIp ? { ip: internalIp, port: internalPort ?? undefined } - : { ip: undefined, port: undefined }; - } - - /** Returns the service's configuration e.g. '3 nodes, 1.5 vCores, 1GiB RAM, 2GiB storage per node' */ - public get configuration(): string { - - // TODO: Resource requests and limits can be configured per role. Figure out how - // to display that in the UI. For now, only show the default configuration. - const cpuLimit = this._service?.spec?.scheduling?._default?.resources?.limits?.['cpu']; - const ramLimit = this._service?.spec?.scheduling?._default?.resources?.limits?.['memory']; - const cpuRequest = this._service?.spec?.scheduling?._default?.resources?.requests?.['cpu']; - const ramRequest = this._service?.spec?.scheduling?._default?.resources?.requests?.['memory']; - const storage = this._service?.spec?.storage?.volumeSize; - const nodes = this.pods?.length; - - let configuration: string[] = []; - - if (nodes) { - 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(`${this.formatCores(cpuLimit ?? cpuRequest!)} ${loc.vCores}`); - } - - if (ramLimit || ramRequest) { - configuration.push(`${this.formatMemory(ramLimit ?? ramRequest!)} ${loc.ram}`); - } - - if (storage) { - configuration.push(`${this.formatMemory(storage)} ${loc.storagePerNode}`); - } - - return configuration.join(', '); - } - - /** Given a V1Pod, returns its PodRole or undefined if the role isn't known */ - public static getPodRole(pod: V1Pod): PodRole | undefined { - const name = pod.metadata?.name; - const role = name?.substring(name.lastIndexOf('-'))[1]; - switch (role) { - case 'm': return PodRole.Monitor; - case 'r': return PodRole.Router; - case 's': return PodRole.Shard; - default: return undefined; - } - } - - /** Given a PodRole, returns its localized name */ - public static getPodRoleName(role?: PodRole): string { - switch (role) { - case PodRole.Monitor: return loc.monitor; - case PodRole.Router: return loc.coordinator; - case PodRole.Shard: return loc.worker; - default: return ''; - } - } - - /** Given a V1Pod returns its status */ - public static getPodStatus(pod: V1Pod): string { - const phase = pod.status?.phase; - if (phase !== 'Running') { - return phase ?? ''; - } - - // Pods can be in the running phase while some - // containers are crashing, so check those too. - for (let c of pod.status?.containerStatuses?.filter(c => !c.ready) ?? []) { - const wReason = c.state?.waiting?.reason; - const tReason = c.state?.terminated?.reason; - if (wReason) { return wReason; } - if (tReason) { return tReason; } - } - - return loc.running; - } - - /** - * Converts millicores to cores (600m -> 0.6 cores) - * https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu - * @param cores The millicores to format e.g. 600m - */ - private formatCores(cores: string): number { - return cores?.endsWith('m') ? +cores.slice(0, -1) / 1000 : +cores; - } - - /** - * Formats the memory to end with 'B' e.g: - * 1 -> 1B - * 1K -> 1KB, 1Ki -> 1KiB - * 1M -> 1MB, 1Mi -> 1MiB - * 1G -> 1GB, 1Gi -> 1GiB - * 1T -> 1TB, 1Ti -> 1TiB - * https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory - * @param memory The amount + unit of memory to format e.g. 1K - */ - private formatMemory(memory: string): string { - return memory && !memory.endsWith('B') ? `${memory}B` : memory; + await this._controllerModel.azdataLogin(); + this._config = (await this._azdataApi.azdata.arc.postgres.server.show(this.info.name)).result; + this.configLastUpdated = new Date(); + this._onConfigUpdated.fire(this._config); } } diff --git a/extensions/arc/src/test/common/utils.test.ts b/extensions/arc/src/test/common/utils.test.ts index fde0f65ac2..275162f95f 100644 --- a/extensions/arc/src/test/common/utils.test.ts +++ b/extensions/arc/src/test/common/utils.test.ts @@ -7,7 +7,7 @@ import { ResourceType } from 'arc'; import 'mocha'; import * as should from 'should'; import * as vscode from 'vscode'; -import { getAzurecoreApi, getConnectionModeDisplayText, getDatabaseStateDisplayText, getErrorMessage, getResourceTypeIcon, parseEndpoint, parseInstanceName, parseIpAndPort, promptAndConfirmPassword, promptForResourceDeletion, resourceTypeToDisplayName } from '../../common/utils'; +import { getAzurecoreApi, getConnectionModeDisplayText, getDatabaseStateDisplayText, getErrorMessage, getResourceTypeIcon, parseEndpoint, parseIpAndPort, promptAndConfirmPassword, promptForResourceDeletion, resourceTypeToDisplayName } from '../../common/utils'; import { ConnectionMode as ConnectionMode, IconPathHelper } from '../../constants'; import * as loc from '../../localizedConstants'; import { MockInputBox } from '../stubs'; @@ -47,24 +47,6 @@ describe('parseEndpoint Method Tests', function (): void { }); }); -describe('parseInstanceName Method Tests', () => { - it('Should parse valid instanceName with namespace correctly', function (): void { - should(parseInstanceName('mynamespace_myinstance')).equal('myinstance'); - }); - - it('Should parse valid instanceName without namespace correctly', function (): void { - should(parseInstanceName('myinstance')).equal('myinstance'); - }); - - it('Should return empty string when undefined value passed in', function (): void { - should(parseInstanceName(undefined)).equal(''); - }); - - it('Should return empty string when empty string value passed in', function (): void { - should(parseInstanceName('')).equal(''); - }); -}); - describe('getAzurecoreApi Method Tests', function () { it('Should get azurecore API correctly', function (): void { should(getAzurecoreApi()).not.be.undefined(); @@ -260,22 +242,6 @@ describe('getErrorMessage Method Tests', function () { }); }); -describe('parseInstanceName Method Tests', function () { - it('2 part name', function (): void { - const name = 'MyName'; - should(parseInstanceName(`MyNamespace_${name}`)).equal(name); - }); - - it('1 part name', function (): void { - const name = 'MyName'; - should(parseInstanceName(name)).equal(name); - }); - - it('Invalid name', function (): void { - should(() => parseInstanceName('Some_Invalid_Name')).throwError(); - }); -}); - describe('parseIpAndPort', function (): void { it('Valid address', function (): void { const ip = '127.0.0.1'; diff --git a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts index 13d53c3d81..f87ba9a185 100644 --- a/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/controller/controllerDashboardOverviewPage.ts @@ -7,7 +7,7 @@ import { ResourceType } from 'arc'; import * as azdata from 'azdata'; import * as azurecore from 'azurecore'; import * as vscode from 'vscode'; -import { getConnectionModeDisplayText, getResourceTypeIcon, parseInstanceName, resourceTypeToDisplayName } from '../../../common/utils'; +import { getConnectionModeDisplayText, getResourceTypeIcon, resourceTypeToDisplayName } from '../../../common/utils'; import { cssStyles, Endpoints, IconPathHelper, iconSize } from '../../../constants'; import * as loc from '../../../localizedConstants'; import { ControllerModel } from '../../../models/controllerModel'; @@ -233,7 +233,7 @@ export class ControllerDashboardOverviewPage extends DashboardPage { url: '' }).component(); (nameComponent).onDidClick(async () => { - await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', parseInstanceName(r.instanceName)); + await this._controllerModel.treeDataProvider.openResourceDashboard(this._controllerModel, r.instanceType || '', r.instanceName); }); } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts index 50857543eb..116dee9869 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresConnectionStringsPage.ts @@ -18,7 +18,7 @@ export class PostgresConnectionStringsPage extends DashboardPage { constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { super(modelView); - this.disposables.push(this._postgresModel.onServiceUpdated( + this.disposables.push(this._postgresModel.onConfigUpdated( () => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated()))); } @@ -65,7 +65,7 @@ export class PostgresConnectionStringsPage extends DashboardPage { this.loading = this.modelView.modelBuilder.loadingComponent() .withItem(this.keyValueContainer.container) .withProperties({ - loading: !this._postgresModel.serviceLastUpdated + loading: !this._postgresModel.configLastUpdated }).component(); content.addItem(this.loading); @@ -98,7 +98,7 @@ export class PostgresConnectionStringsPage extends DashboardPage { } private getConnectionStrings(): KeyValue[] { - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; + const endpoint: { ip: string, port: string } = this._postgresModel.endpoint; 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/postgresDashboard.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts index 9951e99a8c..6bb47cb5d8 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDashboard.ts @@ -14,11 +14,10 @@ import { PostgresPropertiesPage } from './postgresPropertiesPage'; import { Dashboard } from '../../components/dashboard'; import { PostgresDiagnoseAndSolveProblemsPage } from './postgresDiagnoseAndSolveProblemsPage'; import { PostgresSupportRequestPage } from './postgresSupportRequestPage'; -import { PostgresResourceHealthPage } from './postgresResourceHealthPage'; export class PostgresDashboard extends Dashboard { constructor(private _context: vscode.ExtensionContext, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { - super(loc.postgresDashboard(_postgresModel.name)); + super(loc.postgresDashboard(_postgresModel.info.name)); } public async showDashboard(): Promise { @@ -33,7 +32,6 @@ export class PostgresDashboard extends Dashboard { const overviewPage = new PostgresOverviewPage(modelView, this._controllerModel, this._postgresModel); const connectionStringsPage = new PostgresConnectionStringsPage(modelView, this._postgresModel); const propertiesPage = new PostgresPropertiesPage(modelView, this._controllerModel, this._postgresModel); - const resourceHealthPage = new PostgresResourceHealthPage(modelView, this._postgresModel); const diagnoseAndSolveProblemsPage = new PostgresDiagnoseAndSolveProblemsPage(modelView, this._context, this._postgresModel); const supportRequestPage = new PostgresSupportRequestPage(modelView); @@ -49,7 +47,6 @@ export class PostgresDashboard extends Dashboard { { title: loc.supportAndTroubleshooting, tabs: [ - resourceHealthPage.tab, diagnoseAndSolveProblemsPage.tab, supportRequestPage.tab ] diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts index bb884d3b2b..1060303bd6 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresDiagnoseAndSolveProblemsPage.ts @@ -50,8 +50,9 @@ export class PostgresDiagnoseAndSolveProblemsPage extends DashboardPage { this.disposables.push( troubleshootButton.onDidClick(() => { - process.env['POSTGRES_SERVER_NAMESPACE'] = this._postgresModel.namespace; - process.env['POSTGRES_SERVER_NAME'] = this._postgresModel.name; + 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 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 a3d501cc73..1b3c0c6e83 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresOverviewPage.ts @@ -17,12 +17,10 @@ export class PostgresOverviewPage extends DashboardPage { private propertiesLoading?: azdata.LoadingComponent; private kibanaLoading?: azdata.LoadingComponent; private grafanaLoading?: azdata.LoadingComponent; - private nodesTableLoading?: azdata.LoadingComponent; private properties?: azdata.PropertiesContainerComponent; private kibanaLink?: azdata.HyperlinkComponent; private grafanaLink?: azdata.HyperlinkComponent; - private nodesTable?: azdata.DeclarativeTableComponent; constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { super(modelView); @@ -30,8 +28,7 @@ export class PostgresOverviewPage extends DashboardPage { this.disposables.push( this._controllerModel.onEndpointsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdated())), this._controllerModel.onRegistrationsUpdated(() => this.eventuallyRunOnInitialized(() => this.handleRegistrationsUpdated())), - this._postgresModel.onServiceUpdated(() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())), - this._postgresModel.onPodsUpdated(() => this.eventuallyRunOnInitialized(() => this.handlePodsUpdated()))); + this._postgresModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleConfigUpdated()))); } protected get title(): string { @@ -60,7 +57,7 @@ export class PostgresOverviewPage extends DashboardPage { this.propertiesLoading = this.modelView.modelBuilder.loadingComponent() .withItem(this.properties) .withProperties({ - loading: !this._controllerModel.registrationsLastUpdated && !this._postgresModel.serviceLastUpdated && !this._postgresModel.podsLastUpdated + loading: !this._controllerModel.registrationsLastUpdated && !this._postgresModel.configLastUpdated }).component(); content.addItem(this.propertiesLoading, { CSSStyles: cssStyles.text }); @@ -134,60 +131,8 @@ export class PostgresOverviewPage extends DashboardPage { [loc.kibanaDashboard, this.kibanaLoading, loc.kibanaDashboardDescription], [loc.grafanaDashboard, this.grafanaLoading, loc.grafanaDashboardDescription]] }).component(); + content.addItem(endpointsTable); - - // Server group nodes - content.addItem(this.modelView.modelBuilder.text().withProperties({ - value: loc.serverGroupNodes, - CSSStyles: titleCSS - }).component()); - - this.nodesTable = this.modelView.modelBuilder.declarativeTable().withProperties({ - width: '100%', - columns: [ - { - displayName: loc.name, - valueType: azdata.DeclarativeDataType.string, - isReadOnly: true, - width: '30%', - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: loc.type, - valueType: azdata.DeclarativeDataType.string, - isReadOnly: true, - width: '15%', - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: loc.status, - valueType: azdata.DeclarativeDataType.string, - isReadOnly: true, - width: '20%', - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: loc.fullyQualifiedDomain, - valueType: azdata.DeclarativeDataType.string, - isReadOnly: true, - width: '35%', - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - } - ], - data: this.getNodes() - }).component(); - - this.nodesTableLoading = this.modelView.modelBuilder.loadingComponent() - .withItem(this.nodesTable) - .withProperties({ - loading: !this._postgresModel.serviceLastUpdated && !this._postgresModel.podsLastUpdated - }).component(); - - content.addItem(this.nodesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } }); this.initialized = true; return root; } @@ -205,11 +150,7 @@ export class PostgresOverviewPage extends DashboardPage { try { const password = await promptAndConfirmPassword(input => !input ? loc.enterANonEmptyPassword : ''); if (password) { - await this._postgresModel.update(s => { - // TODO chgagnon - // s.arc = s.arc ?? new DuskyObjectModelsDatabaseServiceArcPayload(); - s.arc.servicePassword = password; - }); + // TODO: azdata arc postgres server edit --admin-password vscode.window.showInformationMessage(loc.passwordReset); } } catch (error) { @@ -237,7 +178,7 @@ export class PostgresOverviewPage extends DashboardPage { } */ } catch (error) { - vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._postgresModel.fullName, error)); + vscode.window.showErrorMessage(loc.resourceDeletionFailed(this._postgresModel.info.name, error)); } finally { deleteButton.enabled = true; } @@ -256,7 +197,6 @@ export class PostgresOverviewPage extends DashboardPage { this.propertiesLoading!.loading = true; this.kibanaLoading!.loading = true; this.grafanaLoading!.loading = true; - this.nodesTableLoading!.loading = true; await Promise.all([ this._postgresModel.refresh(), @@ -317,36 +257,18 @@ export class PostgresOverviewPage extends DashboardPage { } private getKibanaLink(): string { - const kibanaQuery = `kubernetes_namespace:"${this._postgresModel.namespace}" and custom_resource_name:"${this._postgresModel.name}"`; + const namespace = this._postgresModel.config?.metadata.namespace; + const kibanaQuery = `kubernetes_namespace:"${namespace}" and custom_resource_name:"${this._postgresModel.info.name}"`; return `${this._controllerModel.getEndpoint(Endpoints.logsui)?.endpoint}/app/kibana#/discover?_a=(query:(language:kuery,query:'${kibanaQuery}'))`; } private getGrafanaLink(): string { - const grafanaQuery = `var-Namespace=${this._postgresModel.namespace}&var-Name=${this._postgresModel.name}`; + const namespace = this._postgresModel.config?.metadata.namespace; + const grafanaQuery = `var-Namespace=${namespace}&var-Name=${this._postgresModel.info.name}`; return `${this._controllerModel.getEndpoint(Endpoints.metricsui)?.endpoint}/d/postgres-metrics?${grafanaQuery}`; } - private getNodes(): string[][] { - /* TODO chgagnon - const endpoint: { ip?: string, port?: number } = this._postgresModel.endpoint; - return this._postgresModel.pods?.map((pod: V1Pod) => { - const name = pod.metadata?.name ?? ''; - const role: PodRole | undefined = PostgresModel.getPodRole(pod); - const service = pod.metadata?.annotations?.['arcdata.microsoft.com/serviceHost']; - const internalDns = service ? `${name}.${service}` : ''; - - return [ - name, - PostgresModel.getPodRoleName(role), - PostgresModel.getPodStatus(pod), - role === PodRole.Router ? `${endpoint.ip}:${endpoint.port}` : internalDns - ]; - }) ?? []; - */ - return []; - } - private handleEndpointsUpdated() { this.kibanaLink!.label = this.getKibanaLink(); this.kibanaLink!.url = this.getKibanaLink(); @@ -362,19 +284,8 @@ export class PostgresOverviewPage extends DashboardPage { this.propertiesLoading!.loading = false; } - private handleServiceUpdated() { + private handleConfigUpdated() { this.properties!.propertyItems = this.getProperties(); this.propertiesLoading!.loading = false; - - this.nodesTable!.data = this.getNodes(); - this.nodesTableLoading!.loading = false; - } - - private handlePodsUpdated() { - this.properties!.propertyItems = this.getProperties(); - this.propertiesLoading!.loading = false; - - this.nodesTable!.data = this.getNodes(); - this.nodesTableLoading!.loading = false; } } diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts index 393e9b129a..00ed6e6f48 100644 --- a/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts +++ b/extensions/arc/src/ui/dashboards/postgres/postgresPropertiesPage.ts @@ -19,7 +19,7 @@ export class PostgresPropertiesPage extends DashboardPage { constructor(protected modelView: azdata.ModelView, private _controllerModel: ControllerModel, private _postgresModel: PostgresModel) { super(modelView); - this.disposables.push(this._postgresModel.onServiceUpdated( + this.disposables.push(this._postgresModel.onConfigUpdated( () => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated()))); this.disposables.push(this._controllerModel.onRegistrationsUpdated( @@ -54,7 +54,7 @@ export class PostgresPropertiesPage extends DashboardPage { this.loading = this.modelView.modelBuilder.loadingComponent() .withItem(this.keyValueContainer.container) .withProperties({ - loading: !this._postgresModel.serviceLastUpdated && !this._controllerModel.registrationsLastUpdated + loading: !this._postgresModel.configLastUpdated && !this._controllerModel.registrationsLastUpdated }).component(); content.addItem(this.loading); diff --git a/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts b/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts deleted file mode 100644 index 2ea339f92b..0000000000 --- a/extensions/arc/src/ui/dashboards/postgres/postgresResourceHealthPage.ts +++ /dev/null @@ -1,225 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as vscode from 'vscode'; -import * as azdata from 'azdata'; -import * as loc from '../../../localizedConstants'; -import { IconPathHelper, cssStyles } from '../../../constants'; -import { DashboardPage } from '../../components/dashboardPage'; -import { PostgresModel } from '../../../models/postgresModel'; -import { fromNow } from '../../../common/date'; - -export class PostgresResourceHealthPage extends DashboardPage { - private interval: NodeJS.Timeout; - private podsUpdated?: azdata.TextComponent; - private podsLoading?: azdata.LoadingComponent; - private conditionsLoading?: azdata.LoadingComponent; - private podsTable?: azdata.DeclarativeTableComponent; - private conditionsTable?: azdata.DeclarativeTableComponent; - - constructor(protected modelView: azdata.ModelView, private _postgresModel: PostgresModel) { - super(modelView); - - this.disposables.push( - modelView.onClosed(() => { - try { clearInterval(this.interval); } - catch { } - })); - - this.disposables.push(this._postgresModel.onServiceUpdated( - () => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated()))); - - // Keep the last updated timestamps up to date with the current time - this.interval = setInterval(() => this.handleServiceUpdated(), 60 * 1000); - } - - protected get title(): string { - return loc.resourceHealth; - } - - protected get id(): string { - return 'postgres-resource-health'; - } - - protected get icon(): { dark: string; light: string; } { - return IconPathHelper.health; - } - - protected get container(): azdata.Component { - const root = this.modelView.modelBuilder.divContainer().component(); - const content = this.modelView.modelBuilder.divContainer().component(); - root.addItem(content, { CSSStyles: { 'margin': '20px' } }); - - content.addItem(this.modelView.modelBuilder.text().withProperties({ - value: loc.resourceHealth, - CSSStyles: { ...cssStyles.title, 'margin-bottom': '30px' } - }).component()); - - content.addItem(this.modelView.modelBuilder.text().withProperties({ - value: loc.podOverview, - CSSStyles: { ...cssStyles.title, 'margin-block-end': '0' } - }).component()); - - this.podsUpdated = this.modelView.modelBuilder.text().withProperties({ - value: this.getPodsLastUpdated(), - CSSStyles: { ...cssStyles.text, 'font-size': '12px', 'margin-block-start': '0' } - }).component(); - - content.addItem(this.podsUpdated); - - // Pod overview - this.podsTable = this.modelView.modelBuilder.declarativeTable().withProperties({ - columns: [ - { - displayName: '', - valueType: azdata.DeclarativeDataType.string, - width: '35%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: { ...cssStyles.tableRow, 'font-size': '20px', 'font-weight': 'bold', 'padding': '7px' } - }, - { - displayName: '', - valueType: azdata.DeclarativeDataType.string, - width: '65%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: { ...cssStyles.tableRow, 'padding': '7px' } - } - ], - data: this.getPodsTable() - }).component(); - - this.podsLoading = this.modelView.modelBuilder.loadingComponent() - .withItem(this.podsTable) - .withProperties({ - loading: !this._postgresModel.serviceLastUpdated - }).component(); - - content.addItem(this.podsLoading, { CSSStyles: { 'margin-bottom': '30px' } }); - - // Conditions table - this.conditionsTable = this.modelView.modelBuilder.declarativeTable().withProperties({ - width: '100%', - columns: [ - { - displayName: loc.condition, - valueType: azdata.DeclarativeDataType.string, - width: '15%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: '', - valueType: azdata.DeclarativeDataType.component, - width: '1%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: loc.details, - valueType: azdata.DeclarativeDataType.string, - width: '64%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: cssStyles.tableRow - }, - { - displayName: loc.lastUpdated, - valueType: azdata.DeclarativeDataType.string, - width: '20%', - isReadOnly: true, - headerCssStyles: cssStyles.tableHeader, - rowCssStyles: { ...cssStyles.tableRow, 'white-space': 'nowrap' } - } - ], - data: this.getConditionsTable() - }).component(); - - this.conditionsLoading = this.modelView.modelBuilder.loadingComponent() - .withItem(this.conditionsTable) - .withProperties({ - loading: !this._postgresModel.serviceLastUpdated - }).component(); - - content.addItem(this.conditionsLoading); - this.initialized = true; - return root; - } - - protected get toolbarContainer(): azdata.ToolbarContainer { - const refreshButton = this.modelView.modelBuilder.button().withProperties({ - label: loc.refresh, - iconPath: IconPathHelper.refresh - }).component(); - - this.disposables.push( - refreshButton.onDidClick(async () => { - refreshButton.enabled = false; - try { - this.podsLoading!.loading = true; - this.conditionsLoading!.loading = true; - await this._postgresModel.refresh(); - } catch (error) { - vscode.window.showErrorMessage(loc.refreshFailed(error)); - } finally { - refreshButton.enabled = true; - } - })); - - return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([ - { component: refreshButton } - ]).component(); - } - - private getPodsLastUpdated(): string { - return this._postgresModel.serviceLastUpdated - ? loc.updated(fromNow(this._postgresModel.serviceLastUpdated!, true)) : ''; - } - - private getPodsTable(): (string | number)[][] { - return [ - [this._postgresModel.service?.status?.podsRunning ?? 0, loc.running], - [this._postgresModel.service?.status?.podsPending ?? 0, loc.pending], - [this._postgresModel.service?.status?.podsFailed ?? 0, loc.failed], - [this._postgresModel.service?.status?.podsUnknown ?? 0, loc.unknown] - ]; - } - - private getConditionsTable(): (string | azdata.ImageComponent)[][] { - /* TODO chgagnon - return this._postgresModel.service?.status?.conditions?.map(c => { - const healthy = c.type === 'Ready' ? c.status === 'True' : c.status === 'False'; - - const image = this.modelView.modelBuilder.image().withProperties({ - iconPath: healthy ? IconPathHelper.success : IconPathHelper.fail, - iconHeight: '20px', - iconWidth: '20px', - width: '20px', - height: '20px' - }).component(); - - return [ - c.type ?? '', - image, - c.message ?? '', - c.lastTransitionTime ? fromNow(c.lastTransitionTime!, true) : '' - ]; - }) ?? []; - */ - return []; - } - - private handleServiceUpdated() { - this.podsUpdated!.value = this.getPodsLastUpdated(); - this.podsTable!.data = this.getPodsTable(); - this.podsLoading!.loading = false; - - this.conditionsTable!.data = this.getConditionsTable(); - this.conditionsLoading!.loading = false; - } -} diff --git a/extensions/arc/src/ui/tree/controllerTreeNode.ts b/extensions/arc/src/ui/tree/controllerTreeNode.ts index f25f42c1f6..7a4c6cd75b 100644 --- a/extensions/arc/src/ui/tree/controllerTreeNode.ts +++ b/extensions/arc/src/ui/tree/controllerTreeNode.ts @@ -5,7 +5,7 @@ import { ResourceInfo, ResourceType } from 'arc'; import * as vscode from 'vscode'; -import { parseInstanceName, UserCancelledError } from '../../common/utils'; +import { UserCancelledError } from '../../common/utils'; import * as loc from '../../localizedConstants'; import { ControllerModel, Registration } from '../../models/controllerModel'; import { MiaaModel } from '../../models/miaaModel'; @@ -83,7 +83,7 @@ export class ControllerTreeNode extends TreeNode { } const resourceInfo: ResourceInfo = { - name: parseInstanceName(registration.instanceName), + name: registration.instanceName, resourceType: registration.instanceType ?? '' }; @@ -100,7 +100,7 @@ export class ControllerTreeNode extends TreeNode { switch (registration.instanceType) { case ResourceType.postgresInstances: - const postgresModel = new PostgresModel(resourceInfo, registration); + const postgresModel = new PostgresModel(this.model, resourceInfo, registration); node = new PostgresTreeNode(postgresModel, this.model, this._context); break; case ResourceType.sqlManagedInstances: diff --git a/extensions/arc/src/ui/tree/postgresTreeNode.ts b/extensions/arc/src/ui/tree/postgresTreeNode.ts index 42c8885c06..17bd523050 100644 --- a/extensions/arc/src/ui/tree/postgresTreeNode.ts +++ b/extensions/arc/src/ui/tree/postgresTreeNode.ts @@ -16,7 +16,7 @@ import { ResourceTreeNode } from './resourceTreeNode'; export class PostgresTreeNode extends ResourceTreeNode { constructor(private _model: PostgresModel, private _controllerModel: ControllerModel, private _context: vscode.ExtensionContext) { - super(_model.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, _model); + super(_model.info.name, vscode.TreeItemCollapsibleState.None, ResourceType.postgresInstances, _model); } public async openDashboard(): Promise { diff --git a/extensions/azdata/src/azdata.ts b/extensions/azdata/src/azdata.ts index 520447803d..0c17a60a9b 100644 --- a/extensions/azdata/src/azdata.ts +++ b/extensions/azdata/src/azdata.ts @@ -83,6 +83,34 @@ export class AzdataTool implements IAzdataTool { }, show: async (name: string) => { return this.executeCommand(['arc', 'postgres', 'server', 'show', '-n', name]); + }, + edit: async (args: { + 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]; + 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); } + if (args.engineSettings !== undefined) { argsArray.push('--engine-settings', args.engineSettings); } + if (args.extensions !== undefined) { argsArray.push('--extensions', args.extensions); } + if (args.memoryLimit !== undefined) { argsArray.push('--memory-limit', args.memoryLimit); } + if (args.memoryRequest !== undefined) { argsArray.push('--memory-request', args.memoryRequest); } + if (args.noWait) { argsArray.push('--no-wait'); } + 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); } } }, diff --git a/extensions/azdata/src/extension.ts b/extensions/azdata/src/extension.ts index f769b88812..a16d5a1f3e 100644 --- a/extensions/azdata/src/extension.ts +++ b/extensions/azdata/src/extension.ts @@ -92,6 +92,23 @@ export async function activate(context: vscode.ExtensionContext): Promise { await throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.postgres.server.show(name); + }, + edit: async (args: { + name: string, + adminPassword?: boolean, + coresLimit?: string, + coresRequest?: string, + engineSettings?: string, + extensions?: string, + memoryLimit?: string, + memoryRequest?: string, + noWait?: boolean, + port?: number, + replaceEngineSettings?: boolean, + workers?: number + }) => { + await throwIfNoAzdataOrEulaNotAccepted(); + return localAzdata!.arc.postgres.server.edit(args); } } }, diff --git a/extensions/azdata/src/typings/azdata-ext.d.ts b/extensions/azdata/src/typings/azdata-ext.d.ts index c6f8b45a2e..c86769fcc4 100644 --- a/extensions/azdata/src/typings/azdata-ext.d.ts +++ b/extensions/azdata/src/typings/azdata-ext.d.ts @@ -142,20 +142,6 @@ declare module 'azdata-ext' { } } - export interface PostgresServerShowResult { - apiVersion: string, // "arcdata.microsoft.com/v1alpha1" - kind: string, // "postgresql-12" - metadata: { - creationTimestamp: string, // "2020-08-19T20:25:11Z" - generation: number, // 1 - name: string, // "chgagnon-pg" - namespace: string, // "arc", - resourceVersion: string, // "214944", - selfLink: string, // "/apis/arcdata.microsoft.com/v1alpha1/namespaces/arc/postgresql-12s/chgagnon-pg", - uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0" - } - } - export interface PostgresServerShowResult { apiVersion: string, // "arcdata.microsoft.com/v1alpha1" kind: string, // "postgresql-12" @@ -169,25 +155,54 @@ declare module 'azdata-ext' { uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0" }, spec: { - backups: { - deltaMinutes: number, // 3, - fullMinutes: number, // 10, - tiers: [ - { - retention: { - maximums: string[], // [ "6", "512MB" ], - minimums: string[], // [ "3" ] + engine: { + extensions: { + name: string // "citus" + }[], + settings: { + default: { } // { "max_connections": "101", "work_mem": "4MB" } + } + }, + scale: { + shards: number // 1 + }, + scheduling: { + default: { + resources: { + requests: { + cpu: string, // "1.5" + memory: string // "256Mi" }, - storage: { - volumeSize: string, // "1Gi" + limits: { + cpu: string, // "1.5" + memory: string // "256Mi" } } - ] + } }, - status: { - readyPods: string, // "1/1", - state: string // "Ready" + service: { + type: string, // "NodePort" + port: number // 5432 + }, + storage: { + data: { + className: string, // "local-storage" + size: string // "5Gi" + }, + logs: { + className: string, // "local-storage" + size: string // "5Gi" + }, + backups: { + className: string, // "local-storage" + size: string // "5Gi" + } } + }, + status: { + externalEndpoint: string, // "10.130.12.136:26630" + readyPods: string, // "1/1", + state: string // "Ready" } } @@ -214,7 +229,21 @@ declare module 'azdata-ext' { postgres: { server: { list(): Promise>, - show(name: string): Promise> + show(name: string): Promise>, + edit(args: { + 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> } }, sql: {