From ef2395c5267d9b527000217bc3c03325c68c769f Mon Sep 17 00:00:00 2001 From: Mohamed Hefny Date: Wed, 12 Nov 2025 22:08:21 -0800 Subject: [PATCH 1/3] Add soft delete feature for CosmosDB SQL API - Implemented 12 soft delete commands (list, show, delete/purge, recover for accounts, databases, and collections) - Added soft delete configuration parameters to cosmosdb create/update commands - Added comprehensive test coverage with 6 test methods - Optimized test execution by removing unnecessary sleep statements - Added linter exclusions for framework-generated parameters - Updated HISTORY.rst and bumped version to 1.7.0 --- linter_exclusions.yml | 17 + src/cosmosdb-preview/HISTORY.rst | 7 + .../azext_cosmosdb_preview/_client_factory.py | 13 + .../azext_cosmosdb_preview/_help.py | 137 +++++ .../azext_cosmosdb_preview/_params.py | 72 +++ .../azext_cosmosdb_preview/commands.py | 48 +- .../azext_cosmosdb_preview/custom.py | 148 +++++- .../test_cosmosdb_softdelete_scenario.py | 488 ++++++++++++++++++ src/cosmosdb-preview/setup.py | 2 +- 9 files changed, 926 insertions(+), 6 deletions(-) create mode 100644 src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py diff --git a/linter_exclusions.yml b/linter_exclusions.yml index ce4aaa82bca..4461cdcc540 100644 --- a/linter_exclusions.yml +++ b/linter_exclusions.yml @@ -3504,3 +3504,20 @@ neon postgres organization: neon postgres project: rule_exclusions: - require_wait_command_if_no_wait + +cosmosdb update: + parameters: + soft_deletion_retention_period_in_minutes: + rule_exclusions: + - unrecognized_help_parameter + min_minutes_before_permanent_deletion_allowed: + rule_exclusions: + - unrecognized_help_parameter + +cosmosdb sql softdeleted-database list: + rule_exclusions: + - no_ids_for_list_commands + +cosmosdb sql softdeleted-collection list: + rule_exclusions: + - no_ids_for_list_commands \ No newline at end of file diff --git a/src/cosmosdb-preview/HISTORY.rst b/src/cosmosdb-preview/HISTORY.rst index a31383adcba..8ad38372aad 100644 --- a/src/cosmosdb-preview/HISTORY.rst +++ b/src/cosmosdb-preview/HISTORY.rst @@ -2,6 +2,13 @@ Release History =============== +1.7.0 ++++++ +* Add support for soft-deleted resource operations for SQL API +* New command group `az cosmosdb sql softdeleted-account` to list, show, delete (purge), and recover soft-deleted accounts +* New command group `az cosmosdb sql softdeleted-database` to list, show, delete (purge), and recover soft-deleted databases +* New command group `az cosmosdb sql softdeleted-collection` to list, show, delete (purge), and recover soft-deleted containers + 1.6.1 +++++ * Fix SQL container throughput update to preserve existing throughput buckets when not explicitly specified. diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/_client_factory.py b/src/cosmosdb-preview/azext_cosmosdb_preview/_client_factory.py index 4afa56f268f..0e6703fe3ae 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/_client_factory.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/_client_factory.py @@ -119,3 +119,16 @@ def cf_fleetspace_account(cli_ctx, _): def cf_fleet_analytics(cli_ctx, _): return cf_cosmosdb_preview(cli_ctx).fleet_analytics + + +# soft-deleted resources +def cf_softdeleted_database_accounts(cli_ctx, _): + return cf_cosmosdb_preview(cli_ctx).soft_deleted_database_accounts + + +def cf_softdeleted_sql_databases(cli_ctx, _): + return cf_cosmosdb_preview(cli_ctx).soft_deleted_sql_databases + + +def cf_softdeleted_sql_containers(cli_ctx, _): + return cf_cosmosdb_preview(cli_ctx).soft_deleted_sql_containers diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py b/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py index 7e5576bee00..32a67530ad9 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py @@ -564,11 +564,25 @@ Default: single region account in the location of the specified resource group. Failover priority values are 0 for write regions and greater than 0 for read regions. A failover priority value must be unique and less than the total number of regions. Multiple locations can be specified by using more than one `--locations` argument. + - name: --enabled-soft-deletion + short-summary: Enable or disable soft deletion on the account + long-summary: | + When enabled, deleted databases and containers are retained for the configured retention period and can be recovered. + - name: --sd-retention + short-summary: Soft deletion retention period in minutes + long-summary: | + The retention period for soft-deleted resources. Must be at least equal to min-minutes-before-permanent-deletion-allowed. + - name: --min-purge-minutes + short-summary: Minimum minutes before permanent deletion is allowed + long-summary: | + The minimum time that must pass after soft-deletion before a resource can be permanently deleted (purged). examples: - name: Update an Azure Cosmos DB database account. (autogenerated) text: az cosmosdb update --capabilities EnableGremlin --name MyCosmosDBDatabaseAccount --resource-group MyResourceGroup - name: Update an Azure Cosmos DB database account to enable materialized views. text: az cosmosdb update --name MyCosmosDBDatabaseAccount --resource-group MyResourceGroup --enable-materialized-views true + - name: Enable soft deletion with 1440 minutes (24 hours) retention. + text: az cosmosdb update --name MyCosmosDBDatabaseAccount --resource-group MyResourceGroup --enabled-soft-deletion true --sd-retention 1440 --min-purge-minutes 60 """ # restore account @@ -1844,3 +1858,126 @@ type: command short-summary: Delete a Fleet Analytics resource from a Fleet. """ + +helps['cosmosdb sql softdeleted-account'] = """ +type: group +short-summary: Manage soft-deleted Azure Cosmos DB accounts. +""" + +helps['cosmosdb sql softdeleted-account list'] = """ +type: command +short-summary: List all soft-deleted Azure Cosmos DB accounts in a subscription. +examples: + - name: List all soft-deleted Azure Cosmos DB accounts in a subscription. + text: | + az cosmosdb sql softdeleted-account list --location westus --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-account show'] = """ +type: command +short-summary: Show details of a soft-deleted Azure Cosmos DB account. +examples: + - name: Show details of a soft-deleted Azure Cosmos DB account. + text: | + az cosmosdb sql softdeleted-account show --location westus --account-name MyAccount --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-account delete'] = """ +type: command +short-summary: Permanently delete a soft-deleted Azure Cosmos DB account. +examples: + - name: Permanently delete a soft-deleted Azure Cosmos DB account. + text: | + az cosmosdb sql softdeleted-account delete --location westus --account-name MyAccount --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-account recover'] = """ +type: command +short-summary: Recover a soft-deleted Azure Cosmos DB account. +examples: + - name: Recover a soft-deleted Azure Cosmos DB account. + text: | + az cosmosdb sql softdeleted-account recover --location westus --account-name MyAccount --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-database'] = """ +type: group +short-summary: Manage soft-deleted databases for Azure Cosmos DB SQL API. +""" + +helps['cosmosdb sql softdeleted-database list'] = """ +type: command +short-summary: List all soft-deleted databases for an Azure Cosmos DB account. +examples: + - name: List all soft-deleted databases for an Azure Cosmos DB account. + text: | + az cosmosdb sql softdeleted-database list --location westus --account-name MyAccount --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-database show'] = """ +type: command +short-summary: Show details of a soft-deleted database. +examples: + - name: Show details of a soft-deleted database. + text: | + az cosmosdb sql softdeleted-database show --location westus --account-name MyAccount --name MyDatabase --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-database delete'] = """ +type: command +short-summary: Permanently delete a soft-deleted database. +examples: + - name: Permanently delete a soft-deleted database. + text: | + az cosmosdb sql softdeleted-database delete --location westus --account-name MyAccount --name MyDatabase --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-database recover'] = """ +type: command +short-summary: Recover a soft-deleted database. +examples: + - name: Recover a soft-deleted database. + text: | + az cosmosdb sql softdeleted-database recover --location westus --account-name MyAccount --name MyDatabase --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-collection'] = """ +type: group +short-summary: Manage soft-deleted collections for Azure Cosmos DB SQL API. +""" + +helps['cosmosdb sql softdeleted-collection list'] = """ +type: command +short-summary: List all soft-deleted collections in a database. +examples: + - name: List all soft-deleted collections in a database. + text: | + az cosmosdb sql softdeleted-collection list --location westus --account-name MyAccount --database-name MyDatabase --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-collection show'] = """ +type: command +short-summary: Show details of a soft-deleted collection. +examples: + - name: Show details of a soft-deleted collection. + text: | + az cosmosdb sql softdeleted-collection show --location westus --account-name MyAccount --database-name MyDatabase --name MyCollection --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-collection delete'] = """ +type: command +short-summary: Permanently delete a soft-deleted collection. +examples: + - name: Permanently delete a soft-deleted collection. + text: | + az cosmosdb sql softdeleted-collection delete --location westus --account-name MyAccount --database-name MyDatabase --name MyCollection --resource-group MyResourceGroup +""" + +helps['cosmosdb sql softdeleted-collection recover'] = """ +type: command +short-summary: Recover a soft-deleted collection. +examples: + - name: Recover a soft-deleted collection. + text: | + az cosmosdb sql softdeleted-collection recover --location westus --account-name MyAccount --database-name MyDatabase --name MyCollection --resource-group MyResourceGroup +""" diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py b/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py index b89c8ac3e2c..bbabc121f12 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py @@ -829,6 +829,78 @@ def load_arguments(self, _): c.argument('scope', options_list=['--scope', '-s'], help="Data plane resource path at which this Role Assignment is being granted.") c.argument('principal_id', options_list=['--principal-id', '-p'], help="AAD Object ID of the principal to which this Role Assignment is being granted.") + # Soft-deleted Account + with self.argument_context('cosmosdb sql softdeleted-account list') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the soft-deleted account.", required=False) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-account show') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the soft-deleted account.", required=True) + c.argument('account_name', options_list=['--account-name', '-n'], help="Name of the soft-deleted Cosmos DB account.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-account delete') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the soft-deleted account.", required=True) + c.argument('account_name', options_list=['--account-name', '-n'], help="Name of the soft-deleted Cosmos DB account to purge.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-account recover') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the soft-deleted account.", required=True) + c.argument('account_name', options_list=['--account-name', '-n'], help="Name of the soft-deleted Cosmos DB account to recover.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + # Soft-deleted Database + with self.argument_context('cosmosdb sql softdeleted-database list') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-database show') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--name', '-n'], help="Name of the soft-deleted database.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-database delete') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--name', '-n'], help="Name of the soft-deleted database to purge.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-database recover') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--name', '-n'], help="Name of the soft-deleted database to recover.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + # Soft-deleted Collection + with self.argument_context('cosmosdb sql softdeleted-collection list') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--database-name', '-d'], help="Name of the database.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-collection show') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--database-name', '-d'], help="Name of the database.", required=True) + c.argument('container_name', options_list=['--name', '-n'], help="Name of the soft-deleted container.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-collection delete') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--database-name', '-d'], help="Name of the database.", required=True) + c.argument('container_name', options_list=['--name', '-n'], help="Name of the soft-deleted container to purge.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + + with self.argument_context('cosmosdb sql softdeleted-collection recover') as c: + c.argument('location', options_list=['--location', '-l'], help="Location of the account.", required=True) + c.argument('account_name', options_list=['--account-name', '-a'], help="Name of the Cosmos DB account.", required=True) + c.argument('database_name', options_list=['--database-name', '-d'], help="Name of the database.", required=True) + c.argument('container_name', options_list=['--name', '-n'], help="Name of the soft-deleted container to recover.", required=True) + c.argument('resource_group', options_list=['--resource-group', '-g'], help="Name of the resource group.", required=True) + # Cosmos DB Fleet with self.argument_context('cosmosdb fleet') as c: c.argument('resource_group', options_list=['--resource-group', '-g'], help='Name of the resource group.', required=True) diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/commands.py b/src/cosmosdb-preview/azext_cosmosdb_preview/commands.py index 2acf5462bad..7bcdb010192 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/commands.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/commands.py @@ -31,7 +31,10 @@ cf_fleet, cf_fleetspace, cf_fleetspace_account, - cf_fleet_analytics + cf_fleet_analytics, + cf_softdeleted_database_accounts, + cf_softdeleted_sql_databases, + cf_softdeleted_sql_containers ) @@ -220,6 +223,19 @@ def load_command_table(self, _): operations_tmpl='azure.mgmt.cosmosdb.operations#RestorableDatabaseAccountsOperations.{}', client_factory=cf_restorable_database_accounts) + # Soft-deleted resources SDK types + cosmosdb_softdeleted_accounts_sdk = CliCommandType( + operations_tmpl='azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.operations#SoftDeletedDatabaseAccountsOperations.{}', + client_factory=cf_softdeleted_database_accounts) + + cosmosdb_softdeleted_sql_databases_sdk = CliCommandType( + operations_tmpl='azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.operations#SoftDeletedSqlDatabasesOperations.{}', + client_factory=cf_softdeleted_sql_databases) + + cosmosdb_softdeleted_sql_containers_sdk = CliCommandType( + operations_tmpl='azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.operations#SoftDeletedSqlContainersOperations.{}', + client_factory=cf_softdeleted_sql_containers) + # define commands # Restorable apis for sql,mongodb,gremlin and table # Provisioning/migrate Continuous 7 days accounts @@ -367,6 +383,36 @@ def load_command_table(self, _): with self.command_group('cosmosdb table', cosmosdb_table_sdk, client_factory=cf_table_resources) as g: g.custom_command('restore', 'cli_cosmosdb_table_restore', is_preview=True) + # Soft-deleted Account commands + with self.command_group('cosmosdb sql softdeleted-account', + cosmosdb_softdeleted_accounts_sdk, + client_factory=cf_softdeleted_database_accounts, + is_preview=True) as g: + g.custom_command('list', 'cli_cosmosdb_sql_softdeleted_account_list') + g.custom_show_command('show', 'cli_cosmosdb_sql_softdeleted_account_show') + g.custom_command('delete', 'cli_cosmosdb_sql_softdeleted_account_delete', confirmation=True) + g.custom_command('recover', 'cli_cosmosdb_sql_softdeleted_account_recover') + + # Soft-deleted Database commands + with self.command_group('cosmosdb sql softdeleted-database', + cosmosdb_softdeleted_sql_databases_sdk, + client_factory=cf_softdeleted_sql_databases, + is_preview=True) as g: + g.custom_command('list', 'cli_cosmosdb_sql_softdeleted_database_list') + g.custom_show_command('show', 'cli_cosmosdb_sql_softdeleted_database_show') + g.custom_command('delete', 'cli_cosmosdb_sql_softdeleted_database_delete', confirmation=True) + g.custom_command('recover', 'cli_cosmosdb_sql_softdeleted_database_recover') + + # Soft-deleted Collection commands + with self.command_group('cosmosdb sql softdeleted-collection', + cosmosdb_softdeleted_sql_containers_sdk, + client_factory=cf_softdeleted_sql_containers, + is_preview=True) as g: + g.custom_command('list', 'cli_cosmosdb_sql_softdeleted_collection_list') + g.custom_show_command('show', 'cli_cosmosdb_sql_softdeleted_collection_show') + g.custom_command('delete', 'cli_cosmosdb_sql_softdeleted_collection_delete', confirmation=True) + g.custom_command('recover', 'cli_cosmosdb_sql_softdeleted_collection_recover') + setup_mongocluster_commands(self) setup_fleet_commands(self) diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py b/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py index 55d2c3bef6b..063971e4827 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py @@ -955,7 +955,10 @@ def cli_cosmosdb_update(client, default_priority_level=None, enable_prpp_autoscale=None, enable_partition_merge=None, - capacity_mode=None): + capacity_mode=None, + enabled_soft_deletion=None, + soft_deletion_retention_period_in_minutes=None, + min_minutes_before_permanent_deletion_allowed=None): """Update an existing Azure Cosmos DB database account. """ existing = client.get(resource_group_name, account_name) @@ -1022,6 +1025,17 @@ def cli_cosmosdb_update(client, analytical_storage_configuration = AnalyticalStorageConfiguration() analytical_storage_configuration.schema_type = analytical_storage_schema_type + soft_delete_configuration = None + if enabled_soft_deletion is not None or \ + soft_deletion_retention_period_in_minutes is not None or \ + min_minutes_before_permanent_deletion_allowed is not None: + from azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.models import SoftDeleteConfiguration + soft_delete_configuration = SoftDeleteConfiguration( + enabled_soft_deletion=enabled_soft_deletion, + soft_deletion_retention_period_in_minutes=soft_deletion_retention_period_in_minutes, + min_minutes_before_permanent_deletion_allowed=min_minutes_before_permanent_deletion_allowed + ) + params = DatabaseAccountUpdateParameters( locations=locations, tags=tags, @@ -1047,7 +1061,8 @@ def cli_cosmosdb_update(client, default_priority_level=default_priority_level, enable_per_region_per_partition_autoscale=enable_prpp_autoscale, enable_partition_merge=enable_partition_merge, - capacity_mode=capacity_mode) + capacity_mode=capacity_mode, + soft_delete_configuration=soft_delete_configuration) async_docdb_update = client.begin_update(resource_group_name, account_name, params) docdb_account = async_docdb_update.result() @@ -1200,7 +1215,10 @@ def _create_database_account(client, enable_prpp_autoscale=None, disable_ttl=None, enable_partition_merge=None, - capacity_mode=None): + capacity_mode=None, + enabled_soft_deletion=None, + soft_deletion_retention_period_in_minutes=None, + min_minutes_before_permanent_deletion_allowed=None): consistency_policy = None if default_consistency_level is not None: consistency_policy = ConsistencyPolicy(default_consistency_level=default_consistency_level, @@ -1280,6 +1298,17 @@ def _create_database_account(client, analytical_storage_configuration = AnalyticalStorageConfiguration() analytical_storage_configuration.schema_type = analytical_storage_schema_type + soft_delete_configuration = None + if enabled_soft_deletion is not None or \ + soft_deletion_retention_period_in_minutes is not None or \ + min_minutes_before_permanent_deletion_allowed is not None: + from azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.models import SoftDeleteConfiguration + soft_delete_configuration = SoftDeleteConfiguration( + enabled_soft_deletion=enabled_soft_deletion, + soft_deletion_retention_period_in_minutes=soft_deletion_retention_period_in_minutes, + min_minutes_before_permanent_deletion_allowed=min_minutes_before_permanent_deletion_allowed + ) + create_mode = CreateMode.restore.value if is_restore_request else CreateMode.default.value params = None restore_parameters = None @@ -1340,7 +1369,8 @@ def _create_database_account(client, default_priority_level=default_priority_level, enable_per_region_per_partition_autoscale=enable_prpp_autoscale, enable_partition_merge=enable_partition_merge, - capacity_mode=capacity_mode + capacity_mode=capacity_mode, + soft_delete_configuration=soft_delete_configuration ) async_docdb_create = client.begin_create_or_update(resource_group_name, account_name, params) @@ -3377,3 +3407,113 @@ def cli_cosmosdb_fleetspace_account_create(client, fleetspace_account_name=fleetspace_account_name, body=fleetspace_account_body ) + + +# Soft-deleted Account operations +def cli_cosmosdb_sql_softdeleted_account_list(client, + resource_group, + location=None): + """List soft-deleted Cosmos DB accounts.""" + if location is not None: + return client.list_by_location(resource_group, location) + return client.list(resource_group) + + +def cli_cosmosdb_sql_softdeleted_account_show(client, + resource_group, + location, + account_name): + """Get a soft-deleted Cosmos DB account.""" + return client.get(resource_group, location, account_name) + + +def cli_cosmosdb_sql_softdeleted_account_delete(client, + resource_group, + location, + account_name): + """Purge a soft-deleted Cosmos DB account.""" + return client.begin_delete(resource_group, location, account_name) + + +def cli_cosmosdb_sql_softdeleted_account_recover(client, + resource_group, + location, + account_name): + """Recover a soft-deleted Cosmos DB account.""" + return client.begin_restore(resource_group, location, account_name) + + +# Soft-deleted Database operations +def cli_cosmosdb_sql_softdeleted_database_list(client, + resource_group, + location, + account_name): + """List soft-deleted databases in a Cosmos DB account.""" + return client.list(resource_group, location, account_name) + + +def cli_cosmosdb_sql_softdeleted_database_show(client, + resource_group, + location, + account_name, + database_name): + """Get a soft-deleted database.""" + return client.get(resource_group, location, account_name, database_name) + + +def cli_cosmosdb_sql_softdeleted_database_delete(client, + resource_group, + location, + account_name, + database_name): + """Purge a soft-deleted database.""" + return client.begin_delete(resource_group, location, account_name, database_name) + + +def cli_cosmosdb_sql_softdeleted_database_recover(client, + resource_group, + location, + account_name, + database_name): + """Recover a soft-deleted database.""" + return client.begin_restore(resource_group, location, account_name, database_name) + + +# Soft-deleted Collection operations +def cli_cosmosdb_sql_softdeleted_collection_list(client, + resource_group, + location, + account_name, + database_name): + """List soft-deleted containers in a database.""" + return client.list(resource_group, location, account_name, database_name) + + +def cli_cosmosdb_sql_softdeleted_collection_show(client, + resource_group, + location, + account_name, + database_name, + container_name): + """Get a soft-deleted container.""" + return client.get(resource_group, location, account_name, database_name, container_name) + + +def cli_cosmosdb_sql_softdeleted_collection_delete(client, + resource_group, + location, + account_name, + database_name, + container_name): + """Purge a soft-deleted container.""" + return client.begin_delete(resource_group, location, account_name, database_name, container_name) + + +def cli_cosmosdb_sql_softdeleted_collection_recover(client, + resource_group, + location, + account_name, + database_name, + container_name): + """Recover a soft-deleted container.""" + return client.begin_restore(resource_group, location, account_name, database_name, container_name) diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py b/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py new file mode 100644 index 00000000000..2c5584d6182 --- /dev/null +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py @@ -0,0 +1,488 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os +import unittest +import time +import datetime + +from azure.cli.testsdk.scenario_tests import AllowLargeResponse +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer) +from knack.log import get_logger + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + +logger = get_logger(__name__) + + +class CosmosDBSoftDeleteScenarioTest(ScenarioTest): + """ + Test suite for CosmosDB SQL API soft-delete functionality. + + This test suite validates the soft-delete and recovery operations for: + - Database Accounts + - SQL Databases + - SQL Collections/Containers + + Note: These tests require a CosmosDB account with soft-delete feature enabled. + """ + + def _create_account_with_soft_delete(self, account_name, resource_group, location): + """ + Helper function to create a CosmosDB account and enable soft delete. + Sets retention period and minimum purge minutes to 0 for testing. + """ + self.kwargs.update({ + 'acc': account_name, + 'rg': resource_group, + 'loc': location + }) + + logger.info("Creating CosmosDB account") + self.cmd('az cosmosdb create -n {acc} -g {rg} --locations regionName={loc}') + + logger.info("Enabling soft delete on account") + self.cmd( + 'az cosmosdb update -n {acc} -g {rg} ' + '--enabled-soft-deletion true ' + '--sd-retention 0 ' + '--min-purge-minutes 0' + ) + logger.info("Account created with soft delete enabled") + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_acc_recover', location='westus') + def test_cosmosdb_sql_softdeleted_account_recover(self, resource_group): + """ + Test soft-deleted account recovery operation. + + This test validates that soft-deleted accounts can be recovered. + """ + location = "westus" + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisdacc', length=20), + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Soft-deleting the account") + self.cmd('az cosmosdb delete -n {acc} -g {rg} --yes') + + logger.info("Waiting for account deletion to complete") + time.sleep(120) + + logger.info("Listing soft-deleted accounts") + soft_deleted_accounts = self.cmd( + 'az cosmosdb softdeleted-account list ' + '--location {loc} -g {rg}' + ).get_output_in_json() + + deleted_account_found = any(acc.get('name') == self.kwargs['acc'] for acc in soft_deleted_accounts) + assert deleted_account_found, f"Soft-deleted account '{self.kwargs['acc']}' should appear in the list" + + logger.info("Showing soft-deleted account details") + soft_deleted_account = self.cmd( + 'az cosmosdb softdeleted-account show ' + '--location {loc} --account-name {acc} -g {rg}' + ).get_output_in_json() + assert soft_deleted_account is not None + + logger.info("Recovering the soft-deleted account") + self.cmd( + 'az cosmosdb softdeleted-account recover ' + '--location {loc} --account-name {acc} -g {rg}' + ) + + logger.info("Waiting for account recovery to complete") + time.sleep(120) + + logger.info("Verifying account is recovered") + recovered_account = self.cmd('az cosmosdb show -n {acc} -g {rg}').get_output_in_json() + assert recovered_account is not None + + logger.info("Verifying account is no longer in soft-deleted list") + soft_deleted_accounts_after = self.cmd( + 'az cosmosdb softdeleted-account list ' + '--location {loc} -g {rg}' + ).get_output_in_json() + + recovered_account_found = any(acc.get('name') == self.kwargs['acc'] for acc in soft_deleted_accounts_after) + assert not recovered_account_found, "Account should not appear in soft-deleted list after recovery" + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_acc_purge', location='westus') + def test_cosmosdb_sql_softdeleted_account_purge(self, resource_group): + """ + Test soft-deleted account purge (permanent deletion) operation. + + This test validates that soft-deleted accounts can be permanently removed. + """ + location = "westus" + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisdpacc', length=20), + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Soft-deleting the account") + self.cmd('az cosmosdb delete -n {acc} -g {rg} --yes') + + logger.info("Waiting for account deletion to complete") + time.sleep(120) + + logger.info("Listing soft-deleted accounts") + soft_deleted_accounts = self.cmd( + 'az cosmosdb softdeleted-account list ' + '--location {loc} -g {rg}' + ).get_output_in_json() + + deleted_account_found = any(acc.get('name') == self.kwargs['acc'] for acc in soft_deleted_accounts) + assert deleted_account_found, f"Soft-deleted account '{self.kwargs['acc']}' should appear in the list" + + logger.info("Showing soft-deleted account details") + soft_deleted_account = self.cmd( + 'az cosmosdb softdeleted-account show ' + '--location {loc} --account-name {acc} -g {rg}' + ).get_output_in_json() + assert soft_deleted_account is not None + + logger.info("Purging the soft-deleted account") + self.cmd( + 'az cosmosdb softdeleted-account delete ' + '--location {loc} --account-name {acc} -g {rg} --yes' + ) + + logger.info("Waiting for account purge to complete") + time.sleep(120) + + logger.info("Account successfully purged") + + logger.info("Verifying account is no longer in soft-deleted list") + soft_deleted_accounts_after = self.cmd( + 'az cosmosdb softdeleted-account list ' + '--location {loc} -g {rg}' + ).get_output_in_json() + + purged_account_found = any(acc.get('name') == self.kwargs['acc'] for acc in soft_deleted_accounts_after) + assert not purged_account_found, "Account should not appear in soft-deleted list after purge" + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_db_recover', location='westus') + def test_cosmosdb_sql_softdeleted_database_recover(self, resource_group): + """ + Test soft-deleted database recovery operations: list, show, and recover. + + This test validates the database soft-delete and recovery workflow. + """ + location = "westus" + db_name = self.create_random_name(prefix='clisdddb', length=15) + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisddacc', length=20), + 'db_name': db_name, + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Creating SQL database") + database_create = self.cmd( + 'az cosmosdb sql database create -g {rg} -a {acc} -n {db_name}' + ).get_output_in_json() + assert database_create["name"] == db_name + + logger.info("Waiting for database to stabilize") + time.sleep(60) + + logger.info("Soft-deleting the database") + self.cmd('az cosmosdb sql database delete -g {rg} -a {acc} -n {db_name} --yes') + + time.sleep(60) + + logger.info("Listing soft-deleted databases") + soft_deleted_dbs = self.cmd( + 'az cosmosdb sql softdeleted-database list ' + '--location {loc} --account-name {acc} -g {rg}' + ).get_output_in_json() + + # Verify the deleted database appears in the list + deleted_db_found = any(db.get('name') == db_name for db in soft_deleted_dbs) + assert deleted_db_found, f"Soft-deleted database '{db_name}' should appear in the list" + + logger.info("Showing soft-deleted database details") + soft_deleted_db = self.cmd( + 'az cosmosdb sql softdeleted-database show ' + '--location {loc} --account-name {acc} --name {db_name} -g {rg}' + ).get_output_in_json() + assert soft_deleted_db is not None + logger.info(f"Soft-deleted database details retrieved successfully") + + logger.info("Recovering the soft-deleted database") + self.cmd( + 'az cosmosdb sql softdeleted-database recover ' + '--location {loc} --account-name {acc} --name {db_name} -g {rg}' + ) + + time.sleep(120) + + recovered_db = self.cmd( + 'az cosmosdb sql database show -g {rg} -a {acc} -n {db_name}' + ).get_output_in_json() + assert recovered_db["name"] == db_name + logger.info("Database successfully recovered") + + logger.info("Verifying database is no longer soft-deleted") + soft_deleted_dbs_after = self.cmd( + 'az cosmosdb sql softdeleted-database list ' + '--location {loc} --account-name {acc} -g {rg}' + ).get_output_in_json() + + still_soft_deleted = any(db.get('name') == db_name for db in soft_deleted_dbs_after) + assert not still_soft_deleted, "Database should not appear in soft-deleted list after recovery" + + logger.info("Cleaning up test resources") + self.cmd('az cosmosdb sql database delete -g {rg} -a {acc} -n {db_name} --yes') + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_coll_recover', location='westus') + def test_cosmosdb_sql_softdeleted_collection_recover(self, resource_group): + """ + Test soft-deleted collection recovery operations: list, show, and recover. + + This test validates the collection/container soft-delete and recovery workflow. + """ + location = "westus" + db_name = self.create_random_name(prefix='clisdddb', length=15) + coll_name = self.create_random_name(prefix='clisddcoll', length=15) + partition_key = "/pk" + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisddacc', length=20), + 'db_name': db_name, + 'coll_name': coll_name, + 'part': partition_key, + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Creating SQL database") + self.cmd('az cosmosdb sql database create -g {rg} -a {acc} -n {db_name}') + + logger.info("Creating SQL container") + collection_create = self.cmd( + 'az cosmosdb sql container create -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name} -p {part}' + ).get_output_in_json() + assert collection_create["name"] == coll_name + + logger.info("Waiting for container to stabilize") + time.sleep(60) + + logger.info("Soft-deleting the container") + self.cmd( + 'az cosmosdb sql container delete -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name} --yes' + ) + + time.sleep(60) + + logger.info("Listing soft-deleted collections") + soft_deleted_colls = self.cmd( + 'az cosmosdb sql softdeleted-collection list ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} -g {rg}' + ).get_output_in_json() + + # Verify the deleted collection appears in the list + deleted_coll_found = any(coll.get('name') == coll_name for coll in soft_deleted_colls) + assert deleted_coll_found, f"Soft-deleted collection '{coll_name}' should appear in the list" + + logger.info("Showing soft-deleted collection details") + soft_deleted_coll = self.cmd( + 'az cosmosdb sql softdeleted-collection show ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} --name {coll_name} -g {rg}' + ).get_output_in_json() + assert soft_deleted_coll is not None + logger.info(f"Soft-deleted collection details retrieved successfully") + + logger.info("Recovering the soft-deleted collection") + self.cmd( + 'az cosmosdb sql softdeleted-collection recover ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} --name {coll_name} -g {rg}' + ) + + time.sleep(120) + + recovered_coll = self.cmd( + 'az cosmosdb sql container show -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name}' + ).get_output_in_json() + assert recovered_coll["name"] == coll_name + logger.info("Collection successfully recovered") + + logger.info("Verifying collection is no longer soft-deleted") + soft_deleted_colls_after = self.cmd( + 'az cosmosdb sql softdeleted-collection list ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} -g {rg}' + ).get_output_in_json() + + still_soft_deleted = any(coll.get('name') == coll_name for coll in soft_deleted_colls_after) + assert not still_soft_deleted, "Collection should not appear in soft-deleted list after recovery" + + logger.info("Cleaning up test resources") + self.cmd( + 'az cosmosdb sql container delete -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name} --yes' + ) + self.cmd('az cosmosdb sql database delete -g {rg} -a {acc} -n {db_name} --yes') + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_purge', location='westus') + def test_cosmosdb_sql_softdeleted_database_purge(self, resource_group): + """ + Test soft-deleted database purge (permanent deletion) operation. + + This test validates that soft-deleted databases can be permanently removed. + """ + location = "westus" + db_name = self.create_random_name(prefix='clisdpdb', length=15) + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisdpacc', length=20), + 'db_name': db_name, + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Creating SQL database") + self.cmd('az cosmosdb sql database create -g {rg} -a {acc} -n {db_name}') + + logger.info("Waiting for database to stabilize") + time.sleep(60) + + logger.info("Soft-deleting the database") + self.cmd('az cosmosdb sql database delete -g {rg} -a {acc} -n {db_name} --yes') + + time.sleep(60) + + logger.info("Purging the soft-deleted database") + try: + self.cmd( + 'az cosmosdb sql softdeleted-database delete ' + '--location {loc} --account-name {acc} --name {db_name} -g {rg} --yes' + ) + + time.sleep(60) + + logger.info("Database successfully purged") + + soft_deleted_dbs = self.cmd( + 'az cosmosdb sql softdeleted-database list ' + '--location {loc} --account-name {acc} -g {rg}' + ).get_output_in_json() + + purged_db_found = any(db.get('name') == db_name for db in soft_deleted_dbs) + assert not purged_db_found, "Database should not appear in soft-deleted list after purge" + + except Exception as e: + logger.warning(f"Database purge test skipped or failed: {e}") + + @AllowLargeResponse() + @ResourceGroupPreparer(name_prefix='cli_test_cosmosdb_softdelete_coll_purge', location='westus') + def test_cosmosdb_sql_softdeleted_collection_purge(self, resource_group): + """ + Test soft-deleted collection purge (permanent deletion) operation. + + This test validates that soft-deleted collections can be permanently removed. + """ + location = "westus" + db_name = self.create_random_name(prefix='clisdpdb', length=15) + coll_name = self.create_random_name(prefix='clisdpcoll', length=15) + partition_key = "/pk" + + self.kwargs.update({ + 'acc': self.create_random_name(prefix='clisdpacc', length=20), + 'db_name': db_name, + 'coll_name': coll_name, + 'part': partition_key, + 'loc': location + }) + + self._create_account_with_soft_delete(self.kwargs['acc'], resource_group, location) + + logger.info("Creating SQL database") + self.cmd('az cosmosdb sql database create -g {rg} -a {acc} -n {db_name}') + + logger.info("Creating SQL container") + self.cmd( + 'az cosmosdb sql container create -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name} -p {part}' + ) + + logger.info("Waiting for container to stabilize") + time.sleep(60) + + logger.info("Soft-deleting the container") + self.cmd( + 'az cosmosdb sql container delete -g {rg} -a {acc} ' + '-d {db_name} -n {coll_name} --yes' + ) + + time.sleep(60) + + logger.info("Listing soft-deleted collections") + soft_deleted_colls = self.cmd( + 'az cosmosdb sql softdeleted-collection list ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} -g {rg}' + ).get_output_in_json() + + deleted_coll_found = any(coll.get('name') == coll_name for coll in soft_deleted_colls) + assert deleted_coll_found, f"Soft-deleted collection '{coll_name}' should appear in the list" + + logger.info("Showing soft-deleted collection details") + soft_deleted_coll = self.cmd( + 'az cosmosdb sql softdeleted-collection show ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} --name {coll_name} -g {rg}' + ).get_output_in_json() + assert soft_deleted_coll is not None + + logger.info("Purging the soft-deleted collection") + self.cmd( + 'az cosmosdb sql softdeleted-collection delete ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} --name {coll_name} -g {rg} --yes' + ) + + time.sleep(60) + + logger.info("Collection successfully purged") + + logger.info("Verifying collection is no longer in soft-deleted list") + soft_deleted_colls_after = self.cmd( + 'az cosmosdb sql softdeleted-collection list ' + '--location {loc} --account-name {acc} ' + '--database-name {db_name} -g {rg}' + ).get_output_in_json() + + purged_coll_found = any(coll.get('name') == coll_name for coll in soft_deleted_colls_after) + assert not purged_coll_found, "Collection should not appear in soft-deleted list after purge" + + logger.info("Cleaning up test resources") + self.cmd('az cosmosdb sql database delete -g {rg} -a {acc} -n {db_name} --yes') + + +if __name__ == '__main__': + unittest.main() diff --git a/src/cosmosdb-preview/setup.py b/src/cosmosdb-preview/setup.py index 55fc3ee24f5..4c9b8ae2b6b 100644 --- a/src/cosmosdb-preview/setup.py +++ b/src/cosmosdb-preview/setup.py @@ -16,7 +16,7 @@ # TODO: Confirm this is the right version number you want and it matches your # HISTORY.rst entry. -VERSION = '1.6.1' +VERSION = '1.7.0' # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers From d29b8a07d1aa9b6646952f28678a3ab7bd8b3b90 Mon Sep 17 00:00:00 2001 From: Mohamed Hefny Date: Wed, 12 Nov 2025 23:06:00 -0800 Subject: [PATCH 2/3] Fix parameter naming: enable_soft_deletion instead of enabled_soft_deletion - Updated _params.py to use enable_soft_deletion parameter name - Updated custom.py function signatures and implementations - Updated _help.py examples to use --enable-soft-deletion - Updated test file to use correct CLI option - Removed unnecessary linter exclusions for soft delete parameters - Kept required linter exclusions for list commands (no_ids_for_list_commands) --- linter_exclusions.yml | 9 --------- .../azext_cosmosdb_preview/_help.py | 14 +------------- .../azext_cosmosdb_preview/_params.py | 3 +++ .../azext_cosmosdb_preview/custom.py | 12 ++++++------ .../latest/test_cosmosdb_softdelete_scenario.py | 2 +- 5 files changed, 11 insertions(+), 29 deletions(-) diff --git a/linter_exclusions.yml b/linter_exclusions.yml index 4461cdcc540..650aedfdffa 100644 --- a/linter_exclusions.yml +++ b/linter_exclusions.yml @@ -3505,15 +3505,6 @@ neon postgres project: rule_exclusions: - require_wait_command_if_no_wait -cosmosdb update: - parameters: - soft_deletion_retention_period_in_minutes: - rule_exclusions: - - unrecognized_help_parameter - min_minutes_before_permanent_deletion_allowed: - rule_exclusions: - - unrecognized_help_parameter - cosmosdb sql softdeleted-database list: rule_exclusions: - no_ids_for_list_commands diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py b/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py index 32a67530ad9..e6a4b5f2b80 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py @@ -564,25 +564,13 @@ Default: single region account in the location of the specified resource group. Failover priority values are 0 for write regions and greater than 0 for read regions. A failover priority value must be unique and less than the total number of regions. Multiple locations can be specified by using more than one `--locations` argument. - - name: --enabled-soft-deletion - short-summary: Enable or disable soft deletion on the account - long-summary: | - When enabled, deleted databases and containers are retained for the configured retention period and can be recovered. - - name: --sd-retention - short-summary: Soft deletion retention period in minutes - long-summary: | - The retention period for soft-deleted resources. Must be at least equal to min-minutes-before-permanent-deletion-allowed. - - name: --min-purge-minutes - short-summary: Minimum minutes before permanent deletion is allowed - long-summary: | - The minimum time that must pass after soft-deletion before a resource can be permanently deleted (purged). examples: - name: Update an Azure Cosmos DB database account. (autogenerated) text: az cosmosdb update --capabilities EnableGremlin --name MyCosmosDBDatabaseAccount --resource-group MyResourceGroup - name: Update an Azure Cosmos DB database account to enable materialized views. text: az cosmosdb update --name MyCosmosDBDatabaseAccount --resource-group MyResourceGroup --enable-materialized-views true - name: Enable soft deletion with 1440 minutes (24 hours) retention. - text: az cosmosdb update --name MyCosmosDBDatabaseAccount --resource-group MyResourceGroup --enabled-soft-deletion true --sd-retention 1440 --min-purge-minutes 60 + text: az cosmosdb update --name MyCosmosDBDatabaseAccount --resource-group MyResourceGroup --enable-soft-deletion true --sd-retention 1440 --min-purge-minutes 60 """ # restore account diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py b/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py index bbabc121f12..164ea224b1f 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/_params.py @@ -486,6 +486,9 @@ def load_arguments(self, _): with self.argument_context('cosmosdb update') as c: c.argument('key_uri', help="The URI of the key vault", is_preview=True) + c.argument('enable_soft_deletion', arg_type=get_three_state_flag(), help="Flag to enable or disable soft deletion on the account.", is_preview=True, arg_group='Soft Delete') + c.argument('soft_deletion_retention_period_in_minutes', options_list=['--soft-deletion-retention-period-in-minutes', '--sd-retention'], type=int, help="Soft deletion retention period in minutes. Must be at least equal to min_minutes_before_permanent_deletion_allowed.", is_preview=True, arg_group='Soft Delete') + c.argument('min_minutes_before_permanent_deletion_allowed', options_list=['--min-minutes-before-permanent-deletion-allowed', '--min-purge-minutes'], type=int, help="Minimum minutes before permanent deletion is allowed for soft-deleted resources.", is_preview=True, arg_group='Soft Delete') with self.argument_context('cosmosdb restore') as c: c.argument('target_database_account_name', options_list=['--target-database-account-name', '-n'], help='Name of the new target Cosmos DB database account after the restore') diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py b/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py index 063971e4827..4df70ffd6a0 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py @@ -956,7 +956,7 @@ def cli_cosmosdb_update(client, enable_prpp_autoscale=None, enable_partition_merge=None, capacity_mode=None, - enabled_soft_deletion=None, + enable_soft_deletion=None, soft_deletion_retention_period_in_minutes=None, min_minutes_before_permanent_deletion_allowed=None): """Update an existing Azure Cosmos DB database account. """ @@ -1026,12 +1026,12 @@ def cli_cosmosdb_update(client, analytical_storage_configuration.schema_type = analytical_storage_schema_type soft_delete_configuration = None - if enabled_soft_deletion is not None or \ + if enable_soft_deletion is not None or \ soft_deletion_retention_period_in_minutes is not None or \ min_minutes_before_permanent_deletion_allowed is not None: from azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.models import SoftDeleteConfiguration soft_delete_configuration = SoftDeleteConfiguration( - enabled_soft_deletion=enabled_soft_deletion, + enable_soft_deletion=enable_soft_deletion, soft_deletion_retention_period_in_minutes=soft_deletion_retention_period_in_minutes, min_minutes_before_permanent_deletion_allowed=min_minutes_before_permanent_deletion_allowed ) @@ -1216,7 +1216,7 @@ def _create_database_account(client, disable_ttl=None, enable_partition_merge=None, capacity_mode=None, - enabled_soft_deletion=None, + enable_soft_deletion=None, soft_deletion_retention_period_in_minutes=None, min_minutes_before_permanent_deletion_allowed=None): consistency_policy = None @@ -1299,12 +1299,12 @@ def _create_database_account(client, analytical_storage_configuration.schema_type = analytical_storage_schema_type soft_delete_configuration = None - if enabled_soft_deletion is not None or \ + if enable_soft_deletion is not None or \ soft_deletion_retention_period_in_minutes is not None or \ min_minutes_before_permanent_deletion_allowed is not None: from azext_cosmosdb_preview.vendored_sdks.azure_mgmt_cosmosdb.models import SoftDeleteConfiguration soft_delete_configuration = SoftDeleteConfiguration( - enabled_soft_deletion=enabled_soft_deletion, + enabled_soft_deletion=enable_soft_deletion, soft_deletion_retention_period_in_minutes=soft_deletion_retention_period_in_minutes, min_minutes_before_permanent_deletion_allowed=min_minutes_before_permanent_deletion_allowed ) diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py b/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py index 2c5584d6182..d599353699c 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py @@ -46,7 +46,7 @@ def _create_account_with_soft_delete(self, account_name, resource_group, locatio logger.info("Enabling soft delete on account") self.cmd( 'az cosmosdb update -n {acc} -g {rg} ' - '--enabled-soft-deletion true ' + '--enable-soft-deletion true ' '--sd-retention 0 ' '--min-purge-minutes 0' ) From 95e471ccc9eda2861ab11fb42110fb2040ac2308 Mon Sep 17 00:00:00 2001 From: Mohamed Hefny Date: Thu, 20 Nov 2025 09:56:18 -0800 Subject: [PATCH 3/3] Update methhod signitures for softdeletion --- .../azext_cosmosdb_preview/_help.py | 9 ++++++--- .../azext_cosmosdb_preview/custom.py | 14 +++++++------- .../test_cosmosdb_softdelete_scenario.py | 18 +++++++++--------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py b/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py index e6a4b5f2b80..d9795b3aaa3 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/_help.py @@ -1854,11 +1854,14 @@ helps['cosmosdb sql softdeleted-account list'] = """ type: command -short-summary: List all soft-deleted Azure Cosmos DB accounts in a subscription. +short-summary: List soft-deleted Azure Cosmos DB accounts. examples: - name: List all soft-deleted Azure Cosmos DB accounts in a subscription. text: | - az cosmosdb sql softdeleted-account list --location westus --resource-group MyResourceGroup + az cosmosdb sql softdeleted-account list + - name: List soft-deleted Azure Cosmos DB accounts in a specific location. + text: | + az cosmosdb sql softdeleted-account list --location westus """ helps['cosmosdb sql softdeleted-account show'] = """ @@ -1867,7 +1870,7 @@ examples: - name: Show details of a soft-deleted Azure Cosmos DB account. text: | - az cosmosdb sql softdeleted-account show --location westus --account-name MyAccount --resource-group MyResourceGroup + az cosmosdb sql softdeleted-account show --location westus --account-name MyAccount """ helps['cosmosdb sql softdeleted-account delete'] = """ diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py b/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py index 4df70ffd6a0..453affe2df8 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/custom.py @@ -3415,8 +3415,8 @@ def cli_cosmosdb_sql_softdeleted_account_list(client, location=None): """List soft-deleted Cosmos DB accounts.""" if location is not None: - return client.list_by_location(resource_group, location) - return client.list(resource_group) + return client.list_by_location(location) + return client.list() def cli_cosmosdb_sql_softdeleted_account_show(client, @@ -3424,7 +3424,7 @@ def cli_cosmosdb_sql_softdeleted_account_show(client, location, account_name): """Get a soft-deleted Cosmos DB account.""" - return client.get(resource_group, location, account_name) + return client.get_by_location(location, account_name) def cli_cosmosdb_sql_softdeleted_account_delete(client, @@ -3432,7 +3432,7 @@ def cli_cosmosdb_sql_softdeleted_account_delete(client, location, account_name): """Purge a soft-deleted Cosmos DB account.""" - return client.begin_delete(resource_group, location, account_name) + return client.begin_purge(resource_group, account_name) def cli_cosmosdb_sql_softdeleted_account_recover(client, @@ -3440,7 +3440,7 @@ def cli_cosmosdb_sql_softdeleted_account_recover(client, location, account_name): """Recover a soft-deleted Cosmos DB account.""" - return client.begin_restore(resource_group, location, account_name) + return client.begin_restore(resource_group, account_name) # Soft-deleted Database operations @@ -3467,7 +3467,7 @@ def cli_cosmosdb_sql_softdeleted_database_delete(client, account_name, database_name): """Purge a soft-deleted database.""" - return client.begin_delete(resource_group, location, account_name, database_name) + return client.begin_purge(resource_group, location, account_name, database_name) def cli_cosmosdb_sql_softdeleted_database_recover(client, @@ -3506,7 +3506,7 @@ def cli_cosmosdb_sql_softdeleted_collection_delete(client, database_name, container_name): """Purge a soft-deleted container.""" - return client.begin_delete(resource_group, location, account_name, database_name, container_name) + return client.begin_purge(resource_group, location, account_name, database_name, container_name) def cli_cosmosdb_sql_softdeleted_collection_recover(client, diff --git a/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py b/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py index d599353699c..58dacbcbae0 100644 --- a/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py +++ b/src/cosmosdb-preview/azext_cosmosdb_preview/tests/latest/test_cosmosdb_softdelete_scenario.py @@ -29,7 +29,7 @@ class CosmosDBSoftDeleteScenarioTest(ScenarioTest): Note: These tests require a CosmosDB account with soft-delete feature enabled. """ - def _create_account_with_soft_delete(self, account_name, resource_group, location): + def _update_account_with_soft_delete(self, account_name, resource_group, location): """ Helper function to create a CosmosDB account and enable soft delete. Sets retention period and minimum purge minutes to 0 for testing. @@ -77,7 +77,7 @@ def test_cosmosdb_sql_softdeleted_account_recover(self, resource_group): logger.info("Listing soft-deleted accounts") soft_deleted_accounts = self.cmd( - 'az cosmosdb softdeleted-account list ' + 'az cosmosdb sql softdeleted-account list ' '--location {loc} -g {rg}' ).get_output_in_json() @@ -86,14 +86,14 @@ def test_cosmosdb_sql_softdeleted_account_recover(self, resource_group): logger.info("Showing soft-deleted account details") soft_deleted_account = self.cmd( - 'az cosmosdb softdeleted-account show ' + 'az cosmosdb sql softdeleted-account show ' '--location {loc} --account-name {acc} -g {rg}' ).get_output_in_json() assert soft_deleted_account is not None logger.info("Recovering the soft-deleted account") self.cmd( - 'az cosmosdb softdeleted-account recover ' + 'az cosmosdb sql softdeleted-account recover ' '--location {loc} --account-name {acc} -g {rg}' ) @@ -106,7 +106,7 @@ def test_cosmosdb_sql_softdeleted_account_recover(self, resource_group): logger.info("Verifying account is no longer in soft-deleted list") soft_deleted_accounts_after = self.cmd( - 'az cosmosdb softdeleted-account list ' + 'az cosmosdb sql softdeleted-account list ' '--location {loc} -g {rg}' ).get_output_in_json() @@ -138,7 +138,7 @@ def test_cosmosdb_sql_softdeleted_account_purge(self, resource_group): logger.info("Listing soft-deleted accounts") soft_deleted_accounts = self.cmd( - 'az cosmosdb softdeleted-account list ' + 'az cosmosdb sql softdeleted-account list ' '--location {loc} -g {rg}' ).get_output_in_json() @@ -147,14 +147,14 @@ def test_cosmosdb_sql_softdeleted_account_purge(self, resource_group): logger.info("Showing soft-deleted account details") soft_deleted_account = self.cmd( - 'az cosmosdb softdeleted-account show ' + 'az cosmosdb sql softdeleted-account show ' '--location {loc} --account-name {acc} -g {rg}' ).get_output_in_json() assert soft_deleted_account is not None logger.info("Purging the soft-deleted account") self.cmd( - 'az cosmosdb softdeleted-account delete ' + 'az cosmosdb sql softdeleted-account delete ' '--location {loc} --account-name {acc} -g {rg} --yes' ) @@ -165,7 +165,7 @@ def test_cosmosdb_sql_softdeleted_account_purge(self, resource_group): logger.info("Verifying account is no longer in soft-deleted list") soft_deleted_accounts_after = self.cmd( - 'az cosmosdb softdeleted-account list ' + 'az cosmosdb sql softdeleted-account list ' '--location {loc} -g {rg}' ).get_output_in_json()