/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { PGResourceInfo } from 'arc'; import * as azdata from 'azdata'; import * as azExt from 'az-ext'; import * as vscode from 'vscode'; import * as loc from '../localizedConstants'; import { ConnectToPGSqlDialog } from '../ui/dialogs/connectPGDialog'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; import { ControllerModel, Registration } from './controllerModel'; import { parseIpAndPort } from '../common/utils'; import { UserCancelledError } from '../common/api'; import { ResourceModel } from './resourceModel'; import { Deferred } from '../common/promise'; export type EngineSettingsModel = { parameterName: string | undefined, value: string | undefined, description: string | undefined, min: string | undefined, max: string | undefined, options: string | undefined, type: string | undefined }; export class PostgresModel extends ResourceModel { private _config?: azExt.PostgresServerShowResult; public workerNodesEngineSettings: EngineSettingsModel[] = []; public coordinatorNodeEngineSettings: EngineSettingsModel[] = []; private readonly _azApi: azExt.IExtension; private readonly _onConfigUpdated = new vscode.EventEmitter(); public onConfigUpdated = this._onConfigUpdated.event; public configLastUpdated?: Date; public engineSettingsLastUpdated?: Date; private _refreshPromise?: Deferred; private _engineSettingsPromise?: Deferred; constructor(_controllerModel: ControllerModel, private _pgInfo: PGResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) { super(_controllerModel, _pgInfo, registration); this._azApi = vscode.extensions.getExtension(azExt.extension.name)?.exports; } /** Returns the configuration of Postgres */ public get config(): azExt.PostgresServerShowResult | undefined { return this._config; } /** Returns the IP address and port of Postgres */ public get endpoint(): { ip: string, port: string } | undefined { return this._config?.status.primaryEndpoint ? parseIpAndPort(this._config.status.primaryEndpoint) : 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 dataStorage = this._config.spec.storage?.data?.volumes?.[0]?.size; const logStorage = this._config.spec.storage?.logs?.volumes?.[0]?.size; const backupsStorage = this._config.spec.storage?.backups?.volumes?.[0]?.size; let configuration: string[] = []; // 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}`); } let storage: string[] = []; if (dataStorage) { storage.push(loc.dataStorage(dataStorage)); } if (logStorage) { storage.push(loc.logStorage(logStorage)); } if (backupsStorage) { storage.push(loc.backupsStorage(backupsStorage)); } if (dataStorage || logStorage || backupsStorage) { storage.push(`${loc.storagePerNode}`); configuration.push(storage.join(' ')); } return configuration.join(', '); } /** Refreshes the model */ public async refresh() { // Only allow one refresh to be happening at a time if (this._refreshPromise) { return this._refreshPromise.promise; } this._refreshPromise = new Deferred(); try { this._config = (await this._azApi.az.postgres.serverarc.show(this.info.name, this.controllerModel.info.namespace, this.controllerModel.azAdditionalEnvVars)).stdout; this.configLastUpdated = new Date(); this._onConfigUpdated.fire(this._config); this._refreshPromise.resolve(); } catch (err) { this._refreshPromise.reject(err); throw err; } finally { this._refreshPromise = undefined; } } public async getEngineSettings(): Promise { // Only allow to get engine setting once at a time if (this._engineSettingsPromise) { return this._engineSettingsPromise.promise; } this._engineSettingsPromise = new Deferred(); try { if (!this._connectionProfile) { await this.getConnectionProfile(); } // We haven't connected yet so do so now and then store the ID for the active connection if (!this._activeConnectionId) { const result = await azdata.connection.connect(this._connectionProfile!, false, false); if (!result.connected) { throw new Error(result.errorMessage); } this._activeConnectionId = result.connectionId!; } // TODO Need to make separate calls for worker nodes and coordinator node const provider = azdata.dataprotocol.getProvider(this._connectionProfile!.providerName, azdata.DataProviderType.QueryProvider); const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId); const skippedEngineSettings: String[] = [ 'archive_command', 'archive_timeout', 'log_directory', 'log_file_mode', 'log_filename', 'restore_command', 'shared_preload_libraries', 'synchronous_commit', 'ssl', 'unix_socket_permissions', 'wal_level' ]; await this.createCoordinatorEngineSettings(provider, ownerUri, skippedEngineSettings); this.engineSettingsLastUpdated = new Date(); this._engineSettingsPromise.resolve(); } catch (err) { this._engineSettingsPromise.reject(err); throw err; } finally { this._engineSettingsPromise = undefined; } } private async createCoordinatorEngineSettings(provider: azdata.QueryProvider, ownerUri: string, skip: String[]): Promise { const engineSettingsCoordinator = await provider.runQueryAndReturn(ownerUri, 'select name, setting, short_desc,min_val, max_val, enumvals, vartype from pg_settings'); this.coordinatorNodeEngineSettings = []; engineSettingsCoordinator.rows.forEach(row => { let rowValues = row.map(c => c.displayValue); let name = rowValues.shift(); if (!skip.includes(name!)) { let result: EngineSettingsModel = { parameterName: name, value: rowValues.shift(), description: rowValues.shift(), min: rowValues.shift(), max: rowValues.shift(), options: rowValues.shift(), type: rowValues.shift() }; this.coordinatorNodeEngineSettings.push(result); } }); } protected createConnectionProfile(): azdata.IConnectionProfile { const ipAndPort = parseIpAndPort(this.config?.status.primaryEndpoint || ''); return { serverName: `${ipAndPort.ip},${ipAndPort.port}`, databaseName: '', authenticationType: azdata.connection.AuthenticationType.SqlLogin, providerName: loc.postgresProviderName, connectionName: '', userName: this._pgInfo.userName || '', password: '', savePassword: true, groupFullName: undefined, saveProfile: true, id: '', groupId: undefined, options: { host: `${ipAndPort.ip}`, port: `${ipAndPort.port}`, } }; } protected async promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise { const connectToSqlDialog = new ConnectToPGSqlDialog(this.controllerModel, this); connectToSqlDialog.showDialog(loc.connectToPGSql(this.info.name), connectionProfile); let profileFromDialog = await connectToSqlDialog.waitForClose(); if (profileFromDialog) { this.updateConnectionProfile(profileFromDialog); } else { throw new UserCancelledError(); } } protected async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise { this._connectionProfile = connectionProfile; this.info.connectionId = connectionProfile.id; this._pgInfo.userName = connectionProfile.userName; await this._treeDataProvider.saveControllers(); } }