diff --git a/src/azure-cli/HISTORY.rst b/src/azure-cli/HISTORY.rst index 7bbafa33310..87412985c9f 100644 --- a/src/azure-cli/HISTORY.rst +++ b/src/azure-cli/HISTORY.rst @@ -96,6 +96,7 @@ Release History * `az postgres flexible-server create/restore/geo-restore/replica create`: Add new arguments `--federated-client-id` and `--backup-federated-client-id` to support multi-tenant application registration (#33645) * `az postgresql flexible-server maintenance-event list/show/apply-now/reschedule`: Add commands for maintenance events (#33662) +* Fix #33776: `az postgres flexible-server replica promote`: Populate `sourceServerResourceId` in the PATCH body so that planned switchover promote no longer fails with `MissingRequiredParameter` (#33777) **Resource** diff --git a/src/azure-cli/azure/cli/command_modules/postgresql/commands/replica_commands.py b/src/azure-cli/azure/cli/command_modules/postgresql/commands/replica_commands.py index 36cbd6e8eb3..11926f3cc91 100644 --- a/src/azure-cli/azure/cli/command_modules/postgresql/commands/replica_commands.py +++ b/src/azure-cli/azure/cli/command_modules/postgresql/commands/replica_commands.py @@ -187,6 +187,11 @@ def flexible_replica_promote(cmd, client, resource_group_name, name, promote_mod ) ) + # The service requires sourceServerResourceId in the PATCH body when promoting a replica. + # Populate it from the replica server's existing source server resource ID. + if server_object.source_server_resource_id: + params['properties']['sourceServerResourceId'] = server_object.source_server_resource_id + return client.begin_update(resource_group_name, name, params) diff --git a/src/azure-cli/azure/cli/command_modules/postgresql/tests/latest/test_postgres_flexible_commands_replica_promote.py b/src/azure-cli/azure/cli/command_modules/postgresql/tests/latest/test_postgres_flexible_commands_replica_promote.py new file mode 100644 index 00000000000..8fcabe9e5c8 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/postgresql/tests/latest/test_postgres_flexible_commands_replica_promote.py @@ -0,0 +1,127 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import unittest +from unittest.mock import MagicMock, patch + + +class ReplicaCommandsTest(unittest.TestCase): + """Unit tests for PostgreSQL flexible-server replica commands.""" + + def setUp(self): + self.resource_group = 'test-rg' + self.server_name = 'test-replica' + self.source_server_resource_id = ( + '/subscriptions/sub-id/resourceGroups/test-rg' + '/providers/Microsoft.DBforPostgreSQL/flexibleServers/source-server' + ) + + def _build_server_object(self, role='AsyncReplica'): + server = MagicMock() + server.replica.role = role + server.source_server_resource_id = self.source_server_resource_id + return server + + def _make_cmd_mock(self): + cmd = MagicMock() + cmd.cli_ctx = MagicMock() + return cmd + + @patch('azure.cli.command_modules.postgresql.commands.replica_commands.is_citus_cluster', return_value=False) + @patch('azure.cli.command_modules.postgresql.commands.replica_commands.validate_resource_group') + def test_flexible_replica_promote_switchover_includes_source_server_resource_id( + self, mock_validate_rg, mock_is_citus): + """Regression test for #33776: planned switchover must populate sourceServerResourceId.""" + from azure.cli.command_modules.postgresql.commands.replica_commands import flexible_replica_promote + + mock_client = MagicMock() + server_object = self._build_server_object(role='AsyncReplica') + mock_client.get.return_value = server_object + + flexible_replica_promote( + cmd=self._make_cmd_mock(), + client=mock_client, + resource_group_name=self.resource_group, + name=self.server_name, + promote_mode='switchover', + promote_option='planned', + ) + + mock_client.begin_update.assert_called_once() + # begin_update is called as positional: (resource_group, name, params) + call_args = mock_client.begin_update.call_args[0] + params = call_args[2] + + # Verify sourceServerResourceId is included in the PATCH body + self.assertEqual( + params['properties']['sourceServerResourceId'], + self.source_server_resource_id, + ) + # Verify replica role and promote settings are correct + self.assertEqual(params['properties']['replica']['role'], 'Primary') + self.assertEqual(params['properties']['replica']['promoteMode'], 'switchover') + self.assertEqual(params['properties']['replica']['promoteOption'], 'planned') + + @patch('azure.cli.command_modules.postgresql.commands.replica_commands.is_citus_cluster', return_value=False) + @patch('azure.cli.command_modules.postgresql.commands.replica_commands.validate_resource_group') + def test_flexible_replica_promote_standalone_includes_source_server_resource_id( + self, mock_validate_rg, mock_is_citus): + """Standalone promote also populates sourceServerResourceId in the PATCH body.""" + from azure.cli.command_modules.postgresql.commands.replica_commands import flexible_replica_promote + + mock_client = MagicMock() + server_object = self._build_server_object(role='AsyncReplica') + mock_client.get.return_value = server_object + + flexible_replica_promote( + cmd=self._make_cmd_mock(), + client=mock_client, + resource_group_name=self.resource_group, + name=self.server_name, + promote_mode='standalone', + promote_option='planned', + ) + + mock_client.begin_update.assert_called_once() + call_args = mock_client.begin_update.call_args[0] + params = call_args[2] + + self.assertEqual( + params['properties']['sourceServerResourceId'], + self.source_server_resource_id, + ) + self.assertEqual(params['properties']['replica']['role'], 'None') + + @patch('azure.cli.command_modules.postgresql.commands.replica_commands.is_citus_cluster', return_value=False) + @patch('azure.cli.command_modules.postgresql.commands.replica_commands.validate_resource_group') + def test_flexible_replica_promote_no_source_server_resource_id_does_not_fail( + self, mock_validate_rg, mock_is_citus): + """If source_server_resource_id is absent on the server object, no KeyError is raised.""" + from azure.cli.command_modules.postgresql.commands.replica_commands import flexible_replica_promote + + mock_client = MagicMock() + server_object = self._build_server_object(role='AsyncReplica') + server_object.source_server_resource_id = None + mock_client.get.return_value = server_object + + flexible_replica_promote( + cmd=self._make_cmd_mock(), + client=mock_client, + resource_group_name=self.resource_group, + name=self.server_name, + promote_mode='switchover', + promote_option='forced', + ) + + mock_client.begin_update.assert_called_once() + call_args = mock_client.begin_update.call_args[0] + params = call_args[2] + + # sourceServerResourceId should NOT be injected when the server has none + self.assertNotIn('sourceServerResourceId', params['properties']) + + +if __name__ == '__main__': + unittest.main()