diff --git a/src/azure-cli/azure/cli/command_modules/acs/_loadbalancer.py b/src/azure-cli/azure/cli/command_modules/acs/_loadbalancer.py index 6db5e36e63d..6eeb78b45cf 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_loadbalancer.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_loadbalancer.py @@ -104,8 +104,11 @@ def configure_load_balancer_profile(managed_outbound_ip_count, managed_outbound_ "ManagedClusterLoadBalancerProfileManagedOutboundIPs" ) profile.managed_outbound_i_ps = ManagedClusterLoadBalancerProfileManagedOutboundIPs() - if managed_outbound_ip_count is not None: - profile.managed_outbound_i_ps.count = managed_outbound_ip_count + profile.managed_outbound_i_ps.count = ( + managed_outbound_ip_count + if managed_outbound_ip_count is not None + else profile.managed_outbound_i_ps.count or 1 + ) if managed_outbound_ipv6_count is not None: profile.managed_outbound_i_ps.count_ipv6 = managed_outbound_ipv6_count elif profile.managed_outbound_i_ps is not None: diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py index 3def6ae241b..a271b1c64af 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py @@ -50,15 +50,28 @@ def __init__(self, method_name): super(AzureKubernetesServiceScenarioTest, self).__init__( method_name, recording_processors=[KeyReplacer()] ) + self._retry_live_without_recording = ( + self.is_live and + os.environ.get('AZURE_CLI_TEST_RETRY_PROVISIONING_CHECK') == 'true' + ) + if self._retry_live_without_recording: + # Poll/refetch requests are incompatible with normal replay. + self.disable_recording = True + + def _save_recording_file(self, *args): + if self._retry_live_without_recording: + # Preparers can temporarily override disable_recording. + self.cassette.dirty = False + if os.path.exists(self.temp_recording_file): + os.remove(self.temp_recording_file) + return + return super()._save_recording_file(*args) def cmd(self, command, checks=None, expect_failure=False): # Live-only retry adapter: when AZURE_CLI_TEST_RETRY_PROVISIONING_CHECK # is set during a live run, retry AKS operation conflicts and poll # provisioningState until terminal so asynchronous service operations # can't fail the test on a transient conflict or stale 'Updating' body. - # Recordings made with the flag enabled must NOT be committed; the - # replay pipeline runs with the flag off and would assert against the - # initial pre-poll response. if (self.is_live and os.environ.get('AZURE_CLI_TEST_RETRY_PROVISIONING_CHECK') == 'true'): if checks is None: @@ -338,12 +351,21 @@ def _create_container_insights_workspace(self, resource_group, location): "promotionCode": "", }, } + self.kwargs["container_insights_solution"] = json.dumps(solution) self.cmd( f"resource create --id {solution_id} --api-version 2015-11-01-preview " - f"--is-full-object --properties '{json.dumps(solution)}'" + "--is-full-object --properties '{container_insights_solution}'" ) return workspace_id + def _wait_for_cluster_update(self): + if self.is_live or self.in_recording: + self.cmd( + 'aks wait --resource-group={resource_group} --name={name} ' + '--updated --interval 30 --timeout 1800', + checks=[self.is_empty()], + ) + def _get_lower_lts_version(self, location, version): """Return the highest LTS version that is lower than the given version.""" lts_versions = self._get_lts_versions(location) @@ -5255,19 +5277,23 @@ def test_aks_create_default_service_with_skip_role_assignment_msi(self, resource def test_aks_create_default_service_with_monitoring_addon_msi(self, resource_group, resource_group_location): # kwargs for string formatting aks_name = self.create_random_name('cliakstest', 16) + workspace_id = self._create_container_insights_workspace( + resource_group, resource_group_location + ) self.kwargs.update({ 'resource_group': resource_group, 'name': aks_name, 'dns_name_prefix': self.create_random_name('cliaksdns', 16), 'ssh_key_value': self.generate_ssh_keys(), 'location': resource_group_location, - 'resource_type': 'Microsoft.ContainerService/ManagedClusters' + 'resource_type': 'Microsoft.ContainerService/ManagedClusters', + 'workspace_id': workspace_id, }) # create cluster with monitoring-addon create_cmd = 'aks create --resource-group={resource_group} --name={name} --location={location} ' \ '--dns-name-prefix={dns_name_prefix} --node-count=1 --ssh-key-value={ssh_key_value} ' \ - '--enable-addons monitoring' + '--enable-addons monitoring --workspace-resource-id={workspace_id}' self.cmd(create_cmd, checks=[ self.exists('fqdn'), self.exists('nodeResourceGroup'), @@ -11403,8 +11429,7 @@ def test_aks_create_and_update_ipv6_count(self, resource_group, resource_group_l create_cmd = 'aks create --resource-group={resource_group} --name={name} --location={location} ' \ '--pod-cidr 172.126.0.0/16 --service-cidr 172.56.0.0/16 --dns-service-ip 172.56.0.10 ' \ '--pod-cidrs 172.126.0.0/16,2001:abcd:1234::/64 --service-cidrs 172.56.0.0/16,2001:ffff::/108 ' \ - '--ip-families IPv4,IPv6 --load-balancer-managed-outbound-ip-count 1 ' \ - '--load-balancer-managed-outbound-ipv6-count 2 ' \ + '--ip-families IPv4,IPv6 --load-balancer-managed-outbound-ipv6-count 2 ' \ '--network-plugin kubenet --ssh-key-value={ssh_key_value} --kubernetes-version {k8s_version} ' \ '--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/AKS-EnableDualStack' self.cmd(create_cmd, checks=[ @@ -15544,6 +15569,7 @@ def test_aks_create_autoscaler_then_update_vms_pool(self, resource_group, resour self.check('agentPoolProfiles[0].virtualMachinesProfile.scale.autoscale[0].minCount', 1), self.check('agentPoolProfiles[0].virtualMachinesProfile.scale.autoscale[0].maxCount', 3), ]) + self._wait_for_cluster_update() # add another vms nodepool with autoscaler enabled add_nodepool_cmd = 'aks nodepool add -g {resource_group} --cluster-name {name} -n {nodepool_name} ' \ @@ -15557,6 +15583,7 @@ def test_aks_create_autoscaler_then_update_vms_pool(self, resource_group, resour self.check('virtualMachinesProfile.scale.autoscale[0].minCount', 0), self.check('virtualMachinesProfile.scale.autoscale[0].maxCount', 3), ]) + self._wait_for_cluster_update() # update an existing autoscale profile using auto-scale update update_autoscale_cmd = 'aks nodepool auto-scale update -g {resource_group} --cluster-name {name} -n {nodepool_name} ' \ @@ -15569,6 +15596,7 @@ def test_aks_create_autoscaler_then_update_vms_pool(self, resource_group, resour self.check('virtualMachinesProfile.scale.autoscale[0].minCount', 1), self.check('virtualMachinesProfile.scale.autoscale[0].maxCount', 5), ]) + self._wait_for_cluster_update() # add a second autoscale profile add_autoscale_cmd = 'aks nodepool auto-scale add -g {resource_group} --cluster-name {name} -n {nodepool_name} ' \ @@ -15580,12 +15608,14 @@ def test_aks_create_autoscaler_then_update_vms_pool(self, resource_group, resour self.check('virtualMachinesProfile.scale.autoscale[1].minCount', 1), self.check('virtualMachinesProfile.scale.autoscale[1].maxCount', 3), ]) + self._wait_for_cluster_update() # delete the second autoscale profile delete_autoscale_cmd = 'aks nodepool auto-scale delete -g {resource_group} --cluster-name {name} -n {nodepool_name} ' \ '--current-node-vm-size {node_vm_size1} --yes' np = self.cmd(delete_autoscale_cmd).get_output_in_json() assert len(np["virtualMachinesProfile"]["scale"]["autoscale"]) == 1 + self._wait_for_cluster_update() # disable autoscaler (auto to manual) disable_autoscaler_cmd = 'aks nodepool update -g {resource_group} --cluster-name {name} -n {nodepool_name} ' \ @@ -15594,6 +15624,7 @@ def test_aks_create_autoscaler_then_update_vms_pool(self, resource_group, resour self.check('provisioningState', 'Succeeded'), self.check('virtualMachinesProfile.scale.manual[0].size', 'standard_d4s_v3'), ]) + self._wait_for_cluster_update() # enable autoscaler (manual to auto) enable_autoscaler_cmd = 'aks nodepool update -g {resource_group} --cluster-name {name} -n {nodepool_name} ' \ @@ -15604,6 +15635,7 @@ def test_aks_create_autoscaler_then_update_vms_pool(self, resource_group, resour self.check('virtualMachinesProfile.scale.autoscale[0].minCount', 1), self.check('virtualMachinesProfile.scale.autoscale[0].maxCount', 3), ]) + self._wait_for_cluster_update() # delete self.cmd('aks delete -g {resource_group} -n {name} --yes --no-wait', checks=[self.is_empty()]) diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_provisioning_retry.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_provisioning_retry.py index 246c72c272e..259066f94ab 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_provisioning_retry.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_provisioning_retry.py @@ -5,8 +5,9 @@ import json import os +import tempfile import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, patch, PropertyMock from azure.cli.testsdk.checkers import JMESPathCheck from knack.util import CLIError @@ -86,6 +87,42 @@ def test_list_response_returns_false(self): class TestCmdRetryDispatch(unittest.TestCase): + @patch.dict(os.environ, {'AZURE_CLI_TEST_RETRY_PROVISIONING_CHECK': 'true'}) + @patch( + 'azure.cli.testsdk.scenario_tests.config.TestConfig.record_mode', + new_callable=PropertyMock, + return_value=True, + ) + def test_retry_enabled_live_instance_disables_recording(self, _record_mode): + from azure.cli.command_modules.acs.tests.latest.test_aks_commands import ( + AzureKubernetesServiceScenarioTest, + ) + instance = AzureKubernetesServiceScenarioTest('runTest') + + self.assertTrue(instance.disable_recording) + + @patch.dict(os.environ, {'AZURE_CLI_TEST_RETRY_PROVISIONING_CHECK': 'true'}) + @patch( + 'azure.cli.testsdk.scenario_tests.config.TestConfig.record_mode', + new_callable=PropertyMock, + return_value=True, + ) + def test_retry_enabled_live_instance_never_saves_cassette(self, _record_mode): + from azure.cli.command_modules.acs.tests.latest.test_aks_commands import ( + AzureKubernetesServiceScenarioTest, + ) + instance = AzureKubernetesServiceScenarioTest('runTest') + instance.cassette = MagicMock() + instance.cassette.dirty = True + fd, temp_recording_file = tempfile.mkstemp() + os.close(fd) + instance.temp_recording_file = temp_recording_file + + instance._save_recording_file() + + self.assertFalse(instance.cassette.dirty) + self.assertFalse(os.path.exists(temp_recording_file)) + @patch.dict(os.environ, {'AZURE_CLI_TEST_RETRY_PROVISIONING_CHECK': 'true'}) def test_live_command_without_checks_uses_retry_path(self): from azure.cli.command_modules.acs.tests.latest.test_aks_commands import ( @@ -100,6 +137,69 @@ def test_live_command_without_checks_uses_retry_path(self): instance._cmd_with_retry.assert_called_once_with('aks delete', [], False) +class TestCreateContainerInsightsWorkspace(unittest.TestCase): + + def test_solution_payload_is_passed_as_registered_kwarg(self): + from azure.cli.command_modules.acs.tests.latest.test_aks_commands import ( + AzureKubernetesServiceScenarioTest, + ) + instance = object.__new__(AzureKubernetesServiceScenarioTest) + instance.kwargs = {} + instance.create_random_name = MagicMock(return_value='workspace') + workspace_result = MockExecutionResult({ + 'id': ( + '/subscriptions/sub/resourceGroups/rg/providers/' + 'Microsoft.OperationalInsights/workspaces/workspace' + ) + }) + solution_result = MockExecutionResult({}) + instance.cmd = MagicMock(side_effect=[workspace_result, solution_result]) + + workspace_id = instance._create_container_insights_workspace('rg', 'westus2') + + self.assertEqual(workspace_result.get_output_in_json()['id'], workspace_id) + self.assertEqual( + json.loads(instance.kwargs['container_insights_solution'])['location'], + 'westus2', + ) + solution_command = instance.cmd.call_args_list[1].args[0] + self.assertIn("'{container_insights_solution}'", solution_command) + self.assertNotIn('{"location"', solution_command) + + +class TestWaitForClusterUpdate(unittest.TestCase): + + @staticmethod + def _make_instance(is_live=False, in_recording=False): + from azure.cli.command_modules.acs.tests.latest.test_aks_commands import ( + AzureKubernetesServiceScenarioTest, + ) + instance = object.__new__(AzureKubernetesServiceScenarioTest) + instance.is_live = is_live + instance.in_recording = in_recording + instance.cmd = MagicMock() + instance.is_empty = MagicMock(return_value='empty-check') + return instance + + def test_replay_does_not_issue_wait_request(self): + instance = self._make_instance() + + instance._wait_for_cluster_update() + + instance.cmd.assert_not_called() + + def test_live_run_waits_for_cluster_update(self): + instance = self._make_instance(is_live=True) + + instance._wait_for_cluster_update() + + instance.cmd.assert_called_once_with( + 'aks wait --resource-group={resource_group} --name={name} ' + '--updated --interval 30 --timeout 1800', + checks=['empty-check'], + ) + + class TestCmdWithRetry(unittest.TestCase): def _make_instance(self): diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_loadbalancer.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_loadbalancer.py index 4bb62ad47d2..1753849add8 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_loadbalancer.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_loadbalancer.py @@ -71,6 +71,54 @@ def test_configure_load_balancer_profile(self): self.assertEqual(p.idle_timeout_in_minutes, 3600) self.assertEqual(p.backend_pool_type, "nodeIP") + def test_create_load_balancer_profile_defaults_ipv4_count_with_ipv6(self): + cmd = MockCmd(MockCLI()) + load_balancer_models = AKSManagedClusterModels( + cmd, ResourceType.MGMT_CONTAINERSERVICE + ).load_balancer_models + + profile = loadbalancer.create_load_balancer_profile( + None, + 2, + None, + None, + None, + None, + None, + load_balancer_models, + ) + + self.assertEqual(profile.managed_outbound_i_ps.count, 1) + self.assertEqual(profile.managed_outbound_i_ps.count_ipv6, 2) + + def test_update_load_balancer_profile_preserves_ipv4_count_with_ipv6(self): + cmd = MockCmd(MockCLI()) + load_balancer_models = AKSManagedClusterModels( + cmd, ResourceType.MGMT_CONTAINERSERVICE + ).load_balancer_models + profile = load_balancer_models.ManagedClusterLoadBalancerProfile( + managed_outbound_i_ps=( + load_balancer_models.ManagedClusterLoadBalancerProfileManagedOutboundIPs( + count=3 + ) + ) + ) + + profile = loadbalancer.update_load_balancer_profile( + None, + 2, + None, + None, + None, + None, + None, + profile, + load_balancer_models, + ) + + self.assertEqual(profile.managed_outbound_i_ps.count, 3) + self.assertEqual(profile.managed_outbound_i_ps.count_ipv6, 2) + def test_update_load_balancer_profile(self): cmd = MockCmd(MockCLI()) managed_outbound_ip_count = None