From 08d380345357490c63d37e7e0f3c79105cda5e6d Mon Sep 17 00:00:00 2001 From: Shagun Sharma Tamta <31092713+shagunt@users.noreply.github.com> Date: Fri, 29 Oct 2021 17:46:05 -0700 Subject: [PATCH] Link Database Earliest and Latest Point in time with DryRun (#17506) --- extensions/arc/package.json | 2 +- extensions/arc/package.nls.json | 2 +- extensions/arc/src/common/utils.ts | 8 ++ extensions/arc/src/localizedConstants.ts | 20 ++-- extensions/arc/src/models/miaaModel.ts | 71 ++++++++++-- .../src/ui/dashboards/miaa/miaaBackupsPage.ts | 43 +++++--- .../src/ui/dialogs/configureRPOSqlDialog.ts | 2 +- .../arc/src/ui/dialogs/restoreSqlDialog.ts | 101 +++++++++++++----- extensions/azcli/src/api.ts | 1 + extensions/azcli/src/az.ts | 6 +- extensions/azcli/src/typings/az-ext.d.ts | 14 ++- 11 files changed, 207 insertions(+), 63 deletions(-) diff --git a/extensions/arc/package.json b/extensions/arc/package.json index 2a9ba17e4f..9c650dcf19 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -1189,7 +1189,7 @@ "description": "%arc.sql.retention.days.description%", "variableName": "AZDATA_NB_VAR_SQL_RETENTION_DAYS", "type": "number", - "min": 1, + "min": 0, "max": 35, "required": false } diff --git a/extensions/arc/package.nls.json b/extensions/arc/package.nls.json index 292e4c17fa..7c6b41f9c3 100644 --- a/extensions/arc/package.nls.json +++ b/extensions/arc/package.nls.json @@ -151,7 +151,7 @@ "arc.sql.license.type.label": "I already have a SQL Server License", "arc.sql.license.type.description": "Apply the Azure Hybrid Benefit if you already own a SQL Server License", "arc.sql.pitr.description": "Point in time restore", - "arc.sql.retention.days.label": "PITR retention (days)", + "arc.sql.retention.days.label": "Point in time retention (days)", "arc.sql.retention.days.description": "Specify how long you want to keep your point-in-time backups.", "arc.sql.dev.use.description": "Check the box to indicate this instance will be used for development or testing purposes only. This instance will not be billed.", "arc.postgres.storage-class.backups.description": "The storage class to be used for backup persistent volumes", diff --git a/extensions/arc/src/common/utils.ts b/extensions/arc/src/common/utils.ts index 8338313a6f..e06074d82e 100644 --- a/extensions/arc/src/common/utils.ts +++ b/extensions/arc/src/common/utils.ts @@ -363,3 +363,11 @@ export function debounce(delay: number): Function { }; }); } + +export function getTimeStamp(dateTime: string | undefined): number { + return dateTime ? (new Date(dateTime)).getTime() : 0; +} + +export function checkISOTimeString(dateTime: string): boolean { + return /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d.*Z/.test(dateTime); +} diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index b0836cfd69..72196e364f 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -60,18 +60,19 @@ export const type = localize('arc.type', "Type"); export const status = localize('arc.status', "Status"); export const database = localize('arc.database', "Database"); export const sourceDatabase = localize('arc.sourceDatabase', "Source database"); -export const earliestPitrRestorePoint = localize('arc.earliestPitrRestorePoint', "Earliest PITR restore point"); -export const latestpitrRestorePoint = localize('arc.latestpitrRestorePoint', "Latest PITR restore point"); -export const pitr = localize('arc.pitr', "Point-in-time restore (PITR)"); +export const earliestPitrRestorePoint = localize('arc.earliestPitrRestorePoint', "Earliest point in time"); +export const latestpitrRestorePoint = localize('arc.latestpitrRestorePoint', "Latest point in time"); +export const pitr = localize('arc.pitr', "Point in time restore"); export const projectDetails = localize('arc.projectDetails', "Project Details"); export const projectDetailsText = localize('arc.projectDetailsText', "Select the subscription to manage deployed resources. Use resource groups like folders to organize and manage all your resources."); export const sourceDetails = localize('arc.sourceDetails', "Source Details"); -export const sourceDetailsText = localize('arc.sourceDetailsText', "Select a backup source and provide details. Additional settings will be defaulted where possible based on the selected backup."); -export const databaseDetails = localize('arc.databaseDetails', "Database Details"); -export const databaseDetailsText = localize('arc.databaseDetailsText', "Enter the required settings for this database, including a name and a target managed instance. By default, the source instance is selected."); +export const sourceDetailsText = localize('arc.sourceDetailsText', "Select a backup source and provide details. Additional settings will be defaulted where possible based on the selected database."); +export const databaseDetails = localize('arc.databaseDetails', "Destination Details"); +export const restorePointDetails = localize('arc.restorePointDetails', "Restore Point Details"); +export const databaseDetailsText = localize('arc.databaseDetailsText', "Enter the required settings for target database name and SQL managed instance. By default, the source managed instance is selected."); export const restore = localize('arc.restore', "Restore"); export const instance = localize('arc.instance', "Instance"); -export const restorePoint = localize('arc.restorePoint', "Restore point (UTC)"); +export const restorePoint = localize('arc.restorePoint', "Restore point (UTC), in a time format: 'YYYY-MM-DDTHH:MM:SSZ"); export const restoreDatabase = localize('arc.restoreDatabase', "Restore Database"); export const miaaAdmin = localize('arc.miaaAdmin', "Managed instance admin"); export const extensionName = localize('arc.extensionName', "Extension name"); @@ -79,6 +80,7 @@ export const extensionsDescription = localize('arc.extensionsDescription', "Post export const extensionsFunction = localize('arc.extensionsFunction', "Some extensions must be loaded into PostgreSQL at startup time before they can be used. These preloaded extensions can be viewed and edited below."); export function extensionsAddFunction(extensions: string): string { return localize('arc.extensionsAddFunction', "Some extensions must be loaded into PostgreSQL at startup time before they can be used. To edit, type in comma separated list of valid extensions: ({0}).", extensions); } export function extensionsAddErrorrMessage(extensions: string): string { return localize('arc.extensionsAddErrorrMessage', "Value should be either of the following: ({0}).", extensions); } +export function restorePointErrorMessage(earliestPoint: string, latestPoint: string) { return localize('arc.restorePointErrorrMessage', "Provide time in correct format and within range: {0} to {1}", earliestPoint, latestPoint); } export const extensionsLearnMore = localize('arc.extensionsLearnMore', "Learn more about PostgreSQL extensions."); export const extensionsTableLoading = localize('arc.extensionsTableLoading', "Table of preloaded extensions are loading."); export const extensionsTableLabel = localize('arc.extensionsTableLabel', "Table of preloaded extensions."); @@ -192,7 +194,8 @@ export const postgresComputeAndStorageDescriptionPartOne = localize('arc.postgre export const miaaComputeAndStorageDescriptionPartOne = localize('arc.miaaComputeAndStorageDescriptionPartOne', "You can scale your Azure SQL managed instance - Azure Arc by"); export const miaaBackupsDatabasesDescription = localize('arc.miaaBackupsDatabasesDescription', "Databases with available backups are displayed below. Restore databases to this instance or any other instance within the same custom location."); export const pitrInfo = localize('arc.pitrInfo', "Specify how long you want to keep your point-in-time backups. Customize this for backup availability."); -export const restoreInfo = localize('arc.restoreInfo', "Restore a database to an Azure Arc enabled SQL Managed Instance of your choice."); +export const restoreInfo = localize('arc.restoreInfo', "Restore a database to an Azure Arc enabled SQL Managed Instance."); +export const restorePointText = localize('arc.restorePointText', "Enter a restore point in the specified time format within given range of earliest and latest restore time."); export const postgresComputeAndStorageDescriptionPartTwo = localize('arc.postgres.computeAndStorageDescriptionPartTwo', "PostgreSQL Hyperscale server group by"); export const computeAndStorageDescriptionPartThree = localize('arc.computeAndStorageDescriptionPartThree', "without downtime and by"); export const computeAndStorageDescriptionPartFour = localize('arc.computeAndStorageDescriptionPartFour', "Before doing so, you need to ensure"); @@ -289,6 +292,7 @@ export const couldNotFindControllerRegistration = localize('arc.couldNotFindCont export const dropMultipleExtensions = localize('arc.dropMultipleExtensions', "Currently dropping another extension, try again once that is completed."); export function updateExtensionsFailed(error: any): string { return localize('arc.updateExtensionsFailed', "Editing extensions failed. {0}", getErrorMessage(error)); } export function refreshFailed(error: any): string { return localize('arc.refreshFailed', "Refresh failed. {0}", getErrorMessage(error)); } +export function restoreTimeWindowUpdateFailed(error: any): string { return localize('arc.restoreTimeWindowUpdateFailed', "Point in time restore time window update failed. {0}", getErrorMessage(error)); } export function resetFailed(error: any): string { return localize('arc.resetFailed', "Reset failed. {0}", getErrorMessage(error)); } export function openDashboardFailed(error: any): string { return localize('arc.openDashboardFailed', "Error opening dashboard. {0}", getErrorMessage(error)); } export function instanceDeletionFailed(name: string, error: any): string { return localize('arc.instanceDeletionFailed', "Failed to delete instance {0}. {1}", name, getErrorMessage(error)); } diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts index 60a44c3b95..d15abd9702 100644 --- a/extensions/arc/src/models/miaaModel.ts +++ b/extensions/arc/src/models/miaaModel.ts @@ -9,14 +9,14 @@ import * as azExt from 'az-ext'; import * as vscode from 'vscode'; import { UserCancelledError } from '../common/api'; import { Deferred } from '../common/promise'; -import { parseIpAndPort } from '../common/utils'; +import { getTimeStamp, parseIpAndPort } from '../common/utils'; import * as loc from '../localizedConstants'; import { ConnectToMiaaSqlDialog } from '../ui/dialogs/connectMiaaDialog'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; import { ControllerModel, Registration } from './controllerModel'; import { ResourceModel } from './resourceModel'; -export type DatabaseModel = { name: string, status: string, lastBackup: string }; +export type DatabaseModel = { name: string, status: string, earliestBackup: string, lastBackup: string }; export type RPModel = { recoveryPointObjective: string, retentionDays: string }; export type PITRModel = { instanceName: string, @@ -27,7 +27,9 @@ export type PITRModel = { restorePoint: string, earliestPitr: string, latestPitr: string, + destDbName: string }; + export const systemDbs = ['master', 'msdb', 'tempdb', 'model']; export class MiaaModel extends ResourceModel { @@ -44,12 +46,20 @@ export class MiaaModel extends ResourceModel { recoveryPointObjective: '', retentionDays: '' }; + private _databaseTimeWindow: Map; private _refreshPromise: Deferred | undefined = undefined; - + private _pitrArgs = { + destName: '', + managedInstance: '', + time: '', + noWait: true, + dryRun: false + }; constructor(_controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) { super(_controllerModel, _miaaInfo, registration); this._azApi = vscode.extensions.getExtension(azExt.extension.name)?.exports; + this._databaseTimeWindow = new Map(); } /** @@ -92,6 +102,7 @@ export class MiaaModel extends ResourceModel { this.configLastUpdated = new Date(); this.rpSettings.retentionDays = this._config?.spec?.backup?.retentionPeriodInDays?.toString() ?? ''; this._onConfigUpdated.fire(this._config); + this._onDatabasesUpdated.fire(this._databases); } catch (err) { // If an error occurs show a message so the user knows something failed but still // fire the event so callers can know to update (e.g. so dashboards don't show the @@ -138,6 +149,12 @@ export class MiaaModel extends ResourceModel { throw error; } } + + /** + * Gets the list of databases and adds backup earliest and latest point in time + * information, this could be used as an upper and lower time limit for restoring + * backup. + */ public async getDatabases(promptForConnection: boolean = true): Promise { if (!this._connectionProfile) { await this.getConnectionProfile(promptForConnection); @@ -158,10 +175,21 @@ export class MiaaModel extends ResourceModel { if (!databases) { throw new Error('Could not fetch databases'); } - if (databases.length > 0 && typeof (databases[0]) === 'object') { - this._databases = (databases).map(db => { return { name: db.options['name'], status: db.options['state'], lastBackup: db.options['lastBackup'] }; }); - } else { - this._databases = (databases).map(db => { return { name: db, status: '-', lastBackup: '' }; }); + else { + if (databases.length > 0 && typeof (databases[0]) === 'object') { + for (let i in databases) { + const di: azdata.DatabaseInfo = databases[i]; + const name = di.options['name']; + await this.executeDryRun(di.options['name']); + const dm: DatabaseModel = { + name: name, status: di.options['state'], earliestBackup: this._databaseTimeWindow.get(name)?.[0] ?? '', + lastBackup: this._databaseTimeWindow.get(name)?.[1] ?? '' + }; + this._databases[i] = dm; + } + } else { + this._databases = (databases).map(db => { return { name: db, status: '-', earliestBackup: '', lastBackup: '' }; }); + } } this.databasesLastUpdated = new Date(); this._onDatabasesUpdated.fire(this._databases); @@ -206,4 +234,33 @@ export class MiaaModel extends ResourceModel { await this._treeDataProvider.saveControllers(); } + protected async executeDryRun(dbName: string): Promise { + // Allow next dry Run to be executed only after 5(300000 ms ) minutes from current time as the log backups are + // generated only at 5 minutes interval + if ((systemDbs.indexOf(dbName) === -1) && (Date.now() - getTimeStamp(this._databaseTimeWindow.get(dbName)?.[1]) >= 300000)) { + try { + //Execute dryRun for earliestTime and save latest time as well so there is one call to az cli + this._pitrArgs.destName = dbName + '-' + Date.now().toString(); + this._pitrArgs.managedInstance = this.info.name; + this._pitrArgs.time = new Date().toISOString(); + this._pitrArgs.noWait = false; + this._pitrArgs.dryRun = true; + const result = await this._azApi.az.sql.midbarc.restore( + dbName, this._pitrArgs, this.controllerModel.info.namespace, this.controllerModel.azAdditionalEnvVars); + const restoreResult = result.stdout; + if (restoreResult) { + const earliestTime = restoreResult['earliestRestoreTime']; + const latestTime = restoreResult['latestRestoreTime']; + console.log(loc.earliestPitrRestorePoint + '-' + dbName + ':' + earliestTime); + console.log(loc.latestpitrRestorePoint + '-' + dbName + ':' + latestTime); + this._databaseTimeWindow.set(dbName, [earliestTime, latestTime]); + } + } + catch (err) { + console.log(loc.pitr + ' ' + loc.failed + ':' + err); + this._databaseTimeWindow.set(dbName, ['', '']); + } + } + + } } diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaBackupsPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaBackupsPage.ts index 4a63790b90..f1c7968ed5 100644 --- a/extensions/arc/src/ui/dashboards/miaa/miaaBackupsPage.ts +++ b/extensions/arc/src/ui/dashboards/miaa/miaaBackupsPage.ts @@ -20,7 +20,6 @@ export class MiaaBackupsPage extends DashboardPage { this._azApi = vscode.extensions.getExtension(azExt.extension.name)?.exports; this.disposables.push( this._miaaModel.onDatabasesUpdated(() => this.eventuallyRunOnInitialized(() => this.handleDatabasesUpdated())), - this._miaaModel.onConfigUpdated(() => this.eventuallyRunOnInitialized(() => this.handleDatabasesUpdated())) ); } private _databasesContainer!: azdata.DivContainer; @@ -32,16 +31,17 @@ export class MiaaBackupsPage extends DashboardPage { private _databasesMessage!: azdata.TextComponent; private readonly _azApi: azExt.IExtension; - public saveArgs: RPModel = { + private _saveArgs: RPModel = { recoveryPointObjective: '', retentionDays: '' }; - public pitrArgs = { + private _pitrArgs = { destName: '', managedInstance: '', time: '', - noWait: true + noWait: true, + dryRun: false }; public get title(): string { @@ -66,7 +66,7 @@ export class MiaaBackupsPage extends DashboardPage { .component(); const content = this.modelView.modelBuilder.divContainer().component(); this._databasesContainer = this.modelView.modelBuilder.divContainer().component(); - root.addItem(content, { CSSStyles: { 'margin': '20px' } }); + root.addItem(content, { CSSStyles: { 'margin': '5px' } }); const databaseTitle = this.modelView.modelBuilder.text().withProps({ value: loc.databases, CSSStyles: { ...cssStyles.title }, @@ -130,11 +130,19 @@ export class MiaaBackupsPage extends DashboardPage { headerCssStyles: cssStyles.tableHeader, rowCssStyles: cssStyles.tableRow }, + { + displayName: loc.earliestPitrRestorePoint, + valueType: azdata.DeclarativeDataType.string, + isReadOnly: true, + width: '30%', + headerCssStyles: cssStyles.tableHeader, + rowCssStyles: cssStyles.tableRow + }, { displayName: loc.latestpitrRestorePoint, valueType: azdata.DeclarativeDataType.string, isReadOnly: true, - width: '50%', + width: '30%', headerCssStyles: cssStyles.tableHeader, rowCssStyles: cssStyles.tableRow }, @@ -142,7 +150,7 @@ export class MiaaBackupsPage extends DashboardPage { displayName: loc.restore, valueType: azdata.DeclarativeDataType.component, isReadOnly: true, - width: '20%', + width: '10%', headerCssStyles: cssStyles.tableHeader, rowCssStyles: cssStyles.tableRow, } @@ -198,13 +206,13 @@ export class MiaaBackupsPage extends DashboardPage { this._configureRetentionPolicyButton.onDidClick(async () => { const retentionPolicySqlDialog = new ConfigureRPOSqlDialog(this._miaaModel); this.refreshRD(); - retentionPolicySqlDialog.showDialog(loc.configureRP, this.saveArgs.retentionDays); + retentionPolicySqlDialog.showDialog(loc.configureRP, this._saveArgs.retentionDays); let rpArg = await retentionPolicySqlDialog.waitForClose(); if (rpArg) { try { this._configureRetentionPolicyButton.enabled = false; - this.saveArgs.retentionDays = rpArg.retentionDays; + this._saveArgs.retentionDays = rpArg.retentionDays; await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, @@ -213,7 +221,7 @@ export class MiaaBackupsPage extends DashboardPage { }, async (_progress, _token): Promise => { await this._azApi.az.sql.miarc.edit( - this._miaaModel.info.name, this.saveArgs, this._miaaModel.controllerModel.info.namespace, this._miaaModel.controllerModel.azAdditionalEnvVars); + this._miaaModel.info.name, this._saveArgs, this._miaaModel.controllerModel.info.namespace, this._miaaModel.controllerModel.azAdditionalEnvVars); try { await this._miaaModel.refresh(); @@ -244,13 +252,16 @@ export class MiaaBackupsPage extends DashboardPage { // If we were able to get the databases it means we have a good connection so update the username too let databaseDisplay = this._miaaModel.databases.map(d => [ d.name, + d.earliestBackup, d.lastBackup, this.createRestoreButton(d)]); + let databasesValues = databaseDisplay.map(d => { return d.map((value): azdata.DeclarativeTableCellValue => { return { value: value }; }); }); + this._databasesTable.setDataValues(databasesValues); this._databasesTableLoading.loading = false; @@ -271,7 +282,7 @@ export class MiaaBackupsPage extends DashboardPage { } private refreshRD(): void { - this.saveArgs.retentionDays = this._miaaModel.config?.spec?.backup?.retentionPeriodInDays.toString() ?? ''; + this._saveArgs.retentionDays = this._miaaModel.config?.spec?.backup?.retentionPeriodInDays.toString() ?? ''; } // Create restore button for every database entry in the database table @@ -292,9 +303,9 @@ export class MiaaBackupsPage extends DashboardPage { if (args) { try { restoreButton.enabled = false; - this.pitrArgs.destName = args.dbName; - this.pitrArgs.managedInstance = args.instanceName; - this.pitrArgs.time = `"${args.restorePoint}"`; + this._pitrArgs.destName = args.destDbName; + this._pitrArgs.managedInstance = args.instanceName; + this._pitrArgs.time = `"${args.restorePoint}"`; await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, @@ -303,7 +314,7 @@ export class MiaaBackupsPage extends DashboardPage { }, async (_progress, _token): Promise => { await this._azApi.az.sql.midbarc.restore( - db.name, this.pitrArgs, this._miaaModel.controllerModel.info.namespace, this._miaaModel.controllerModel.azAdditionalEnvVars); + db.name, this._pitrArgs, this._miaaModel.controllerModel.info.namespace, this._miaaModel.controllerModel.azAdditionalEnvVars); try { await this._miaaModel.refresh(); } catch (error) { @@ -320,4 +331,6 @@ export class MiaaBackupsPage extends DashboardPage { })); return restoreButton; } + } + diff --git a/extensions/arc/src/ui/dialogs/configureRPOSqlDialog.ts b/extensions/arc/src/ui/dialogs/configureRPOSqlDialog.ts index 1e9df7682e..232e46d1e2 100644 --- a/extensions/arc/src/ui/dialogs/configureRPOSqlDialog.ts +++ b/extensions/arc/src/ui/dialogs/configureRPOSqlDialog.ts @@ -32,7 +32,7 @@ export class ConfigureRPOSqlDialog extends InitializingComponent { this.retentionDaysInputBox = this.modelBuilder.inputBox() .withProps({ readOnly: false, - min: 1, + min: 0, max: 35, inputType: 'number', ariaLabel: loc.retentionDays, diff --git a/extensions/arc/src/ui/dialogs/restoreSqlDialog.ts b/extensions/arc/src/ui/dialogs/restoreSqlDialog.ts index 8211ace880..04eaf8e870 100644 --- a/extensions/arc/src/ui/dialogs/restoreSqlDialog.ts +++ b/extensions/arc/src/ui/dialogs/restoreSqlDialog.ts @@ -5,6 +5,7 @@ import * as azdata from 'azdata'; import { Deferred } from '../../common/promise'; +import { getTimeStamp, checkISOTimeString } from '../../common/utils'; import * as loc from '../../localizedConstants'; import * as vscode from 'vscode'; import { cssStyles } from '../../constants'; @@ -24,6 +25,7 @@ export class RestoreSqlDialog extends InitializingComponent { restorePoint: '-', earliestPitr: '-', latestPitr: '-', + destDbName: '-', }; private earliestRestorePointInputBox!: azdata.InputBoxComponent; @@ -36,10 +38,11 @@ export class RestoreSqlDialog extends InitializingComponent { private instanceInputBox!: azdata.InputBoxComponent; protected _completionPromise = new Deferred(); private _azurecoreApi: azurecore.IExtension; + protected disposables: vscode.Disposable[] = []; constructor(protected _miaaModel: MiaaModel, protected _controllerModel: ControllerModel, protected _database: DatabaseModel) { super(); this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports; - + this.refreshPitrSettings(); } public showDialog(dialogTitle: string): azdata.window.Dialog { @@ -50,14 +53,15 @@ export class RestoreSqlDialog extends InitializingComponent { this.refreshPitrSettings(); const pitrTitle = this.modelBuilder.text().withProps({ value: loc.pitr, - CSSStyles: { ...cssStyles.title } + CSSStyles: { ...cssStyles.title, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' }, }).component(); const projectDetailsTitle = this.modelBuilder.text().withProps({ value: loc.projectDetails, - CSSStyles: { ...cssStyles.title } + CSSStyles: { ...cssStyles.title, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' } }).component(); const projectDetailsTextLabel = this.modelBuilder.text().withProps({ value: loc.projectDetailsText, + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' } }).component(); this.subscriptionInputBox = this.modelBuilder.inputBox() .withProps({ @@ -74,10 +78,11 @@ export class RestoreSqlDialog extends InitializingComponent { const sourceDetailsTitle = this.modelBuilder.text().withProps({ value: loc.sourceDetails, - CSSStyles: { ...cssStyles.title } + CSSStyles: { ...cssStyles.title, 'margin-block-end': '0px', 'max-width': 'auto' } }).component(); const sourceDetailsTextLabel = this.modelBuilder.text().withProps({ value: loc.sourceDetailsText, + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' } }).component(); this.sourceDbInputBox = this.modelBuilder.inputBox() .withProps({ @@ -85,12 +90,15 @@ export class RestoreSqlDialog extends InitializingComponent { ariaLabel: loc.sourceDatabase, value: this._database.name }).component(); - + const restoreDetailsTextLabel = this.modelBuilder.text().withProps({ + value: loc.restorePointText, + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' } + }).component(); this.earliestRestorePointInputBox = this.modelBuilder.inputBox() .withProps({ enabled: false, ariaLabel: loc.earliestPitrRestorePoint, - value: '' + value: this._database.earliestBackup }).component(); this.latestRestorePointInputBox = this.modelBuilder.inputBox() @@ -104,14 +112,39 @@ export class RestoreSqlDialog extends InitializingComponent { .withProps({ readOnly: false, ariaLabel: loc.restorePoint, - value: '' + value: '', + validationErrorMessage: loc.restorePointErrorMessage(this.earliestRestorePointInputBox.value ?? loc.earliestPitrRestorePoint, this.latestRestorePointInputBox.value ?? loc.latestpitrRestorePoint), + }).withValidation(async () => { + try { + if (!checkISOTimeString(this.restorePointInputBox.value ?? '')) { return false; } + if (this.earliestRestorePointInputBox.value) { + if ((getTimeStamp(this.restorePointInputBox.value) >= getTimeStamp(this.earliestRestorePointInputBox.value) + && getTimeStamp(this.restorePointInputBox.value) <= getTimeStamp(this.latestRestorePointInputBox.value))) { + this.pitrSettings.restorePoint = this.restorePointInputBox.value ?? ''; + return true; + } + else { + return false; + } + } + } + catch (err) { + throw err; + return false; + } + return true; }).component(); - const databaseDetailsTitle = this.modelBuilder.text().withProps({ + const pitrDetailsTitle = this.modelBuilder.text().withProps({ + value: loc.restorePointDetails, + CSSStyles: { ...cssStyles.title, 'margin-block-end': '0px', 'max-width': 'auto' } + }).component(); + const destinationDetailsTitle = this.modelBuilder.text().withProps({ value: loc.databaseDetails, - CSSStyles: { ...cssStyles.title } + CSSStyles: { ...cssStyles.title, 'margin-block-end': '0px', 'max-width': 'auto' } }).component(); const databaseDetailsTextLabel = this.modelBuilder.text().withProps({ value: loc.databaseDetailsText, + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' } }).component(); this.databaseNameInputBox = this.modelBuilder.inputBox() .withProps({ @@ -119,7 +152,10 @@ export class RestoreSqlDialog extends InitializingComponent { ariaLabel: loc.databaseName, value: '' }).component(); - + this.disposables.push( + this.databaseNameInputBox.onTextChanged(() => { + this.pitrSettings.destDbName = this.databaseNameInputBox.value ?? ''; + })); this.instanceInputBox = this.modelBuilder.inputBox() .withProps({ enabled: false, @@ -128,7 +164,7 @@ export class RestoreSqlDialog extends InitializingComponent { }).component(); const info = this.modelBuilder.text().withProps({ value: loc.restoreInfo, - CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' } + CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'max-width': 'auto' } }).component(); const link = this.modelBuilder.hyperlink().withProps({ @@ -178,22 +214,7 @@ export class RestoreSqlDialog extends InitializingComponent { }, { - component: this.earliestRestorePointInputBox, - title: loc.earliestPitrRestorePoint, - - }, - { - component: this.latestRestorePointInputBox, - title: loc.latestpitrRestorePoint, - - }, - { - component: this.restorePointInputBox, - title: loc.restorePoint, - required: true - }, - { - component: databaseDetailsTitle, + component: destinationDetailsTitle, }, { component: databaseDetailsTextLabel, @@ -208,6 +229,27 @@ export class RestoreSqlDialog extends InitializingComponent { title: loc.instance, }, + { + component: pitrDetailsTitle + }, + { + component: restoreDetailsTextLabel, + }, + { + component: this.earliestRestorePointInputBox, + title: loc.earliestPitrRestorePoint, + + }, + { + component: this.latestRestorePointInputBox, + title: loc.latestpitrRestorePoint, + + }, + { + component: this.restorePointInputBox, + title: loc.restorePoint, + required: true + }, ], title: '' }]).withLayout({ width: '100%' }).component(); @@ -269,4 +311,9 @@ export class RestoreSqlDialog extends InitializingComponent { this.pitrSettings.restorePoint = this._database.lastBackup; this.pitrSettings.earliestPitr = ''; } + public updatePitrTimeWindow(earliestPitr: string, latestPitr: string): void { + this.earliestRestorePointInputBox.value = earliestPitr; + this.latestRestorePointInputBox.value = latestPitr; + } + } diff --git a/extensions/azcli/src/api.ts b/extensions/azcli/src/api.ts index 4fcae48652..c05ac06427 100644 --- a/extensions/azcli/src/api.ts +++ b/extensions/azcli/src/api.ts @@ -140,6 +140,7 @@ export function getAzApi(localAzDiscovered: Promise, azTool managedInstance?: string, time?: string, noWait?: boolean, + dryRun?: boolean }, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars) => { await localAzDiscovered; diff --git a/extensions/azcli/src/az.ts b/extensions/azcli/src/az.ts index 0a45c2ef6e..4f36314d14 100644 --- a/extensions/azcli/src/az.ts +++ b/extensions/azcli/src/az.ts @@ -185,16 +185,18 @@ export class AzTool implements azExt.IAzApi { managedInstance?: string, time?: string, noWait?: boolean, + dryRun?: boolean }, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars - ): Promise> => { + ): Promise> => { const argsArray = ['sql', 'midb-arc', 'restore', '--name', name, '--k8s-namespace', namespace, '--use-k8s']; if (args.destName) { argsArray.push('--dest-name', args.destName); } if (args.managedInstance) { argsArray.push('--managed-instance', args.managedInstance); } if (args.time) { argsArray.push('--time', args.time); } if (args.noWait) { argsArray.push('--no-wait'); } - return this.executeCommand(argsArray, additionalEnvVars); + if (args.dryRun) { argsArray.push('--dry-run'); } + return this.executeCommand(argsArray, additionalEnvVars); } } }; diff --git a/extensions/azcli/src/typings/az-ext.d.ts b/extensions/azcli/src/typings/az-ext.d.ts index a4ff276c57..4d3d328809 100644 --- a/extensions/azcli/src/typings/az-ext.d.ts +++ b/extensions/azcli/src/typings/az-ext.d.ts @@ -173,6 +173,17 @@ declare module 'az-ext' { } } + export interface SqlMiDbRestoreResult { + destDatabase: string, //testDbToRestore + earliestRestoreTime: string, // "2020-08-19T20:25:11Z" + latestRestoreTime: string, //"2020-08-19T20:25:11Z" + message: string, //Dry run for restore operation succeeded. + observedGeneration: number, //1 + restorePoint: string, // "2020-08-19T20:25:11Z" + sourceDatabase: string, //testDb + state: string //Completed + } + export interface PostgresServerShowResult { apiVersion: string, // "arcdata.microsoft.com/v1alpha1" kind: string, // "postgresql" @@ -336,10 +347,11 @@ declare module 'az-ext' { managedInstance?: string, //sqlmi1 time?: string, //2021-10-12T11:16:30.000Z noWait?: boolean, //true + dryRun?: boolean, //true }, namespace?: string, additionalEnvVars?: AdditionalEnvVars - ): Promise> + ): Promise> } }, getPath(): Promise,