-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[App Service] Fix #16111: Add az webapp deployment slot copy command (preview)
#33069
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -720,6 +720,9 @@ def load_arguments(self, _): | |
| c.argument('action', | ||
| help="swap types. use 'preview' to apply target slot's settings on the source slot first; use 'swap' to complete it; use 'reset' to reset the swap", | ||
| arg_type=get_enum_type(['swap', 'preview', 'reset'])) | ||
| with self.argument_context('webapp deployment slot copy') as c: | ||
| 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'") | ||
|
Comment on lines
+724
to
+725
|
||
|
|
||
| with self.argument_context('webapp deployment github-actions')as c: | ||
| c.argument('name', arg_type=webapp_name_arg_type) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5610,6 +5610,27 @@ def swap_slot(cmd, resource_group_name, webapp, slot, target_slot=None, preserve | |||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def copy_slot(cmd, resource_group_name, webapp, slot, target_slot=None): | ||||||||||||||||||||||||||||
| from azure.cli.core.commands.client_factory import get_subscription_id | ||||||||||||||||||||||||||||
| client = web_client_factory(cmd.cli_ctx) | ||||||||||||||||||||||||||||
| target_slot = target_slot or 'production' | ||||||||||||||||||||||||||||
| subscription_id = get_subscription_id(cmd.cli_ctx) | ||||||||||||||||||||||||||||
|
Comment on lines
+5616
to
+5617
|
||||||||||||||||||||||||||||
| url = ("/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Web/sites/{}" | ||||||||||||||||||||||||||||
| "/slots/{}/slotcopy?api-version={}").format( | ||||||||||||||||||||||||||||
| subscription_id, resource_group_name, webapp, slot, | ||||||||||||||||||||||||||||
| client.DEFAULT_API_VERSION) | ||||||||||||||||||||||||||||
|
Comment on lines
+5618
to
+5621
|
||||||||||||||||||||||||||||
| 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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |
| list_snapshots, | ||
| restore_snapshot, | ||
| create_managed_ssl_cert, | ||
| copy_slot, | ||
| add_github_actions, | ||
| update_app_settings, | ||
| update_application_settings_polling, | ||
|
|
@@ -644,5 +645,77 @@ def __init__(self, status_code): | |
| self.status_code = status_code | ||
|
|
||
|
|
||
| class TestCopySlot(unittest.TestCase): | ||
|
|
||
| @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_success(self, client_factory_mock, send_raw_request_mock): | ||
| """Test copy_slot sends correct REST call and returns on 200.""" | ||
| 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 = '{"status": "completed"}' | ||
| response.json.return_value = {"status": "completed"} | ||
| send_raw_request_mock.return_value = response | ||
|
|
||
| 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 | ||
|
Comment on lines
+668
to
+713
|
||
| body_arg = call_args[1].get('body', '') | ||
| import json | ||
| body = json.loads(body_arg) | ||
| self.assertEqual(body['targetSlot'], 'production') | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| unittest.main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This help example uses
--slot production, but elsewhere in this module production is represented by omitting--slot/ passingslot=None(e.g., swap preview usesif slot is None:). Unlesscopy_slotexplicitly handlesproductionas a special case, this example will likely not work and should be adjusted to match the actual supported syntax/behavior.