Skip to content

[KeyVault] az keyvault secret copy: Add command to copy secrets between vaults#32751

Open
jcassanji-southworks wants to merge 43 commits into
Azure:devfrom
jcassanji-southworks:jcassanji-southworks/feature-keyvault-copy
Open

[KeyVault] az keyvault secret copy: Add command to copy secrets between vaults#32751
jcassanji-southworks wants to merge 43 commits into
Azure:devfrom
jcassanji-southworks:jcassanji-southworks/feature-keyvault-copy

Conversation

@jcassanji-southworks

Copy link
Copy Markdown
Contributor

Related command
az keyvault secret copy

Description
This PR introduces a new command az keyvault secret copy to simplify the process of copying secrets from one Key Vault to another. It supports copying individual secrets or all secrets in bulk, with options to control overwrite behavior and preserve metadata.

Motivation and Benefits
Currently, users needing to migrate or replicate secrets between Key Vaults (e.g., promoting from Dev to Prod, or replicating for DR) must write complex scripts. This new command standardizes this workflow into a single CLI operation.

  • Productivity: Reduces multi-step scripting to a single command.
  • Accuracy: Ensures all metadata (tags, content-type) is preserved.
  • Safety: Built-in protection against accidental overwrites and automatic filtering of Azure-managed secrets.

Implementation Details

  • Registered copy command under keyvault secret group.
  • Implemented core logic using azure-keyvault-secrets track2 SDK.
  • Added comprehensive unit tests covering single copy, bulk copy, overwrite protection, and metadata preservation.

Testing Guide

  1. Copy a single secret:

    az keyvault secret copy --source-vault <SourceKV> --destination-vault <DestKV> --name <SecretName>
  2. Copy all secrets:

    az keyvault secret copy --source-vault <SourceKV> --destination-vault <DestKV> --all
  3. Force copy (overwrite existing):

    az keyvault secret copy --source-vault <SourceKV> --destination-vault <DestKV> --name <SecretName> --overwrite

History Notes
[KeyVault] az keyvault secret copy: Add new command to copy secrets between Key Vaults


This checklist is used to make sure that common guidelines for a pull request are followed.

jcassanji-southworks and others added 24 commits February 4, 2026 19:24
…test_keyvault_commands.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…test_keyvault_commands.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 5, 2026 22:05
@azure-client-tools-bot-prd

azure-client-tools-bot-prd Bot commented Feb 5, 2026

Copy link
Copy Markdown
❌AzureCLI-FullTest
️✔️acr
️✔️latest
️✔️3.12
️✔️3.14
️✔️acs
️✔️latest
️✔️3.12
️✔️3.14
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.14
️✔️ams
️✔️latest
️✔️3.12
️✔️3.14
️✔️apim
️✔️latest
️✔️3.12
️✔️3.14
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.14
️✔️appservice
️✔️latest
️✔️3.12
️✔️3.14
️✔️aro
️✔️latest
️✔️3.12
️✔️3.14
️✔️backup
️✔️latest
️✔️3.12
️✔️3.14
️✔️batch
️✔️latest
️✔️3.12
️✔️3.14
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.14
️✔️billing
️✔️latest
️✔️3.12
️✔️3.14
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.14
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.14
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.14
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.14
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.14
️✔️config
️✔️latest
️✔️3.12
️✔️3.14
️✔️configure
️✔️latest
️✔️3.12
️✔️3.14
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.14
️✔️container
️✔️latest
️✔️3.12
️✔️3.14
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.14
️✔️core
️✔️latest
️✔️3.12
️✔️3.14
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.14
️✔️databoxedge
️✔️latest
️✔️3.12
️✔️3.14
️✔️dls
️✔️latest
️✔️3.12
️✔️3.14
️✔️dms
️✔️latest
️✔️3.12
️✔️3.14
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.14
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.14
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.14
️✔️find
️✔️latest
️✔️3.12
️✔️3.14
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.14
️✔️identity
️✔️latest
️✔️3.12
️✔️3.14
️✔️iot
️✔️latest
️✔️3.12
️✔️3.14
❌keyvault
❌latest
❌3.12
Type Test Case Error Message Line
Failed test_create_vault_sets_enable_soft_delete_true self = <azure.cli.command_modules.keyvault.tests.latest.test_keyvault_commands.CreateVaultSoftDeleteTest testMethod=test_create_vault_sets_enable_soft_delete_true>
mock_sdk_no_wait = <MagicMock name='sdk_no_wait' id='140067147232368'>
mock_profile = <MagicMock name='Profile' id='140067146647600'>
mock_network = <MagicMock name='create_network_rule_set' id='140067144751568'>

    @mock.patch('azure.cli.command_modules.keyvault.custom.create_network_rule_set', return_value=None)
    @mock.patch('azure.cli.core.profile.Profile')
    @mock.patch('azure.cli.core.util.sdk_no_wait')
    def test_create_vault_sets_enable_soft_delete_true(self, mock_sdk_no_wait, mock_profile, mock_network):
        from azure.cli.command_modules.keyvault.custom import create_vault
    
        mock_profile.return_value.get_subscription.return_value = {
            'tenantId': '00000000-0000-0000-0000-000000000000'
        }
    
        # Build a minimal cmd mock that returns simple model classes
        cmd = mock.MagicMock()
        cmd.cli_ctx.data = {}
    
        # get_models returns a simple class that records its kwargs
        def fake_get_models(name, **kwargs):
            return type(name, (), {'init': lambda self, **kw: self.dict.update(kw)})
    
        cmd.get_models.side_effect = fake_get_models
    
        # Client whose get() raises so vault-already-exists check is skipped
        client = mock.MagicMock()
        client.get.side_effect = HttpResponseError()
    
        create_vault(
            cmd, client,
            resource_group_name='rg',
            vault_name='testvault',
            location='eastus',
            retention_days='90',
            no_self_perms=True,
        )
    
        # sdk_no_wait is called with the VaultCreateOrUpdateParameters as 'parameters'
>       mock_sdk_no_wait.assert_called_once()

src/azure-cli/azure/cli/command_modules/keyvault/tests/latest/test_keyvault_commands.py:149: 
 
 
 
 
                                   _ 

self = <MagicMock name='sdk_no_wait' id='140067147232368'>

    def assert_called_once(self):
        """assert that the mock was called only once.
        """
        if not self.call_count == 1:
            msg = ("Expected '%s' to have been called once. Called %s times.%s"
                   % (self._mock_name or 'mock',
                      self.call_count,
                      self._calls_repr()))
>           raise AssertionError(msg)
E           AssertionError: Expected 'sdk_no_wait' to have been called once. Called 0 times.

/opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/unittest/mock.py:928: AssertionError
azure/cli/command_modules/keyvault/tests/latest/test_keyvault_commands.py:114
❌3.14
Type Test Case Error Message Line
Failed test_create_vault_sets_enable_soft_delete_true self = <azure.cli.command_modules.keyvault.tests.latest.test_keyvault_commands.CreateVaultSoftDeleteTest testMethod=test_create_vault_sets_enable_soft_delete_true>
mock_sdk_no_wait = <MagicMock name='sdk_no_wait' id='140256481201184'>
mock_profile = <MagicMock name='Profile' id='140256481202192'>
mock_network = <MagicMock name='create_network_rule_set' id='140256481202864'>

    @mock.patch('azure.cli.command_modules.keyvault.custom.create_network_rule_set', return_value=None)
    @mock.patch('azure.cli.core.profile.Profile')
    @mock.patch('azure.cli.core.util.sdk_no_wait')
    def test_create_vault_sets_enable_soft_delete_true(self, mock_sdk_no_wait, mock_profile, mock_network):
        from azure.cli.command_modules.keyvault.custom import create_vault
    
        mock_profile.return_value.get_subscription.return_value = {
            'tenantId': '00000000-0000-0000-0000-000000000000'
        }
    
        # Build a minimal cmd mock that returns simple model classes
        cmd = mock.MagicMock()
        cmd.cli_ctx.data = {}
    
        # get_models returns a simple class that records its kwargs
        def fake_get_models(name, **kwargs):
            return type(name, (), {'init': lambda self, **kw: self.dict.update(kw)})
    
        cmd.get_models.side_effect = fake_get_models
    
        # Client whose get() raises so vault-already-exists check is skipped
        client = mock.MagicMock()
        client.get.side_effect = HttpResponseError()
    
        create_vault(
            cmd, client,
            resource_group_name='rg',
            vault_name='testvault',
            location='eastus',
            retention_days='90',
            no_self_perms=True,
        )
    
        # sdk_no_wait is called with the VaultCreateOrUpdateParameters as 'parameters'
>       mock_sdk_no_wait.assert_called_once()

src/azure-cli/azure/cli/command_modules/keyvault/tests/latest/test_keyvault_commands.py:149: 
 
 
 
 
                                   _ 

self = <MagicMock name='sdk_no_wait' id='140256481201184'>

    def assert_called_once(self):
        """assert that the mock was called only once.
        """
        if not self.call_count == 1:
            msg = ("Expected '%s' to have been called once. Called %s times.%s"
                   % (self._mock_name or 'mock',
                      self.call_count,
                      self._calls_repr()))
>           raise AssertionError(msg)
E           AssertionError: Expected 'sdk_no_wait' to have been called once. Called 0 times.

/opt/hostedtoolcache/Python/3.14.2/x64/lib/python3.14/unittest/mock.py:964: AssertionError
azure/cli/command_modules/keyvault/tests/latest/test_keyvault_commands.py:114
️✔️lab
️✔️latest
️✔️3.12
️✔️3.14
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.14
️✔️maps
️✔️latest
️✔️3.12
️✔️3.14
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.14
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.14
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.14
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.14
️✔️network
️✔️latest
️✔️3.12
️✔️3.14
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.14
️✔️postgresql
️✔️latest
️✔️3.12
️✔️3.14
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.14
️✔️profile
️✔️latest
️✔️3.12
️✔️3.14
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.14
️✔️redis
️✔️latest
️✔️3.12
️✔️3.14
️✔️relay
️✔️latest
️✔️3.12
️✔️3.14
️✔️resource
️✔️latest
️✔️3.12
️✔️3.14
️✔️role
️✔️latest
️✔️3.12
️✔️3.14
️✔️search
️✔️latest
️✔️3.12
️✔️3.14
️✔️security
️✔️latest
️✔️3.12
️✔️3.14
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.14
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.14
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.14
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.14
️✔️sql
️✔️latest
️✔️3.12
️✔️3.14
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.14
️✔️storage
️✔️latest
️✔️3.12
️✔️3.14
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.14
️✔️telemetry
️✔️latest
️✔️3.12
️✔️3.14
️✔️util
️✔️latest
️✔️3.12
️✔️3.14
️✔️vm
️✔️latest
️✔️3.12
️✔️3.14

@jcassanji-southworks jcassanji-southworks changed the title [KeyVault] az keyvault secret copy: Add command to copy secrets between vaults [KeyVault] az keyvault secret copy: Add command to copy secrets between vaults Feb 17, 2026
@notyashhh

Copy link
Copy Markdown
Member

@jcassanji-southworks Please check the tests are failing

@jcassanji-southworks

Copy link
Copy Markdown
Contributor Author

@jcassanji-southworks Please check the tests are failing

Done

@jcassanji-southworks

Copy link
Copy Markdown
Contributor Author

@yonzhan Could you please adjust the milestone for this feature to add it to the backlog?

@jcassanji-southworks jcassanji-southworks requested a review from a team as a code owner July 8, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

act-identity-squad Auto-Assign Auto assign by bot customer-reported Issues that are reported by GitHub users external to the Azure organization. KeyVault az keyvault

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants