[App Service] Fix #16111: Add az webapp deployment slot copy command (preview)#33069
[App Service] Fix #16111: Add az webapp deployment slot copy command (preview)#33069
az webapp deployment slot copy command (preview)#33069Conversation
…ommand (preview) Add new command to copy content and configuration from one deployment slot to another using the REST API slotcopy endpoint. Unlike swap, this is a one-way operation that overwrites the target slot content. - Register command with is_preview=True in commands.py - Add --slot (source) and --target-slot (destination) parameters - Implementation uses send_raw_request since SDK lacks copy_slot method - Add help text with examples - Add unit tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
️✔️AzureCLI-FullTest
|
|
Hi @seligj95, |
|
| rule | cmd_name | rule_message | suggest_message |
|---|---|---|---|
| webapp deployment slot copy | cmd webapp deployment slot copy added |
|
Thank you for your contribution! We will review the pull request and get back to you soon. |
|
The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR. Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions). pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>
|
There was a problem hiding this comment.
Pull request overview
Adds a new App Service CLI surface for copying deployment slot content/config using the underlying slotcopy ARM endpoint, exposing it as a preview command under az webapp deployment slot.
Changes:
- Registers new preview command
az webapp deployment slot copy. - Adds parameters and help content for the new command.
- Implements
copy_slot()viasend_raw_requestand adds unit tests for 200/202 and default target behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/azure-cli/azure/cli/command_modules/appservice/commands.py |
Registers the new copy slot command as preview. |
src/azure-cli/azure/cli/command_modules/appservice/_params.py |
Adds argument context for webapp deployment slot copy. |
src/azure-cli/azure/cli/command_modules/appservice/custom.py |
Implements copy_slot() calling the slotcopy REST API. |
src/azure-cli/azure/cli/command_modules/appservice/_help.py |
Adds help text and examples for the new command. |
src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py |
Adds unit tests validating basic request/response behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| url = ("/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/sites/{}" | ||
| "/slots/{}/slotcopy?api-version={}").format( | ||
| subscription_id, resource_group_name, webapp, slot, | ||
| client.DEFAULT_API_VERSION) |
There was a problem hiding this comment.
--slot production is documented as a supported source (see help examples), but this implementation always calls the slot-scoped ARM resource /sites/{name}/slots/{slot}/slotcopy. In App Service, the production slot is the site resource itself (not a slots/production child), so this will likely 404 or invoke the wrong endpoint when slot is production. Consider normalizing slot=='production' (or empty) to the production endpoint (typically /sites/{name}/slotcopy) or explicitly validating and rejecting production if the REST API only supports slot-to-slot copy.
| url = ("/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/sites/{}" | |
| "/slots/{}/slotcopy?api-version={}").format( | |
| subscription_id, resource_group_name, webapp, slot, | |
| client.DEFAULT_API_VERSION) | |
| # In App Service, the production slot is the site resource itself (no 'slots/production' child). | |
| # Use the site-level slotcopy endpoint when the source is production, otherwise use the slot-scoped endpoint. | |
| is_production_source = not slot or str(slot).lower() == 'production' | |
| base_path = ("/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/sites/{}" | |
| .format(subscription_id, resource_group_name, webapp)) | |
| if is_production_source: | |
| url = "{}/slotcopy?api-version={}".format(base_path, client.DEFAULT_API_VERSION) | |
| else: | |
| url = "{}/slots/{}/slotcopy?api-version={}".format(base_path, slot, client.DEFAULT_API_VERSION) |
| target_slot = target_slot or 'production' | ||
| subscription_id = get_subscription_id(cmd.cli_ctx) |
There was a problem hiding this comment.
The linked issue #16111’s proposed behavior states the target slot cannot be production, but this command defaults target_slot to 'production' and the help text/examples encourage copying to production. Please confirm the REST API actually allows production as a target; if it doesn't, add validation to block --target-slot production and update the default/help accordingly.
| c.argument('slot', help='the name of the source slot to copy from') | ||
| c.argument('target_slot', help="the name of the destination slot to copy to, default to 'production'") |
There was a problem hiding this comment.
Help text says "default to 'production'"; grammatically this should be "defaults to 'production'" (and similarly in other new/updated strings) to read correctly in --help output.
| az webapp deployment slot copy -g MyResourceGroup -n MyUniqueApp --slot production \\ | ||
| --target-slot staging |
There was a problem hiding this comment.
This help example uses --slot production, but elsewhere in this module production is represented by omitting --slot / passing slot=None (e.g., swap preview uses if slot is None:). Unless copy_slot explicitly handles production as a special case, this example will likely not work and should be adjusted to match the actual supported syntax/behavior.
| az webapp deployment slot copy -g MyResourceGroup -n MyUniqueApp --slot production \\ | |
| --target-slot staging | |
| az webapp deployment slot copy -g MyResourceGroup -n MyUniqueApp --target-slot staging |
| result = copy_slot(cmd_mock, 'rg1', 'myapp', 'staging', 'production') | ||
| self.assertEqual(result, {"status": "completed"}) | ||
| send_raw_request_mock.assert_called_once() | ||
| call_args = send_raw_request_mock.call_args | ||
| self.assertIn('/slotcopy', call_args[1].get('url', '') or str(call_args)) | ||
|
|
||
| @mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request', autospec=True) | ||
| @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) | ||
| def test_copy_slot_accepted(self, client_factory_mock, send_raw_request_mock): | ||
| """Test copy_slot returns None on 202 accepted.""" | ||
| client = mock.MagicMock() | ||
| client.DEFAULT_API_VERSION = '2024-04-01' | ||
| client_factory_mock.return_value = client | ||
| cmd_mock = _get_test_cmd() | ||
| cli_ctx_mock = mock.MagicMock() | ||
| cli_ctx_mock.data = {'subscription_id': 'sub1'} | ||
| cmd_mock.cli_ctx = cli_ctx_mock | ||
|
|
||
| response = mock.MagicMock() | ||
| response.status_code = 202 | ||
| response.text = '' | ||
| send_raw_request_mock.return_value = response | ||
|
|
||
| result = copy_slot(cmd_mock, 'rg1', 'myapp', 'staging') | ||
| self.assertIsNone(result) | ||
|
|
||
| @mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request', autospec=True) | ||
| @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True) | ||
| def test_copy_slot_default_target(self, client_factory_mock, send_raw_request_mock): | ||
| """Test copy_slot defaults target_slot to 'production'.""" | ||
| client = mock.MagicMock() | ||
| client.DEFAULT_API_VERSION = '2024-04-01' | ||
| client_factory_mock.return_value = client | ||
| cmd_mock = _get_test_cmd() | ||
| cli_ctx_mock = mock.MagicMock() | ||
| cli_ctx_mock.data = {'subscription_id': 'sub1'} | ||
| cmd_mock.cli_ctx = cli_ctx_mock | ||
|
|
||
| response = mock.MagicMock() | ||
| response.status_code = 200 | ||
| response.text = '{}' | ||
| response.json.return_value = {} | ||
| send_raw_request_mock.return_value = response | ||
|
|
||
| copy_slot(cmd_mock, 'rg1', 'myapp', 'staging') | ||
| call_args = send_raw_request_mock.call_args |
There was a problem hiding this comment.
Tests cover 200/202 and default target_slot, but they don't cover the documented --slot production case. Add a unit test asserting the correct URL/path behavior for a production source (or, if production source isn't supported, a test that it fails/validates accordingly) so this doesn't regress.
|
Consolidated into #33066 |
Description
Fixes #16111
Problem
The Azure REST API exposes a
slotcopyendpoint for copying content and configuration from one deployment slot to another, but no CLI command exposed it. This was requested by the App Service team.Solution
Add a new
az webapp deployment slot copycommand (marked as preview) that:slotcopyREST API endpoint directly (SDK does not yet expose this method)--slot(source) to--target-slot(destination, defaults toproduction)Usage
Changes
copycommand withis_preview=True--slotand--target-slotparameters for the copy commandcopy_slot()implementation usingsend_raw_requestto theslotcopyREST APITesting