From f126c998d2e10edaca2a17ec363fd6090d567dc4 Mon Sep 17 00:00:00 2001
From: Shagun Sharma Tamta <31092713+shagunt@users.noreply.github.com>
Date: Thu, 14 Oct 2021 12:29:53 -0700
Subject: [PATCH] Add Backup tab under SQL Miaa dashboard, 'Configure Retention
Policy' settings dialog, listing databases with latest PITR timetamp, Pitr
dialog to restore (#17269)
* backup page
* config rpo first
* rpo az cli
* working 1
* working 2
* working -3
* working -3
* working 4
* working with button component
* remove Date usage, use string instead
* cleanup
* cleanup 2
* Update localizedConstants.ts
rectify the wording until, figure out a way to fetch earliest backup
* pitr dialog, remove rpo
* pr feedback
* pr feedback
* pr feedback
* pr feedback
* feedback
* remove iso time conversion and show time as-is
---
extensions/arc/images/pitr.svg | 23 ++
extensions/arc/src/constants.ts | 5 +
extensions/arc/src/localizedConstants.ts | 25 +-
extensions/arc/src/models/miaaModel.ts | 38 ++-
.../src/ui/dashboards/miaa/miaaBackupsPage.ts | 323 ++++++++++++++++++
.../src/ui/dashboards/miaa/miaaDashboard.ts | 5 +-
.../miaa/miaaDashboardOverviewPage.ts | 16 +-
.../src/ui/dialogs/configureRPOSqlDialog.ts | 101 ++++++
.../arc/src/ui/dialogs/restoreSqlDialog.ts | 272 +++++++++++++++
extensions/azcli/src/api.ts | 14 +
extensions/azcli/src/az.ts | 23 ++
extensions/azcli/src/typings/az-ext.d.ts | 27 +-
12 files changed, 845 insertions(+), 27 deletions(-)
create mode 100644 extensions/arc/images/pitr.svg
create mode 100644 extensions/arc/src/ui/dashboards/miaa/miaaBackupsPage.ts
create mode 100644 extensions/arc/src/ui/dialogs/configureRPOSqlDialog.ts
create mode 100644 extensions/arc/src/ui/dialogs/restoreSqlDialog.ts
diff --git a/extensions/arc/images/pitr.svg b/extensions/arc/images/pitr.svg
new file mode 100644
index 0000000000..0b3cc89f2e
--- /dev/null
+++ b/extensions/arc/images/pitr.svg
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/extensions/arc/src/constants.ts b/extensions/arc/src/constants.ts
index 38622a4961..931049d3cd 100644
--- a/extensions/arc/src/constants.ts
+++ b/extensions/arc/src/constants.ts
@@ -34,6 +34,7 @@ export class IconPathHelper {
public static backup: IconPath;
public static properties: IconPath;
public static networking: IconPath;
+ public static pitr: IconPath;
public static refresh: IconPath;
public static reset: IconPath;
public static support: IconPath;
@@ -155,6 +156,10 @@ export class IconPathHelper {
light: context.asAbsolutePath('images/gear-colored-gray.svg'),
dark: context.asAbsolutePath('images/gear-colored-gray.svg'),
};
+ IconPathHelper.pitr = {
+ light: context.asAbsolutePath('images/pitr.svg'),
+ dark: context.asAbsolutePath('images/pitr.svg'),
+ };
}
}
diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts
index 6eb8d87d82..d4761ff8cd 100644
--- a/extensions/arc/src/localizedConstants.ts
+++ b/extensions/arc/src/localizedConstants.ts
@@ -24,6 +24,7 @@ export const properties = localize('arc.properties', "Properties");
export const settings = localize('arc.settings', "Settings");
export const security = localize('arc.security', "Security");
export const computeAndStorage = localize('arc.computeAndStorage', "Compute + Storage");
+export const backups = localize('arc.backups', "Backups");
export const coordinatorNodeParameters = localize('arc.coordinatorNodeParameters', "Coordinator Node Parameters");
export const workerNodeParameters = localize('arc.workerNodeParameters', "Worker Node Parameters");
export const compute = localize('arc.compute', "Compute");
@@ -34,7 +35,6 @@ export const supportAndTroubleshooting = localize('arc.supportAndTroubleshooting
export const resourceHealth = localize('arc.resourceHealth', "Resource health");
export const parameterName = localize('arc.parameterName', "Parameter Name");
export const value = localize('arc.value', "Value");
-
export const newInstance = localize('arc.createNew', "New Instance");
export const deleteText = localize('arc.delete', "Delete");
export const learnMore = localize('arc.learnMore', "Learn More.");
@@ -43,12 +43,14 @@ export const saveText = localize('arc.save', "Save");
export const discardText = localize('arc.discard', "Discard");
export const resetPassword = localize('arc.resetPassword', "Reset Password");
export const loadExtensions = localize('arc.loadExtensions', "Load extensions");
+export const configureRP = localize('arc.configureRP', "Configure retention policy");
export const unloadExtensions = localize('arc.unloadExtensions', "Unload extensions");
export const noExtensions = localize('arc.noExtensions', "No extensions listed in configuration.");
export const openInAzurePortal = localize('arc.openInAzurePortal', "Open in Azure Portal");
export const resourceGroup = localize('arc.resourceGroup', "Resource Group");
export const region = localize('arc.region', "Region");
export const subscriptionId = localize('arc.subscriptionId', "Subscription ID");
+export const subscription = localize('arc.subscription', "Subscription");
export const state = localize('arc.state', "State");
export const connectionMode = localize('arc.connectionMode', "Connection Mode");
export const namespace = localize('arc.namespace', "Namespace");
@@ -56,6 +58,21 @@ export const externalEndpoint = localize('arc.externalEndpoint', "External Endpo
export const name = localize('arc.name', "Name");
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 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 restore = localize('arc.restore', "Restore");
+export const instance = localize('arc.instance', "Instance");
+export const restorePoint = localize('arc.restorePoint', "Restore point (UTC)");
+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");
export const extensionsDescription = localize('arc.extensionsDescription', "PostgreSQL provides the ability to extend the functionality of your database by using extensions. Extensions allow for bundling multiple related SQL objects together in a single package that can be loaded or removed from your database with a single command. After being loaded in the database, extensions can function like built-in features.");
@@ -91,6 +108,7 @@ export const workerOneNodeValidationMessage = localize('arc.workerOneNodeValidat
export const vCores = localize('arc.vCores', "vCores");
export const ram = localize('arc.ram', "RAM");
export const refresh = localize('arc.refresh', "Refresh");
+export const configureRetentionPolicyButton = localize('arc.configureRetentionPolicyButton', "Configure Retention Policy");
export const resetAllToDefault = localize('arc.resetAllToDefault', "Reset all to default");
export const resetToDefault = localize('arc.resetToDefault', "Reset to default");
export const troubleshoot = localize('arc.troubleshoot', "Troubleshoot");
@@ -130,6 +148,7 @@ export const password = localize('arc.password', "Password");
export const rememberPassword = localize('arc.rememberPassword', "Remember Password");
export const connect = localize('arc.connect', "Connect");
export const cancel = localize('arc.cancel', "Cancel");
+export const apply = localize('arc.apply', "Apply");
export const ok = localize('arc.ok', "Ok");
export const on = localize('arc.on', "On");
export const off = localize('arc.off', "Off");
@@ -171,6 +190,9 @@ export const searchToFilter = localize('arc.searchToFilter', "Search to filter i
export const scalingCompute = localize('arc.scalingCompute', "scaling compute vCores and memory.");
export const postgresComputeAndStorageDescriptionPartOne = localize('arc.postgresComputeAndStorageDescriptionPartOne', "You can scale your Azure Arc-enabled");
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 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");
@@ -192,6 +214,7 @@ export const coresRequest = localize('arc.coresRequest', "CPU request");
export const workerCoresRequest = localize('arc.workerCoresRequest', "Worker Nodes CPU request");
export const coordinatorCoresRequest = localize('arc.coordinatorCoresRequest', "Coordinator Node CPU request");
export const memoryLimit = localize('arc.memoryLimit', "Memory limit (in GB)");
+export const retentionDays = localize('arc.retentionDays', "Point-In-Time Recovery retention (days)");
export const workerMemoryLimit = localize('arc.workerMemoryLimit', "Worker Nodes Memory limit (in GB)");
export const coordinatorMemoryLimit = localize('arc.coordinatorMemoryLimit', "Coordinator Node Memory limit (in GB)");
export const memoryRequest = localize('arc.memoryRequest', "Memory request (in GB)");
diff --git a/extensions/arc/src/models/miaaModel.ts b/extensions/arc/src/models/miaaModel.ts
index 8362f5c340..60a44c3b95 100644
--- a/extensions/arc/src/models/miaaModel.ts
+++ b/extensions/arc/src/models/miaaModel.ts
@@ -16,13 +16,23 @@ import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
import { ControllerModel, Registration } from './controllerModel';
import { ResourceModel } from './resourceModel';
-export type DatabaseModel = { name: string, status: string };
-
+export type DatabaseModel = { name: string, status: string, lastBackup: string };
+export type RPModel = { recoveryPointObjective: string, retentionDays: string };
+export type PITRModel = {
+ instanceName: string,
+ resourceGroupName: string,
+ location: string,
+ subscriptionId: string,
+ dbName: string,
+ restorePoint: string,
+ earliestPitr: string,
+ latestPitr: string,
+};
+export const systemDbs = ['master', 'msdb', 'tempdb', 'model'];
export class MiaaModel extends ResourceModel {
private _config: azExt.SqlMiShowResult | undefined;
private _databases: DatabaseModel[] = [];
-
private readonly _onConfigUpdated = new vscode.EventEmitter();
private readonly _onDatabasesUpdated = new vscode.EventEmitter();
private readonly _azApi: azExt.IExtension;
@@ -30,6 +40,10 @@ export class MiaaModel extends ResourceModel {
public onDatabasesUpdated = this._onDatabasesUpdated.event;
public configLastUpdated: Date | undefined;
public databasesLastUpdated: Date | undefined;
+ public rpSettings: RPModel = {
+ recoveryPointObjective: '',
+ retentionDays: ''
+ };
private _refreshPromise: Deferred | undefined = undefined;
@@ -76,6 +90,7 @@ export class MiaaModel extends ResourceModel {
const result = await this._azApi.az.sql.miarc.show(this.info.name, this.controllerModel.info.namespace, this.controllerModel.azAdditionalEnvVars);
this._config = result.stdout;
this.configLastUpdated = new Date();
+ this.rpSettings.retentionDays = this._config?.spec?.backup?.retentionPeriodInDays?.toString() ?? '';
this._onConfigUpdated.fire(this._config);
} catch (err) {
// If an error occurs show a message so the user knows something failed but still
@@ -111,6 +126,18 @@ export class MiaaModel extends ResourceModel {
}
}
+ public async callGetDatabases(): Promise {
+ try {
+ await this.getDatabases();
+ } catch (error) {
+ if (error instanceof UserCancelledError) {
+ vscode.window.showWarningMessage(loc.miaaConnectionRequired);
+ } else {
+ vscode.window.showErrorMessage(loc.fetchDatabasesFailed(this.info.name, error));
+ }
+ throw error;
+ }
+ }
public async getDatabases(promptForConnection: boolean = true): Promise {
if (!this._connectionProfile) {
await this.getConnectionProfile(promptForConnection);
@@ -132,9 +159,9 @@ export class MiaaModel extends ResourceModel {
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'] }; });
+ 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: '-' }; });
+ this._databases = (databases).map(db => { return { name: db, status: '-', lastBackup: '' }; });
}
this.databasesLastUpdated = new Date();
this._onDatabasesUpdated.fire(this._databases);
@@ -178,4 +205,5 @@ export class MiaaModel extends ResourceModel {
this._miaaInfo.userName = connectionProfile.userName;
await this._treeDataProvider.saveControllers();
}
+
}
diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaBackupsPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaBackupsPage.ts
new file mode 100644
index 0000000000..4a63790b90
--- /dev/null
+++ b/extensions/arc/src/ui/dashboards/miaa/miaaBackupsPage.ts
@@ -0,0 +1,323 @@
+/*---------------------------------------------------------------------------------------------
+ * 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 azExt from 'az-ext';
+import * as loc from '../../../localizedConstants';
+import { IconPathHelper, cssStyles } from '../../../constants';
+import { DashboardPage } from '../../components/dashboardPage';
+import { MiaaModel, RPModel, DatabaseModel, systemDbs } from '../../../models/miaaModel';
+import { ControllerModel } from '../../../models/controllerModel';
+import { ConfigureRPOSqlDialog } from '../../dialogs/configureRPOSqlDialog';
+import { RestoreSqlDialog } from '../../dialogs/restoreSqlDialog';
+
+export class MiaaBackupsPage extends DashboardPage {
+ constructor(modelView: azdata.ModelView, dashboard: azdata.window.ModelViewDashboard, private _controllerModel: ControllerModel, private _miaaModel: MiaaModel) {
+ super(modelView, dashboard);
+ 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;
+ private _configureRetentionPolicyButton!: azdata.ButtonComponent;
+ private _connectToServerLoading!: azdata.LoadingComponent;
+ private _connectToServerButton!: azdata.ButtonComponent;
+ private _databasesTableLoading!: azdata.LoadingComponent;
+ private _databasesTable!: azdata.DeclarativeTableComponent;
+ private _databasesMessage!: azdata.TextComponent;
+ private readonly _azApi: azExt.IExtension;
+
+ public saveArgs: RPModel = {
+ recoveryPointObjective: '',
+ retentionDays: ''
+ };
+
+ public pitrArgs = {
+ destName: '',
+ managedInstance: '',
+ time: '',
+ noWait: true
+ };
+
+ public get title(): string {
+ return loc.backups;
+ }
+
+ public get id(): string {
+ return 'backups';
+ }
+
+ public get icon(): { dark: string, light: string } {
+ return IconPathHelper.pitr;
+ }
+ protected async refresh(): Promise {
+ await Promise.all([this._controllerModel.refresh(false, this._controllerModel.info.namespace), this._miaaModel.refresh()]);
+ }
+
+ public get container(): azdata.Component {
+ const root = this.modelView.modelBuilder.flexContainer()
+ .withLayout({ flexFlow: 'column' })
+ .withProps({ CSSStyles: { 'margin': '18px' } })
+ .component();
+ const content = this.modelView.modelBuilder.divContainer().component();
+ this._databasesContainer = this.modelView.modelBuilder.divContainer().component();
+ root.addItem(content, { CSSStyles: { 'margin': '20px' } });
+ const databaseTitle = this.modelView.modelBuilder.text().withProps({
+ value: loc.databases,
+ CSSStyles: { ...cssStyles.title },
+ }).component();
+
+ content.addItem(databaseTitle);
+ const infoBackupDatabases = this.modelView.modelBuilder.text().withProps({
+ value: loc.miaaBackupsDatabasesDescription,
+ CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' }
+ }).component();
+ const backupInfoDescription = this.modelView.modelBuilder.flexContainer()
+ .withLayout({ flexWrap: 'wrap' })
+ .withItems([
+ infoBackupDatabases
+ ], { CSSStyles: { 'margin-right': '5px' } }).component();
+
+ const backupsDbsLearnMoreLink = this.modelView.modelBuilder.hyperlink().withProps({
+ label: loc.learnMore,
+ url: 'https://docs.microsoft.com/azure/azure-arc/data/point-in-time-restore',
+ CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
+ }).component();
+
+ const backupDatabaseInfoAndLink = this.modelView.modelBuilder.flexContainer()
+ .withLayout({ flexWrap: 'wrap' })
+ .withItems([
+ backupInfoDescription,
+ backupsDbsLearnMoreLink
+ ], { CSSStyles: { 'margin-right': '5px' } }).component();
+
+ content.addItem(backupDatabaseInfoAndLink, { CSSStyles: { 'min-height': '30px' } });
+
+ // Create loaded components
+ const connectToServerText = this.modelView.modelBuilder.text().withProps({
+ value: loc.miaaConnectionRequired
+ }).component();
+
+ this._connectToServerButton = this.modelView.modelBuilder.button().withProps({
+ label: loc.connectToServer,
+ enabled: false,
+ CSSStyles: { 'max-width': '125px', 'margin-left': '40%' }
+ }).component();
+
+ const connectToServerContainer = this.modelView.modelBuilder.divContainer().component();
+
+ connectToServerContainer.addItem(connectToServerText, { CSSStyles: { 'text-align': 'center', 'margin-top': '20px' } });
+ connectToServerContainer.addItem(this._connectToServerButton);
+
+ this._connectToServerLoading = this.modelView.modelBuilder.loadingComponent().withItem(connectToServerContainer).component();
+
+ this._databasesContainer.addItem(this._connectToServerLoading, { CSSStyles: { 'margin-top': '20px' } });
+
+ this._databasesTableLoading = this.modelView.modelBuilder.loadingComponent().component();
+ this._databasesTable = this.modelView.modelBuilder.declarativeTable().withProps({
+ width: '100%',
+ columns: [
+ {
+ displayName: loc.database,
+ 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%',
+ headerCssStyles: cssStyles.tableHeader,
+ rowCssStyles: cssStyles.tableRow
+ },
+ {
+ displayName: loc.restore,
+ valueType: azdata.DeclarativeDataType.component,
+ isReadOnly: true,
+ width: '20%',
+ headerCssStyles: cssStyles.tableHeader,
+ rowCssStyles: cssStyles.tableRow,
+ }
+ ],
+ dataValues: []
+ }).component();
+
+ this._databasesMessage = this.modelView.modelBuilder.text()
+ .withProps({ CSSStyles: { 'text-align': 'center' } })
+ .component();
+
+ this.handleDatabasesUpdated();
+ this._databasesTableLoading.component = this._databasesTable;
+ this.disposables.push(
+ this._connectToServerButton!.onDidClick(async () => {
+ this._connectToServerButton!.enabled = false;
+ this._databasesTableLoading!.loading = true;
+ try {
+ await this._miaaModel.callGetDatabases();
+ } catch {
+ this._connectToServerButton!.enabled = true;
+ }
+ })
+ );
+ root.addItem(this._databasesContainer);
+ root.addItem(this._databasesMessage);
+
+ this.initialized = true;
+ return root;
+ }
+
+ public get toolbarContainer(): azdata.ToolbarContainer {
+ // Refresh
+ const refreshButton = this.modelView.modelBuilder.button().withProps({
+ label: loc.refresh,
+ iconPath: IconPathHelper.refresh
+ }).component();
+ this.disposables.push(
+ refreshButton.onDidClick(async () => {
+ refreshButton.enabled = false;
+ try {
+ await this.refresh();
+ } finally {
+ refreshButton.enabled = true;
+ }
+ }));
+ this._configureRetentionPolicyButton = this.modelView.modelBuilder.button().withProps({
+ label: loc.configureRetentionPolicyButton,
+ enabled: true,
+ iconPath: IconPathHelper.edit,
+ }).component();
+ this.disposables.push(
+ this._configureRetentionPolicyButton.onDidClick(async () => {
+ const retentionPolicySqlDialog = new ConfigureRPOSqlDialog(this._miaaModel);
+ this.refreshRD();
+ retentionPolicySqlDialog.showDialog(loc.configureRP, this.saveArgs.retentionDays);
+
+ let rpArg = await retentionPolicySqlDialog.waitForClose();
+ if (rpArg) {
+ try {
+ this._configureRetentionPolicyButton.enabled = false;
+ this.saveArgs.retentionDays = rpArg.retentionDays;
+ await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: loc.updatingInstance(this._miaaModel.info.name),
+ cancellable: false
+ },
+ 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);
+
+ try {
+ await this._miaaModel.refresh();
+ } catch (error) {
+ vscode.window.showErrorMessage(loc.refreshFailed(error));
+ }
+ }
+ );
+
+ } catch (error) {
+ vscode.window.showErrorMessage(loc.updateExtensionsFailed(error));
+ } finally {
+ this._configureRetentionPolicyButton.enabled = true;
+ }
+ }
+ }));
+
+ return this.modelView.modelBuilder.toolbarContainer().withToolbarItems(
+ [
+ { component: refreshButton, toolbarSeparatorAfter: true },
+ { component: this._configureRetentionPolicyButton, toolbarSeparatorAfter: false },
+
+ ]
+ ).component();
+ }
+
+ private handleDatabasesUpdated(): void {
+ // 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.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;
+
+ if (this._miaaModel.databasesLastUpdated) {
+ // We successfully connected so now can remove the button and replace it with the actual databases table
+ this._databasesContainer.removeItem(this._connectToServerLoading);
+ this._databasesContainer.addItem(this._databasesTableLoading, { CSSStyles: { 'margin-bottom': '20px' } });
+
+ } else {
+ // If we don't have an endpoint then there's no point in showing the connect button - but the logic
+ // to display text informing the user of this is already handled by the handleMiaaConfigUpdated
+ if (this._miaaModel?.config?.status.primaryEndpoint) {
+ this._connectToServerLoading.loading = false;
+ this._connectToServerButton.enabled = true;
+ }
+ }
+ }
+
+ private refreshRD(): void {
+ this.saveArgs.retentionDays = this._miaaModel.config?.spec?.backup?.retentionPeriodInDays.toString() ?? '';
+ }
+
+ // Create restore button for every database entry in the database table
+ private createRestoreButton(db: DatabaseModel): azdata.ButtonComponent | string {
+ let pitrDate = db.lastBackup;
+ if (!pitrDate) {
+ return '';
+ }
+ const restoreButton = this.modelView.modelBuilder.button().withProps({
+ enabled: systemDbs.indexOf(db.name) > -1 ? false : true,
+ iconPath: IconPathHelper.openInTab,
+ }).component();
+ this.disposables.push(
+ restoreButton.onDidClick(async () => {
+ const restoreDialog = new RestoreSqlDialog(this._miaaModel, this._controllerModel, db);
+ restoreDialog.showDialog(loc.restoreDatabase);
+ let args = await restoreDialog.waitForClose();
+ if (args) {
+ try {
+ restoreButton.enabled = false;
+ this.pitrArgs.destName = args.dbName;
+ this.pitrArgs.managedInstance = args.instanceName;
+ this.pitrArgs.time = `"${args.restorePoint}"`;
+ await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: loc.updatingInstance(this._miaaModel.info.name),
+ cancellable: false
+ },
+ async (_progress, _token): Promise => {
+ await this._azApi.az.sql.midbarc.restore(
+ db.name, this.pitrArgs, this._miaaModel.controllerModel.info.namespace, this._miaaModel.controllerModel.azAdditionalEnvVars);
+ try {
+ await this._miaaModel.refresh();
+ } catch (error) {
+ vscode.window.showErrorMessage(loc.refreshFailed(error));
+ }
+ }
+ );
+ } catch (error) {
+ vscode.window.showErrorMessage(loc.updateExtensionsFailed(error));
+ } finally {
+ this._configureRetentionPolicyButton.enabled = true;
+ }
+ }
+ }));
+ return restoreButton;
+ }
+}
diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts b/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts
index 67601e386a..ba2fb32da6 100644
--- a/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts
+++ b/extensions/arc/src/ui/dashboards/miaa/miaaDashboard.ts
@@ -11,6 +11,7 @@ import * as loc from '../../../localizedConstants';
import { MiaaConnectionStringsPage } from './miaaConnectionStringsPage';
import { MiaaModel } from '../../../models/miaaModel';
import { MiaaComputeAndStoragePage } from './miaaComputeAndStoragePage';
+import { MiaaBackupsPage } from './miaaBackupsPage';
export class MiaaDashboard extends Dashboard {
@@ -29,13 +30,15 @@ export class MiaaDashboard extends Dashboard {
const overviewPage = new MiaaDashboardOverviewPage(modelView, this.dashboard, this._controllerModel, this._miaaModel);
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this.dashboard, this._miaaModel);
const computeAndStoragePage = new MiaaComputeAndStoragePage(modelView, this.dashboard, this._miaaModel);
+ const miaaBackupsPage = new MiaaBackupsPage(modelView, this.dashboard, this._controllerModel, this._miaaModel);
return [
overviewPage.tab,
{
title: loc.settings,
tabs: [
connectionStringsPage.tab,
- computeAndStoragePage.tab
+ computeAndStoragePage.tab,
+ miaaBackupsPage.tab
]
},
];
diff --git a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts
index 95b9bbd489..edf00834bc 100644
--- a/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts
+++ b/extensions/arc/src/ui/dashboards/miaa/miaaDashboardOverviewPage.ts
@@ -14,7 +14,6 @@ import { ControllerModel } from '../../../models/controllerModel';
import { MiaaModel } from '../../../models/miaaModel';
import { DashboardPage } from '../../components/dashboardPage';
import { ResourceType } from 'arc';
-import { UserCancelledError } from '../../../common/api';
export class MiaaDashboardOverviewPage extends DashboardPage {
@@ -212,7 +211,7 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
this._connectToServerButton!.enabled = false;
this._databasesTableLoading!.loading = true;
try {
- await this.callGetDatabases();
+ await this._miaaModel.callGetDatabases();
} catch {
this._connectToServerButton!.enabled = true;
}
@@ -322,19 +321,6 @@ export class MiaaDashboardOverviewPage extends DashboardPage {
).component();
}
- private async callGetDatabases(): Promise {
- try {
- await this._miaaModel.getDatabases();
- } catch (error) {
- if (error instanceof UserCancelledError) {
- vscode.window.showWarningMessage(loc.miaaConnectionRequired);
- } else {
- vscode.window.showErrorMessage(loc.fetchDatabasesFailed(this._miaaModel.info.name, error));
- }
- throw error;
- }
- }
-
private handleRegistrationsUpdated(): void {
const config = this._controllerModel.controllerConfig;
if (this._openInAzurePortalButton) {
diff --git a/extensions/arc/src/ui/dialogs/configureRPOSqlDialog.ts b/extensions/arc/src/ui/dialogs/configureRPOSqlDialog.ts
new file mode 100644
index 0000000000..1e9df7682e
--- /dev/null
+++ b/extensions/arc/src/ui/dialogs/configureRPOSqlDialog.ts
@@ -0,0 +1,101 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as azdata from 'azdata';
+import { Deferred } from '../../common/promise';
+import * as loc from '../../localizedConstants';
+import { cssStyles } from '../../constants';
+import { InitializingComponent } from '../components/initializingComponent';
+import { MiaaModel, RPModel } from '../../models/miaaModel';
+
+export class ConfigureRPOSqlDialog extends InitializingComponent {
+ protected modelBuilder!: azdata.ModelBuilder;
+ protected retentionDaysInputBox!: azdata.InputBoxComponent;
+ protected _completionPromise = new Deferred();
+ public saveArgs: RPModel = {
+ recoveryPointObjective: '',
+ retentionDays: ''
+ };
+
+ constructor(protected _model: MiaaModel) {
+ super();
+ }
+
+ public showDialog(dialogTitle: string, retentionDays: string | undefined): azdata.window.Dialog {
+ const dialog = azdata.window.createModelViewDialog(dialogTitle);
+ dialog.cancelButton.onClick(() => this.handleCancel());
+ retentionDays = (retentionDays === undefined ? this._model.config?.spec?.backup?.retentionPeriodInDays?.toString() : retentionDays);
+ dialog.registerContent(async view => {
+ this.modelBuilder = view.modelBuilder;
+ this.retentionDaysInputBox = this.modelBuilder.inputBox()
+ .withProps({
+ readOnly: false,
+ min: 1,
+ max: 35,
+ inputType: 'number',
+ ariaLabel: loc.retentionDays,
+ value: retentionDays
+ }).component();
+
+ const info = this.modelBuilder.text().withProps({
+ value: loc.pitrInfo,
+ CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
+ }).component();
+
+ const link = this.modelBuilder.hyperlink().withProps({
+ label: loc.learnMore,
+ url: 'https://docs.microsoft.com/azure/azure-arc/data/point-in-time-restore',
+ }).component();
+
+ const infoAndLink = this.modelBuilder.flexContainer().withLayout({ flexWrap: 'wrap' }).component();
+ infoAndLink.addItem(this.modelBuilder.text().withProps({
+ value: loc.pitr,
+ CSSStyles: { ...cssStyles.title }
+ }).component());
+ infoAndLink.addItem(info, { CSSStyles: { 'margin-right': '5px' } });
+ infoAndLink.addItem(link);
+
+ let formModel = this.modelBuilder.formContainer()
+ .withFormItems([{
+ components: [
+ {
+ component: infoAndLink
+ },
+ {
+ component: this.retentionDaysInputBox,
+ title: loc.retentionDays,
+ required: false
+ }
+ ],
+ title: ''
+ }]).withLayout({ width: '100%' }).component();
+ await view.initializeModel(formModel);
+ this.retentionDaysInputBox.focus();
+ this.initialized = true;
+ });
+
+ dialog.okButton.label = loc.apply;
+ dialog.cancelButton.label = loc.cancel;
+ dialog.registerCloseValidator(async () => await this.validate());
+ dialog.okButton.onClick(() => {
+ this.saveArgs.retentionDays = this.retentionDaysInputBox.value ?? '';
+ this._completionPromise.resolve(this.saveArgs);
+ });
+ azdata.window.openDialog(dialog);
+ return dialog;
+ }
+
+ public async validate(): Promise {
+ return !!this.retentionDaysInputBox.value;
+ }
+
+ private handleCancel(): void {
+ this._completionPromise.resolve(undefined);
+ }
+
+ public waitForClose(): Promise {
+ return this._completionPromise.promise;
+ }
+}
diff --git a/extensions/arc/src/ui/dialogs/restoreSqlDialog.ts b/extensions/arc/src/ui/dialogs/restoreSqlDialog.ts
new file mode 100644
index 0000000000..8211ace880
--- /dev/null
+++ b/extensions/arc/src/ui/dialogs/restoreSqlDialog.ts
@@ -0,0 +1,272 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as azdata from 'azdata';
+import { Deferred } from '../../common/promise';
+import * as loc from '../../localizedConstants';
+import * as vscode from 'vscode';
+import { cssStyles } from '../../constants';
+import { InitializingComponent } from '../components/initializingComponent';
+import { MiaaModel, PITRModel, DatabaseModel } from '../../models/miaaModel';
+import * as azurecore from 'azurecore';
+import { ControllerModel } from '../../models/controllerModel';
+
+export class RestoreSqlDialog extends InitializingComponent {
+ protected modelBuilder!: azdata.ModelBuilder;
+ private pitrSettings: PITRModel = {
+ instanceName: '-',
+ resourceGroupName: '-',
+ location: '-',
+ subscriptionId: '-',
+ dbName: '-',
+ restorePoint: '-',
+ earliestPitr: '-',
+ latestPitr: '-',
+ };
+
+ private earliestRestorePointInputBox!: azdata.InputBoxComponent;
+ private latestRestorePointInputBox!: azdata.InputBoxComponent;
+ private subscriptionInputBox!: azdata.InputBoxComponent;
+ private resourceGroupInputBox!: azdata.InputBoxComponent;
+ private sourceDbInputBox!: azdata.InputBoxComponent;
+ private restorePointInputBox!: azdata.InputBoxComponent;
+ private databaseNameInputBox!: azdata.InputBoxComponent;
+ private instanceInputBox!: azdata.InputBoxComponent;
+ protected _completionPromise = new Deferred();
+ private _azurecoreApi: azurecore.IExtension;
+ constructor(protected _miaaModel: MiaaModel, protected _controllerModel: ControllerModel, protected _database: DatabaseModel) {
+ super();
+ this._azurecoreApi = vscode.extensions.getExtension(azurecore.extension.name)?.exports;
+
+ }
+
+ public showDialog(dialogTitle: string): azdata.window.Dialog {
+ const dialog = azdata.window.createModelViewDialog(dialogTitle, dialogTitle, 'normal');
+ dialog.cancelButton.onClick(() => this.handleCancel());
+ dialog.registerContent(async view => {
+ this.modelBuilder = view.modelBuilder;
+ this.refreshPitrSettings();
+ const pitrTitle = this.modelBuilder.text().withProps({
+ value: loc.pitr,
+ CSSStyles: { ...cssStyles.title }
+ }).component();
+ const projectDetailsTitle = this.modelBuilder.text().withProps({
+ value: loc.projectDetails,
+ CSSStyles: { ...cssStyles.title }
+ }).component();
+ const projectDetailsTextLabel = this.modelBuilder.text().withProps({
+ value: loc.projectDetailsText,
+ }).component();
+ this.subscriptionInputBox = this.modelBuilder.inputBox()
+ .withProps({
+ enabled: false,
+ ariaLabel: loc.subscription,
+ value: this.pitrSettings.subscriptionId
+ }).component();
+ this.resourceGroupInputBox = this.modelBuilder.inputBox()
+ .withProps({
+ enabled: false,
+ ariaLabel: loc.resourceGroup,
+ value: this.pitrSettings.resourceGroupName
+ }).component();
+
+ const sourceDetailsTitle = this.modelBuilder.text().withProps({
+ value: loc.sourceDetails,
+ CSSStyles: { ...cssStyles.title }
+ }).component();
+ const sourceDetailsTextLabel = this.modelBuilder.text().withProps({
+ value: loc.sourceDetailsText,
+ }).component();
+ this.sourceDbInputBox = this.modelBuilder.inputBox()
+ .withProps({
+ enabled: false,
+ ariaLabel: loc.sourceDatabase,
+ value: this._database.name
+ }).component();
+
+ this.earliestRestorePointInputBox = this.modelBuilder.inputBox()
+ .withProps({
+ enabled: false,
+ ariaLabel: loc.earliestPitrRestorePoint,
+ value: ''
+ }).component();
+
+ this.latestRestorePointInputBox = this.modelBuilder.inputBox()
+ .withProps({
+ enabled: false,
+ ariaLabel: loc.latestpitrRestorePoint,
+ value: this._database.lastBackup
+ }).component();
+
+ this.restorePointInputBox = this.modelBuilder.inputBox()
+ .withProps({
+ readOnly: false,
+ ariaLabel: loc.restorePoint,
+ value: ''
+ }).component();
+ const databaseDetailsTitle = this.modelBuilder.text().withProps({
+ value: loc.databaseDetails,
+ CSSStyles: { ...cssStyles.title }
+ }).component();
+ const databaseDetailsTextLabel = this.modelBuilder.text().withProps({
+ value: loc.databaseDetailsText,
+ }).component();
+ this.databaseNameInputBox = this.modelBuilder.inputBox()
+ .withProps({
+ readOnly: false,
+ ariaLabel: loc.databaseName,
+ value: ''
+ }).component();
+
+ this.instanceInputBox = this.modelBuilder.inputBox()
+ .withProps({
+ enabled: false,
+ ariaLabel: loc.instance,
+ value: this.pitrSettings.instanceName
+ }).component();
+ const info = this.modelBuilder.text().withProps({
+ value: loc.restoreInfo,
+ CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
+ }).component();
+
+ const link = this.modelBuilder.hyperlink().withProps({
+ label: loc.learnMore,
+ url: 'https://docs.microsoft.com/azure/azure-arc/data/point-in-time-restore',
+ }).component();
+
+ const infoAndLink = this.modelBuilder.flexContainer().withLayout({ flexWrap: 'wrap' }).component();
+
+ infoAndLink.addItem(info, { CSSStyles: { 'margin-right': '5px' } });
+ infoAndLink.addItem(link);
+
+ let formModel = this.modelBuilder.formContainer()
+ .withFormItems([{
+ components: [
+ {
+ component: pitrTitle
+ },
+ {
+ component: infoAndLink
+ },
+ {
+ component: projectDetailsTitle,
+ },
+ {
+ component: projectDetailsTextLabel,
+ },
+ {
+ component: this.subscriptionInputBox,
+ title: loc.subscription,
+
+ },
+ {
+ component: this.resourceGroupInputBox,
+ title: loc.resourceGroup,
+
+ },
+ {
+ component: sourceDetailsTitle,
+ },
+ {
+ component: sourceDetailsTextLabel,
+ },
+ {
+ component: this.sourceDbInputBox,
+ title: loc.sourceDatabase,
+
+ },
+ {
+ component: this.earliestRestorePointInputBox,
+ title: loc.earliestPitrRestorePoint,
+
+ },
+ {
+ component: this.latestRestorePointInputBox,
+ title: loc.latestpitrRestorePoint,
+
+ },
+ {
+ component: this.restorePointInputBox,
+ title: loc.restorePoint,
+ required: true
+ },
+ {
+ component: databaseDetailsTitle,
+ },
+ {
+ component: databaseDetailsTextLabel,
+ },
+ {
+ component: this.databaseNameInputBox,
+ title: loc.databaseName,
+ required: true
+ },
+ {
+ component: this.instanceInputBox,
+ title: loc.instance,
+
+ },
+ ],
+ title: ''
+ }]).withLayout({ width: '100%' }).component();
+ await view.initializeModel(formModel);
+ this.subscriptionInputBox.focus();
+ this.resourceGroupInputBox.focus();
+ this.sourceDbInputBox.focus();
+ this.earliestRestorePointInputBox.focus();
+ this.latestRestorePointInputBox.focus();
+ this.restorePointInputBox.focus();
+ this.databaseNameInputBox.focus();
+ this.instanceInputBox.focus();
+ this.initialized = true;
+ });
+
+ dialog.okButton.label = loc.restore;
+ dialog.cancelButton.label = loc.cancel;
+ dialog.registerCloseValidator(async () => await this.validate());
+ dialog.okButton.onClick(() => {
+ this.pitrSettings.subscriptionId = this.subscriptionInputBox.value ?? '';
+ this.pitrSettings.instanceName = this.instanceInputBox.value ?? '';
+ this.pitrSettings.resourceGroupName = this.resourceGroupInputBox.value ?? '';
+ this.pitrSettings.dbName = this.databaseNameInputBox.value ?? '';
+ this.pitrSettings.earliestPitr = this.earliestRestorePointInputBox.value ?? '';
+ this.pitrSettings.latestPitr = this.latestRestorePointInputBox.value ?? '';
+ this.pitrSettings.restorePoint = this.restorePointInputBox.value ?? '';
+ this._completionPromise.resolve(this.pitrSettings);
+ });
+ azdata.window.openDialog(dialog);
+ return dialog;
+ }
+
+ public async validate(): Promise {
+ if (!this.subscriptionInputBox.value || !this.resourceGroupInputBox.value
+ || !this.sourceDbInputBox.value || !this.latestRestorePointInputBox.value
+ || !this.restorePointInputBox.value || !this.databaseNameInputBox.value
+ || !this.instanceInputBox.value) {
+ return false;
+ }
+ else {
+ return true;
+ }
+ }
+
+ private handleCancel(): void {
+ this._completionPromise.resolve(undefined);
+ }
+
+ public waitForClose(): Promise {
+ return this._completionPromise.promise;
+ }
+
+ public refreshPitrSettings(): void {
+ this.pitrSettings.instanceName = this._miaaModel?.config?.metadata.name || this.pitrSettings.instanceName;
+ this.pitrSettings.resourceGroupName = this._controllerModel?.controllerConfig?.spec.settings.azure.resourceGroup || this.pitrSettings.resourceGroupName;
+ this.pitrSettings.location = this._azurecoreApi.getRegionDisplayName(this._controllerModel?.controllerConfig?.spec.settings.azure.location) || this.pitrSettings.location;
+ this.pitrSettings.subscriptionId = this._controllerModel?.controllerConfig?.spec.settings.azure.subscription || this.pitrSettings.subscriptionId;
+ this.pitrSettings.dbName = this._database.name;
+ this.pitrSettings.restorePoint = this._database.lastBackup;
+ this.pitrSettings.earliestPitr = '';
+ }
+}
diff --git a/extensions/azcli/src/api.ts b/extensions/azcli/src/api.ts
index 3119f35df3..4fcae48652 100644
--- a/extensions/azcli/src/api.ts
+++ b/extensions/azcli/src/api.ts
@@ -132,6 +132,20 @@ export function getAzApi(localAzDiscovered: Promise, azTool
validateAz(azToolService.localAz);
return azToolService.localAz!.sql.miarc.edit(name, args, namespace, additionalEnvVars);
}
+ },
+ midbarc: {
+ restore: async (name: string,
+ args: {
+ destName?: string,
+ managedInstance?: string,
+ time?: string,
+ noWait?: boolean,
+ },
+ namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars) => {
+ await localAzDiscovered;
+ validateAz(azToolService.localAz);
+ return azToolService.localAz!.sql.midbarc.restore(name, args, namespace, additionalEnvVars);
+ }
}
},
getPath: async () => {
diff --git a/extensions/azcli/src/az.ts b/extensions/azcli/src/az.ts
index fa6c8ee209..0a45c2ef6e 100644
--- a/extensions/azcli/src/az.ts
+++ b/extensions/azcli/src/az.ts
@@ -162,6 +162,7 @@ export class AzTool implements azExt.IAzApi {
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
+ retentionDays?: string
},
namespace: string,
additionalEnvVars?: azExt.AdditionalEnvVars
@@ -172,11 +173,33 @@ export class AzTool implements azExt.IAzApi {
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
+ if (args.retentionDays) { argsArray.push('--retention-days', args.retentionDays); }
+ return this.executeCommand(argsArray, additionalEnvVars);
+ }
+ },
+ midbarc: {
+ restore: (
+ name: string,
+ args: {
+ destName?: string,
+ managedInstance?: string,
+ time?: string,
+ noWait?: boolean,
+ },
+ namespace: string,
+ additionalEnvVars?: azExt.AdditionalEnvVars
+ ): 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);
}
}
};
+
/**
* Gets the output of running '--version' command on the az tool.
* It also updates the cachedVersion property based on the return value from the tool.
diff --git a/extensions/azcli/src/typings/az-ext.d.ts b/extensions/azcli/src/typings/az-ext.d.ts
index 51cc8ef700..a4ff276c57 100644
--- a/extensions/azcli/src/typings/az-ext.d.ts
+++ b/extensions/azcli/src/typings/az-ext.d.ts
@@ -141,6 +141,9 @@ declare module 'az-ext' {
uid: string // "cea737aa-3f82-4f6a-9bed-2b51c2c33dff"
},
spec: {
+ backup?: {
+ retentionPeriodInDays: number, // 1
+ }
scheduling?: {
default?: {
resources?: {
@@ -314,11 +317,25 @@ declare module 'az-ext' {
edit(
name: string,
args: {
- coresLimit?: string,
- coresRequest?: string,
- memoryLimit?: string,
- memoryRequest?: string,
- noWait?: boolean,
+ coresLimit?: string, //2
+ coresRequest?: string, //1
+ memoryLimit?: string, // 2Gi
+ memoryRequest?: string, //1Gi
+ noWait?: boolean, //true
+ retentionDays?: string, //5
+ },
+ namespace?: string,
+ additionalEnvVars?: AdditionalEnvVars
+ ): Promise>
+ },
+ midbarc: {
+ restore(
+ name: string,
+ args: {
+ destName?: string, //testDb
+ managedInstance?: string, //sqlmi1
+ time?: string, //2021-10-12T11:16:30.000Z
+ noWait?: boolean, //true
},
namespace?: string,
additionalEnvVars?: AdditionalEnvVars