diff --git a/src/aks-preview/azext_aks_preview/_helpers.py b/src/aks-preview/azext_aks_preview/_helpers.py index e1c31d582e9..4cf4ce3e13c 100644 --- a/src/aks-preview/azext_aks_preview/_helpers.py +++ b/src/aks-preview/azext_aks_preview/_helpers.py @@ -2,6 +2,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from collections.abc import MutableMapping + import errno import os import platform @@ -44,6 +46,28 @@ allowed_extensions = ["microsoft.dataprotection.kubernetes"] +def reset_agentpool_to_name_and_mode(agentpool, mode): + """Remove all agent pool fields except the resource name and pool mode.""" + name = agentpool.name + properties = getattr(agentpool, "properties", None) + if isinstance(properties, MutableMapping): + properties.clear() + properties["mode"] = mode + agentpool.clear() + agentpool["name"] = name + agentpool["properties"] = properties + elif isinstance(agentpool, MutableMapping): + agentpool.clear() + agentpool["name"] = name + agentpool["mode"] = mode + else: + agentpool.mode = mode + for attr in list(vars(agentpool)): + if attr not in ("name", "mode") and not attr.startswith("_") and hasattr(agentpool, attr): + setattr(agentpool, attr, None) + return agentpool + + def which(binary): path_var = os.getenv('PATH') if platform.system() == 'Windows': diff --git a/src/aks-preview/azext_aks_preview/_loadbalancer.py b/src/aks-preview/azext_aks_preview/_loadbalancer.py index 22f705fa918..d9cc6528e10 100644 --- a/src/aks-preview/azext_aks_preview/_loadbalancer.py +++ b/src/aks-preview/azext_aks_preview/_loadbalancer.py @@ -172,8 +172,11 @@ def configure_load_balancer_profile( 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 else: diff --git a/src/aks-preview/azext_aks_preview/agentpool_decorator.py b/src/aks-preview/azext_aks_preview/agentpool_decorator.py index 36a611d1859..6a40b536491 100644 --- a/src/aks-preview/azext_aks_preview/agentpool_decorator.py +++ b/src/aks-preview/azext_aks_preview/agentpool_decorator.py @@ -57,6 +57,7 @@ get_nodepool_snapshot_by_snapshot_id, filter_hard_taints, process_dns_overrides, + reset_agentpool_to_name_and_mode, ) logger = get_logger(__name__) @@ -1615,14 +1616,9 @@ def set_up_managed_system_mode(self, agentpool: AgentPool) -> AgentPool: if agentpool is None: raise CLIInternalError("agentpool cannot be None for ManagedSystem mode") - # Instead of creating a new instance, modify the existing one - # Keep name and set mode to ManagedSystem - agentpool.mode = CONST_NODEPOOL_MODE_MANAGEDSYSTEM - # Make sure all other attributes are None - for attr in vars(agentpool): - if attr != 'name' and attr != 'mode' and not attr.startswith('_'): - if hasattr(agentpool, attr): - setattr(agentpool, attr, None) + agentpool = reset_agentpool_to_name_and_mode( + agentpool, CONST_NODEPOOL_MODE_MANAGEDSYSTEM + ) return agentpool @@ -1636,28 +1632,9 @@ def set_up_machines_mode(self, agentpool: AgentPool) -> AgentPool: mode = self.context.get_mode() if mode == CONST_NODEPOOL_MODE_MACHINES: - agentpool.mode = CONST_NODEPOOL_MODE_MACHINES - # Make sure all other attributes are None - # Check properties sub-model first (AgentPool), then flat fields (ManagedClusterAgentPoolProfile) - props = getattr(agentpool, 'properties', None) - rest_fields = getattr(props, '_attr_to_rest_field', None) if props is not None else None - if rest_fields is not None: - target, fields = props, rest_fields - else: - rest_fields = getattr(agentpool, '_attr_to_rest_field', None) - if rest_fields is not None and 'mode' in rest_fields: - target, fields = agentpool, rest_fields - else: - target, fields = None, None - if target is not None: - for attr in list(fields.keys()): - if attr not in ('name', 'mode'): - setattr(agentpool, attr, None) - else: - for attr in vars(agentpool): - if attr != 'name' and attr != 'mode' and not attr.startswith('_'): - if hasattr(agentpool, attr): - setattr(agentpool, attr, None) + agentpool = reset_agentpool_to_name_and_mode( + agentpool, CONST_NODEPOOL_MODE_MACHINES + ) return agentpool @@ -2119,12 +2096,9 @@ def update_agentpool_profile_preview(self, agentpools: List[AgentPool] = None) - # Check if agentpool is in ManagedSystem mode and handle special case if agentpool.mode == CONST_NODEPOOL_MODE_MANAGEDSYSTEM: - # Make sure all other attributes are None - for attr in vars(agentpool): - if attr != 'name' and attr != 'mode' and not attr.startswith('_'): - if hasattr(agentpool, attr): - setattr(agentpool, attr, None) - return agentpool + return reset_agentpool_to_name_and_mode( + agentpool, CONST_NODEPOOL_MODE_MANAGEDSYSTEM + ) # update network profile agentpool = self.update_network_profile(agentpool) diff --git a/src/aks-preview/azext_aks_preview/aks_diagnostics.py b/src/aks-preview/azext_aks_preview/aks_diagnostics.py index 2bac3d71d8b..ff007c4af1d 100644 --- a/src/aks-preview/azext_aks_preview/aks_diagnostics.py +++ b/src/aks-preview/azext_aks_preview/aks_diagnostics.py @@ -3,6 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from collections.abc import Mapping + import datetime import json import os @@ -45,6 +47,16 @@ class ClusterFeatures(Flag): WIN_HPC = auto() +def _get_storage_account_key(storage_account_keys): + keys = ( + storage_account_keys["keys"] + if isinstance(storage_account_keys, Mapping) + else storage_account_keys.keys + ) + first_key = keys[0] + return first_key["value"] if isinstance(first_key, Mapping) else first_key.value + + # pylint: disable=line-too-long def aks_kollect_cmd(cmd, # pylint: disable=too-many-statements,too-many-locals client, @@ -99,17 +111,18 @@ def aks_kollect_cmd(cmd, # pylint: disable=too-many-statements,too-many-local cmd.cli_ctx, parsed_storage_account['subscription']) storage_account_keys = storage_client.storage_accounts.list_keys(parsed_storage_account['resource_group'], storage_account_name) + storage_account_key = _get_storage_account_key(storage_account_keys) t_generate_blob_service_sas = get_sdk(cmd.cli_ctx, ResourceType.DATA_STORAGE_BLOB, '#generate_account_sas') sas_token = t_generate_blob_service_sas(storage_account_name, - storage_account_keys.keys[0].value, + storage_account_key, resource_types='sco', permission='rwdlacup', expiry=datetime.datetime.utcnow() + datetime.timedelta(days=1)) readonly_sas_token = t_generate_blob_service_sas(storage_account_name, - storage_account_keys.keys[0].value, + storage_account_key, resource_types='sco', permission='rl', expiry=datetime.datetime.utcnow() + datetime.timedelta(days=1)) diff --git a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py index 6dfff64bb88..f47040fbfe2 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -68,6 +68,7 @@ get_cluster_snapshot_by_snapshot_id, get_monitoring_addon_key, filter_hard_taints, + reset_agentpool_to_name_and_mode, ) from azext_aks_preview._loadbalancer import create_load_balancer_profile from azext_aks_preview._loadbalancer import ( @@ -6200,26 +6201,9 @@ def update_managed_system_pools(self, mc: ManagedCluster) -> ManagedCluster: # Check if agentpool is in ManagedSystem mode and handle special case if agentpool.mode != CONST_NODEPOOL_MODE_MANAGEDSYSTEM: continue - # Make sure all other attributes are None - # Check properties sub-model first (AgentPool), then flat fields (ManagedClusterAgentPoolProfile) - props = getattr(agentpool, 'properties', None) - rest_fields = getattr(props, '_attr_to_rest_field', None) if props is not None else None - if rest_fields is not None: - target, fields = props, rest_fields - else: - rest_fields = getattr(agentpool, '_attr_to_rest_field', None) - if rest_fields is not None and 'mode' in rest_fields: - target, fields = agentpool, rest_fields - else: - target, fields = None, None - if target is not None: - for attr in list(fields.keys()): - if attr not in ('name', 'mode'): - setattr(agentpool, attr, None) - else: - for attr in vars(agentpool): - if attr not in ('name', 'mode') and not attr.startswith('_') and hasattr(agentpool, attr): - setattr(agentpool, attr, None) + reset_agentpool_to_name_and_mode( + agentpool, CONST_NODEPOOL_MODE_MANAGEDSYSTEM + ) return mc def init_models(self) -> None: diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py index a93026edbcb..3f1ccc860ba 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py @@ -2054,12 +2054,12 @@ def common_set_up_managed_system_mode(self): # Verify that name is preserved self.assertEqual(dec_agentpool_1.name, original_name) - # Verify that all other properties are reset to None - for attr_name in vars(dec_agentpool_1): - if attr_name not in ['name', 'mode'] and not attr_name.startswith('_'): - attr_value = getattr(dec_agentpool_1, attr_name) - self.assertIsNone(attr_value, - f"Attribute '{attr_name}' should be None but was '{attr_value}'") + self.assertIsNone(dec_agentpool_1.count) + self.assertIsNone(dec_agentpool_1.vm_size) + self.assertIsNone(dec_agentpool_1.os_type) + self.assertIsNone(dec_agentpool_1.enable_auto_scaling) + self.assertIsNone(dec_agentpool_1.min_count) + self.assertIsNone(dec_agentpool_1.max_count) # Test case 2: mode is not ManagedSystem - should return agentpool unchanged dec_2 = AKSPreviewAgentPoolAddDecorator( @@ -2138,11 +2138,12 @@ def common_set_up_machines_mode(self): dec_agentpool_1 = dec_1.set_up_machines_mode(agentpool_1) self.assertEqual(dec_agentpool_1.name, original_name) self.assertEqual(dec_agentpool_1.mode, CONST_NODEPOOL_MODE_MACHINES) - for attr_name in vars(dec_agentpool_1): - if attr_name not in ['name', 'mode'] and not attr_name.startswith('_'): - attr_value = getattr(dec_agentpool_1, attr_name) - self.assertIsNone(attr_value, - f"Attribute '{attr_name}' should be None but was '{attr_value}'") + self.assertIsNone(dec_agentpool_1.count) + self.assertIsNone(dec_agentpool_1.vm_size) + self.assertIsNone(dec_agentpool_1.os_type) + self.assertIsNone(dec_agentpool_1.enable_auto_scaling) + self.assertIsNone(dec_agentpool_1.min_count) + self.assertIsNone(dec_agentpool_1.max_count) def common_construct_agentpool_profile_preview_with_managed_system_mode(self): """Test that construct_agentpool_profile_preview properly handles ManagedSystem mode""" diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py index 7da7157acba..7d5b0ab12ed 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py @@ -5,6 +5,7 @@ import os import pty +import random import semver import subprocess import tempfile @@ -42,6 +43,193 @@ 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 make retry-enabled cassettes incompatible + # with normal replay, so runner validation must not save them. + self.disable_recording = True + + def _save_recording_file(self, *args): + if self._retry_live_without_recording: + # Preparers temporarily override disable_recording. Mark the + # cassette clean before its context exits so none of that traffic + # can produce a partial, unreplayable 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): + if (self.is_live and + os.environ.get("AZURE_CLI_TEST_RETRY_PROVISIONING_CHECK") == "true"): + if checks is None: + normalized_checks = [] + elif isinstance(checks, (list, tuple)): + normalized_checks = checks + else: + normalized_checks = [checks] + return self._cmd_with_retry(command, normalized_checks, expect_failure) + return super().cmd(command, checks=checks, expect_failure=expect_failure) + + @staticmethod + def _is_provisioning_state_check(check): + from azure.cli.testsdk.checkers import JMESPathCheck + return ( + isinstance(check, JMESPathCheck) and + check._query == "provisioningState" and + check._expected_result == "Succeeded" + ) + + @staticmethod + def _should_retry_for_provisioning_state(result): + if not hasattr(result, "get_output_in_json"): + return False, None + data = result.get_output_in_json() + if not isinstance(data, dict) or "id" not in data: + return False, None + provisioning_state = data.get("provisioningState") + if not provisioning_state: + return False, None + if provisioning_state in {"Failed", "Canceled"}: + raise AssertionError(f"provisioningState is {provisioning_state}") + if provisioning_state == "Succeeded": + return False, None + return True, data["id"] + + @staticmethod + def _is_transient_operation_conflict(ex): + message = str(ex) + return ( + "Another operation is in progress" in message or + "Operation is not allowed because there's an in-progress" in message or + "in-progress PutExtensionAddonHandler.PUT operation" in message or + "is in Updating state, please wait for it to succeed" in message or + "ProvisioningState of extension: Updating" in message + ) + + def _execute_with_transient_conflict_retry(self, command, expect_failure): + from azure.cli.testsdk.base import execute + import logging + + max_retries = max(1, int(os.environ.get("AZURE_CLI_TEST_OPERATION_MAX_RETRIES", "10"))) + base_delay = float(os.environ.get("AZURE_CLI_TEST_OPERATION_BASE_DELAY", "5.0")) + max_delay = float(os.environ.get("AZURE_CLI_TEST_OPERATION_MAX_DELAY", "60.0")) + + for attempt in range(max_retries): + try: + return execute(self.cli_ctx, command, expect_failure=expect_failure) + except (HttpResponseError, CLIError) as ex: + if ( + expect_failure or + not self._is_transient_operation_conflict(ex) or + attempt == max_retries - 1 + ): + raise + delay = min(base_delay * (2 ** attempt), max_delay) + random.uniform(0, 1) + logging.warning( + "AKS operation is still in progress; retrying command in %.1f seconds (%d/%d)", + delay, + attempt + 1, + max_retries, + ) + time.sleep(delay) + + raise AssertionError("unreachable") + + def _refetch_settled_aks_result(self, resource_id, fallback_result): + from azure.cli.testsdk.base import execute + + resource_parts = resource_id.strip("/").split("/") + normalized_parts = [part.lower() for part in resource_parts] + try: + resource_group = resource_parts[normalized_parts.index("resourcegroups") + 1] + cluster_index = normalized_parts.index("managedclusters") + cluster_name = resource_parts[cluster_index + 1] + except (ValueError, IndexError): + return fallback_result + + remaining_parts = normalized_parts[cluster_index + 2:] + if not remaining_parts: + show_command = f"aks show --resource-group {resource_group} --name {cluster_name}" + elif len(remaining_parts) == 2 and remaining_parts[0] == "agentpools": + try: + nodepool_name = resource_parts[normalized_parts.index("agentpools") + 1] + except IndexError: + return fallback_result + show_command = ( + f"aks nodepool show --resource-group {resource_group} " + f"--cluster-name {cluster_name} --name {nodepool_name}" + ) + else: + return fallback_result + + return execute(self.cli_ctx, show_command, expect_failure=False) + + def _cmd_with_retry(self, command, checks, expect_failure): + from azure.cli.testsdk.base import execute + import logging + + command = self._apply_kwargs(command) + result = self._execute_with_transient_conflict_retry(command, expect_failure) + + provisioning_checks = [c for c in checks if self._is_provisioning_state_check(c)] + other_checks = [c for c in checks if not self._is_provisioning_state_check(c)] + + if provisioning_checks: + should_retry, resource_id = self._should_retry_for_provisioning_state(result) + if should_retry: + initial_data = result.get_output_in_json() + initial_etag = initial_data.get("etag") + last_seen_etag = initial_etag + max_retries = max(1, int(os.environ.get("AZURE_CLI_TEST_PROVISIONING_MAX_RETRIES", "10"))) + base_delay = float(os.environ.get("AZURE_CLI_TEST_PROVISIONING_BASE_DELAY", "2.0")) + max_delay = float(os.environ.get("AZURE_CLI_TEST_PROVISIONING_MAX_DELAY", "60.0")) + + for attempt in range(max_retries): + delay = min(base_delay * (2 ** attempt), max_delay) + random.uniform(0, 1) + time.sleep(delay) + poll_result = execute( + self.cli_ctx, + f"resource show --ids {resource_id}", + expect_failure=False, + ) + poll_data = poll_result.get_output_in_json() + poll_properties = poll_data.get("properties") or {} + current_provisioning_state = ( + poll_data.get("provisioningState") or + poll_properties.get("provisioningState") + ) + current_etag = poll_data.get("etag") + + if current_etag and last_seen_etag and current_etag != last_seen_etag: + logging.warning("ETag changed during polling (external modification detected)") + last_seen_etag = current_etag + + if current_provisioning_state == "Succeeded": + result = self._refetch_settled_aks_result(resource_id, result) + break + if current_provisioning_state in {"Failed", "Canceled"}: + raise AssertionError( + f"provisioningState reached terminal failure: {current_provisioning_state}" + ) + else: + final_etag_msg = "" + if initial_etag and last_seen_etag: + final_etag_msg = f" (initial etag: {initial_etag}, final: {last_seen_etag})" + raise TimeoutError( + f"provisioningState did not reach 'Succeeded' after {max_retries} retries. " + f"Final state: {current_provisioning_state}{final_etag_msg}" + ) + result.assert_with_checks(provisioning_checks) + + if other_checks: + result.assert_with_checks(other_checks) + + return result def _create_log_analytics_workspace(self, resource_group_location): workspace_name = self.create_random_name("clilaw", 16) @@ -62,7 +250,10 @@ def _create_log_analytics_workspace(self, resource_group_location): def _get_versions(self, location): """Return the previous and current Kubernetes minor release versions, such as ("1.11.6", "1.12.4").""" supported_versions = self.cmd( - "az aks get-versions -l {} --query 'values[*].patchVersions.keys(@)[]'".format(location) + "az aks get-versions -l {} " + "--query \"values[?contains(capabilities.supportPlan, 'KubernetesOfficial')].patchVersions.keys(@)[]\"".format( + location + ) ).get_output_in_json() sorted_supported_versions = sorted(supported_versions, key=version_to_tuple, reverse=True) upgrade_version = sorted_supported_versions[0] @@ -86,6 +277,20 @@ def _get_version_in_range( return version return "" + def _get_version_at_least(self, location: str, min_version: str) -> str: + """Return the latest community-supported version at or above the minimum.""" + versions = self.cmd( + "az aks get-versions -l {} " + "--query \"values[?contains(capabilities.supportPlan, 'KubernetesOfficial')].patchVersions.keys(@)[]\"".format( + location + ) + ).get_output_in_json() + versions = sorted(versions, key=version_to_tuple, reverse=True) + minimum = version_to_tuple(min_version) + return next( + version for version in versions if version_to_tuple(version) >= minimum + ) + def _get_lts_version(self, location): """Return the latest LTS version in the given location.""" data = self.cmd( @@ -3961,7 +4166,6 @@ def test_aks_nodepool_add_with_localdns_config(self, resource_group, resource_gr create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -3970,7 +4174,6 @@ def test_aks_nodepool_add_with_localdns_config(self, resource_group, resource_gr "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={localdns_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4007,7 +4210,6 @@ def test_aks_nodepool_add_with_localdns_required_mode(self, resource_group, reso create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4016,7 +4218,6 @@ def test_aks_nodepool_add_with_localdns_required_mode(self, resource_group, reso "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={required_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4052,7 +4253,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_single_vnetdns(self, resou create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4060,7 +4260,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_single_vnetdns(self, resou "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={vnetdns_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) with self.assertRaises(HttpResponseError) as context: @@ -4093,7 +4292,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_single_kubedns(self, resou create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4101,7 +4299,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_single_kubedns(self, resou "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={kubedns_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) # This should fail because kubedns config without vnetdns should be rejected @@ -4136,7 +4333,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_with_extra_property(self, create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4145,7 +4341,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_with_extra_property(self, "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={required_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) with self.assertRaises(HttpResponseError) as context: @@ -4180,7 +4375,6 @@ def test_aks_nodepool_add_with_localdns_config_with_extra_property(self, resourc create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4189,7 +4383,6 @@ def test_aks_nodepool_add_with_localdns_config_with_extra_property(self, resourc "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={required_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4226,7 +4419,6 @@ def test_aks_nodepool_add_with_localdns_config_with_extra_property_in_dnsOverrid create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4235,7 +4427,6 @@ def test_aks_nodepool_add_with_localdns_config_with_extra_property_in_dnsOverrid "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={required_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4272,7 +4463,6 @@ def test_aks_nodepool_update_with_localdns_config(self, resource_group, resource create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4280,7 +4470,6 @@ def test_aks_nodepool_update_with_localdns_config(self, resource_group, resource add_cmd = ( "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4328,7 +4517,6 @@ def test_aks_nodepool_update_with_localdns_required_mode_to_dns_partial_puts(sel create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4337,7 +4525,6 @@ def test_aks_nodepool_update_with_localdns_required_mode_to_dns_partial_puts(sel "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={valid_dns_overrides} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4394,7 +4581,6 @@ def test_aks_nodepool_update_localdns_required_to_disabled(self, resource_group, create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4403,7 +4589,6 @@ def test_aks_nodepool_update_localdns_required_to_disabled(self, resource_group, "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={required_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4456,7 +4641,6 @@ def test_aks_nodepool_update_localdns_disabled_to_required(self, resource_group) create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4465,7 +4649,6 @@ def test_aks_nodepool_update_localdns_disabled_to_required(self, resource_group) "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={disabled_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4518,7 +4701,6 @@ def test_aks_nodepool_update_localdns_required_to_localdnsconfig_with_extra_prop create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4527,7 +4709,6 @@ def test_aks_nodepool_update_localdns_required_to_localdnsconfig_with_extra_prop "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={required_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4580,7 +4761,6 @@ def test_aks_nodepool_update_localdns_required_to_localdnsconfig_with_extra_prop create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4589,7 +4769,6 @@ def test_aks_nodepool_update_localdns_required_to_localdnsconfig_with_extra_prop "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={required_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" # k8s version > 1.33 to support localDNS ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4643,7 +4822,6 @@ def test_aks_nodepool_update_with_localdns_invalid_mode(self, resource_group, re create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4651,7 +4829,6 @@ def test_aks_nodepool_update_with_localdns_invalid_mode(self, resource_group, re "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={valid_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4694,7 +4871,6 @@ def test_aks_nodepool_update_with_localdns_empty_config(self, resource_group, re create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4702,7 +4878,6 @@ def test_aks_nodepool_update_with_localdns_empty_config(self, resource_group, re "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={valid_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4743,7 +4918,6 @@ def test_aks_nodepool_update_with_localdns_required_mode_invalid_vnetdns(self, r create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4751,7 +4925,6 @@ def test_aks_nodepool_update_with_localdns_required_mode_invalid_vnetdns(self, r "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={valid_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4793,7 +4966,6 @@ def test_aks_nodepool_update_with_localdns_required_mode_invalid_kubedns(self, r create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4801,7 +4973,6 @@ def test_aks_nodepool_update_with_localdns_required_mode_invalid_kubedns(self, r "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={valid_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4841,7 +5012,6 @@ def test_aks_nodepool_add_with_localdns_missing_mode(self, resource_group, resou create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4849,7 +5019,6 @@ def test_aks_nodepool_add_with_localdns_missing_mode(self, resource_group, resou "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={missing_mode_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) with self.assertRaises(HttpResponseError) as context: self.cmd(add_cmd) @@ -4882,7 +5051,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_empty_overrides(self, reso create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4890,7 +5058,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_empty_overrides(self, reso "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={empty_overrides_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) with self.assertRaises(Exception) as context: @@ -4922,7 +5089,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_partial_invalid(self, reso create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4930,7 +5096,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_partial_invalid(self, reso "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={partial_invalid_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) # Attempt to update nodepool with null DNS overrides - should fail @@ -4962,7 +5127,6 @@ def test_aks_nodepool_add_with_localdns_empty_mode(self, resource_group, resourc create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -4970,7 +5134,6 @@ def test_aks_nodepool_add_with_localdns_empty_mode(self, resource_group, resourc "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={empty_mode_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) with self.assertRaises(HttpResponseError) as context: self.cmd(add_cmd) @@ -5000,7 +5163,6 @@ def test_aks_nodepool_add_with_localdns_null_mode(self, resource_group, resource create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5008,7 +5170,6 @@ def test_aks_nodepool_add_with_localdns_null_mode(self, resource_group, resource "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={null_mode_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) with self.assertRaises(HttpResponseError) as context: self.cmd(add_cmd) @@ -5040,7 +5201,6 @@ def test_aks_nodepool_add_with_localdns_empty_config(self, resource_group, resou create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5048,7 +5208,6 @@ def test_aks_nodepool_add_with_localdns_empty_config(self, resource_group, resou "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={empty_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) with self.assertRaises(HttpResponseError) as context: self.cmd(add_cmd) @@ -5081,7 +5240,6 @@ def test_aks_nodepool_add_with_localdns_null_config(self, resource_group, resour create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5089,7 +5247,6 @@ def test_aks_nodepool_add_with_localdns_null_config(self, resource_group, resour "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={null_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) with self.assertRaises(InvalidArgumentValueError) as context: self.cmd(add_cmd) @@ -5121,7 +5278,6 @@ def test_aks_nodepool_add_with_localdns_invalid_mode(self, resource_group, resou create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5129,7 +5285,6 @@ def test_aks_nodepool_add_with_localdns_invalid_mode(self, resource_group, resou "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={valid_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5173,7 +5328,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_invalid_kubedns(self, reso create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5181,7 +5335,6 @@ def test_aks_nodepool_add_with_localdns_required_mode_invalid_kubedns(self, reso "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={valid_config} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5224,7 +5377,6 @@ def test_aks_nodepool_add_localdns_null_dnsOverrides(self, resource_group, resou create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5234,7 +5386,6 @@ def test_aks_nodepool_add_localdns_null_dnsOverrides(self, resource_group, resou "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={config_path} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) # Verify the error message @@ -5266,7 +5417,6 @@ def test_aks_nodepool_add_localdns_number_dnsOverrides(self, resource_group, res create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5276,7 +5426,6 @@ def test_aks_nodepool_add_localdns_number_dnsOverrides(self, resource_group, res "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 --localdns-config={config_path} " "--aks-custom-headers AKSHTTPCustomFeatures=Microsoft.ContainerService/LocalDNSPreview " - "--kubernetes-version 1.33.0" ) # Verify the error message @@ -5308,7 +5457,6 @@ def test_aks_nodepool_update_localdns_null_dnsOverrides(self, resource_group, re create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5316,7 +5464,6 @@ def test_aks_nodepool_update_localdns_null_dnsOverrides(self, resource_group, re add_cmd = ( "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5357,7 +5504,6 @@ def test_aks_nodepool_update_localdns_number_dnsOverrides(self, resource_group, create_cmd = ( "aks create --resource-group={resource_group} --name={name} " "--node-count 1 --ssh-key-value={ssh_key_value} --generate-ssh-keys " - "--kubernetes-version 1.33.0" ) self.cmd(create_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -5365,7 +5511,6 @@ def test_aks_nodepool_update_localdns_number_dnsOverrides(self, resource_group, add_cmd = ( "aks nodepool add --resource-group={resource_group} --cluster-name={name} " "--name={nodepool_name} --node-count 1 " - "--kubernetes-version 1.33.0" ) self.cmd(add_cmd, checks=[self.check("provisioningState", "Succeeded")]) @@ -13214,7 +13359,9 @@ def test_aks_create_with_kms_pmk_and_update_cmk( aks_name = self.create_random_name("cliakstest", 16) kv_name = self.create_random_name("cliakstestkv", 16) identity_name = self.create_random_name("cliakstestidentity", 24) - k8s_version = self._get_version_in_range(location=resource_group_location, min_version="1.33.0", max_version="1.34.0") + k8s_version = self._get_version_at_least( + location=resource_group_location, min_version="1.33.0" + ) self.kwargs.update( { "resource_group": resource_group, @@ -13379,7 +13526,9 @@ def test_aks_create_with_kms_pmk_and_cmk_and_disable_cmk( aks_name = self.create_random_name("cliakstest", 16) kv_name = self.create_random_name("cliakstestkv", 16) identity_name = self.create_random_name("cliakstestidentity", 24) - k8s_version = self._get_version_in_range(location=resource_group_location, min_version="1.33.0", max_version="1.34.0") + k8s_version = self._get_version_at_least( + location=resource_group_location, min_version="1.33.0" + ) self.kwargs.update( { "resource_group": resource_group, @@ -13527,7 +13676,9 @@ def test_aks_create_with_kms_pmk_and_cmk_and_disable_cmk_private( aks_name = self.create_random_name("cliakstest", 16) kv_name = self.create_random_name("cliakstestkv", 16) identity_name = self.create_random_name("cliakstestidentity", 24) - k8s_version = self._get_version_in_range(location=resource_group_location, min_version="1.33.0", max_version="1.34.0") + k8s_version = self._get_version_at_least( + location=resource_group_location, min_version="1.33.0" + ) self.kwargs.update( { "resource_group": resource_group, @@ -13695,7 +13846,9 @@ def test_aks_create_with_kms_cmk_and_disable_cmk_and_update_pmk( aks_name = self.create_random_name("cliakstest", 16) kv_name = self.create_random_name("cliakstestkv", 16) identity_name = self.create_random_name("cliakstestidentity", 24) - k8s_version = self._get_version_in_range(location=resource_group_location, min_version="1.33.0", max_version="1.34.0") + k8s_version = self._get_version_at_least( + location=resource_group_location, min_version="1.33.0" + ) self.kwargs.update( { "resource_group": resource_group, @@ -13833,7 +13986,9 @@ def test_aks_update_with_kms_pmk( self, resource_group, resource_group_location ): aks_name = self.create_random_name("cliakstest", 16) - k8s_version = self._get_version_in_range(location=resource_group_location, min_version="1.33.0", max_version="1.34.0") + k8s_version = self._get_version_at_least( + location=resource_group_location, min_version="1.33.0" + ) self.kwargs.update( { "resource_group": resource_group, diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_diagnostics.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_diagnostics.py index 4d351a568c8..7da4ce6d067 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_diagnostics.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_diagnostics.py @@ -4,6 +4,8 @@ # -------------------------------------------------------------------------------------------- import unittest +from types import SimpleNamespace + import azext_aks_preview.aks_diagnostics as commands @@ -27,5 +29,17 @@ def test_generate_container_name_not_containing_hcp(self): self.assertEqual(expected_container_name, trim_container_name) +class TestGetStorageAccountKey(unittest.TestCase): + def test_mapping_sdk_model(self): + response = {"keys": [{"value": "mapping-key"}]} + + self.assertEqual("mapping-key", commands._get_storage_account_key(response)) + + def test_attribute_sdk_model(self): + response = SimpleNamespace(keys=[SimpleNamespace(value="attribute-key")]) + + self.assertEqual("attribute-key", commands._get_storage_account_key(response)) + + if __name__ == "__main__": unittest.main() diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_provisioning_retry.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_provisioning_retry.py new file mode 100644 index 00000000000..4f13cd5b431 --- /dev/null +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_provisioning_retry.py @@ -0,0 +1,263 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import json +import os +import tempfile +import unittest +from unittest.mock import MagicMock, patch + +from azure.cli.testsdk.checkers import JMESPathCheck +from knack.util import CLIError + + +class MockExecutionResult: + def __init__(self, output_json): + self._json = output_json + self.output = json.dumps(output_json) + self.json_value = None + + def get_output_in_json(self): + return self._json + + def assert_with_checks(self, *args): + checks = [] + for each in args: + if isinstance(each, list): + checks.extend(each) + elif callable(each): + checks.append(each) + for check in checks: + check(self) + return self + + +class AKSRetryTestCase(unittest.TestCase): + def _make_instance(self): + from azext_aks_preview.tests.latest.test_aks_commands import ( + AzureKubernetesServiceScenarioTest, + ) + instance = object.__new__(AzureKubernetesServiceScenarioTest) + instance.kwargs = {} + instance._apply_kwargs = lambda command: command + instance.cli_ctx = MagicMock() + return instance + + @staticmethod + def _result(data): + return MockExecutionResult(data) + + +class TestCmdRetryDispatch(AKSRetryTestCase): + @patch.dict(os.environ, { + "AZURE_TEST_RUN_LIVE": "true", + "AZURE_CLI_TEST_RETRY_PROVISIONING_CHECK": "true", + }) + def test_retry_enabled_live_instance_disables_recording(self): + from azext_aks_preview.tests.latest.test_aks_commands import ( + AzureKubernetesServiceScenarioTest, + ) + + instance = AzureKubernetesServiceScenarioTest( + "test_aks_addon_list_available" + ) + + self.assertTrue(instance.disable_recording) + + @patch.dict(os.environ, { + "AZURE_TEST_RUN_LIVE": "true", + "AZURE_CLI_TEST_RETRY_PROVISIONING_CHECK": "true", + }) + def test_retry_enabled_live_instance_never_saves_cassette(self): + from azext_aks_preview.tests.latest.test_aks_commands import ( + AzureKubernetesServiceScenarioTest, + ) + instance = AzureKubernetesServiceScenarioTest( + "test_aks_addon_list_available" + ) + 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): + instance = self._make_instance() + instance.is_live = True + instance._cmd_with_retry = MagicMock() + + instance.cmd("aks delete", checks=None, expect_failure=False) + + instance._cmd_with_retry.assert_called_once_with("aks delete", [], False) + + +class TestProvisioningStateRetry(AKSRetryTestCase): + @patch.dict(os.environ, { + "AZURE_CLI_TEST_PROVISIONING_MAX_RETRIES": "2", + "AZURE_CLI_TEST_PROVISIONING_BASE_DELAY": "0.01", + }) + @patch("time.sleep", return_value=None) + @patch("random.uniform", return_value=0) + @patch("azure.cli.testsdk.base.execute") + def test_polls_nested_arm_state_then_refetches_native_result( + self, mock_execute, _mock_random, _mock_sleep + ): + resource_id = ( + "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.ContainerService/" + "managedClusters/cluster" + ) + initial_result = self._result({ + "id": resource_id, + "provisioningState": "Updating", + }) + settled_result = self._result({ + "id": resource_id, + "provisioningState": "Succeeded", + "feature": {"enabled": True}, + }) + mock_execute.side_effect = [ + initial_result, + self._result({"properties": {"provisioningState": "Updating"}}), + self._result({"properties": {"provisioningState": "Succeeded"}}), + ] + instance = self._make_instance() + instance._refetch_settled_aks_result = MagicMock(return_value=settled_result) + + result = instance._cmd_with_retry( + "aks update", + [ + JMESPathCheck("provisioningState", "Succeeded"), + JMESPathCheck("feature.enabled", True), + ], + False, + ) + + self.assertIs(result, settled_result) + instance._refetch_settled_aks_result.assert_called_once_with( + resource_id, initial_result + ) + + @patch.dict(os.environ, { + "AZURE_CLI_TEST_PROVISIONING_MAX_RETRIES": "2", + "AZURE_CLI_TEST_PROVISIONING_BASE_DELAY": "0.01", + }) + @patch("time.sleep", return_value=None) + @patch("random.uniform", return_value=0) + @patch("azure.cli.testsdk.base.execute") + def test_times_out_when_state_never_settles( + self, mock_execute, _mock_random, _mock_sleep + ): + poll = self._result({"properties": {"provisioningState": "Updating"}}) + mock_execute.side_effect = [ + self._result({ + "id": "/subscriptions/sub/resourceGroups/rg/providers/" + "Microsoft.ContainerService/managedClusters/cluster", + "provisioningState": "Updating", + }), + poll, + poll, + ] + + with self.assertRaises(TimeoutError): + self._make_instance()._cmd_with_retry( + "aks update", + [JMESPathCheck("provisioningState", "Succeeded")], + False, + ) + + +class TestTransientConflictRetry(AKSRetryTestCase): + @patch.dict(os.environ, { + "AZURE_CLI_TEST_OPERATION_MAX_RETRIES": "2", + "AZURE_CLI_TEST_OPERATION_BASE_DELAY": "0.01", + }) + @patch("time.sleep", return_value=None) + @patch("random.uniform", return_value=0) + @patch("azure.cli.testsdk.base.execute") + def test_retries_only_known_transient_conflicts( + self, mock_execute, _mock_random, mock_sleep + ): + messages = [ + "Operation is not allowed: Another operation is in progress.", + "Operation is not allowed because there's an in-progress update managed cluster operation", + "Operation is not allowed: in-progress PutExtensionAddonHandler.PUT operation", + "The managed cluster test is in Updating state, please wait for it to succeed.", + "ProvisioningState of extension: Updating", + ] + for message in messages: + with self.subTest(message=message): + expected = self._result({"provisioningState": "Succeeded"}) + mock_execute.reset_mock() + mock_sleep.reset_mock() + mock_execute.side_effect = [CLIError(message), expected] + + result = self._make_instance()._execute_with_transient_conflict_retry( + "aks update", False + ) + + self.assertIs(result, expected) + self.assertEqual(mock_execute.call_count, 2) + mock_sleep.assert_called_once() + + @patch.dict(os.environ, {"AZURE_CLI_TEST_OPERATION_MAX_RETRIES": "2"}) + @patch("time.sleep", return_value=None) + @patch("azure.cli.testsdk.base.execute") + def test_does_not_retry_other_errors(self, mock_execute, mock_sleep): + mock_execute.side_effect = CLIError("Invalid parameter") + + with self.assertRaisesRegex(CLIError, "Invalid parameter"): + self._make_instance()._execute_with_transient_conflict_retry( + "aks update", False + ) + + mock_execute.assert_called_once() + mock_sleep.assert_not_called() + + @patch.dict(os.environ, {"AZURE_CLI_TEST_OPERATION_MAX_RETRIES": "2"}) + @patch("time.sleep", return_value=None) + @patch("azure.cli.testsdk.base.execute") + def test_does_not_retry_expected_failure(self, mock_execute, mock_sleep): + mock_execute.side_effect = CLIError( + "Operation is not allowed: Another operation is in progress." + ) + + with self.assertRaises(CLIError): + self._make_instance()._execute_with_transient_conflict_retry( + "aks update", True + ) + + mock_execute.assert_called_once() + mock_sleep.assert_not_called() + + +class TestRefetchSettledResult(AKSRetryTestCase): + @patch("azure.cli.testsdk.base.execute") + def test_refetches_agentpool_with_native_show(self, mock_execute): + expected = self._result({"provisioningState": "Succeeded"}) + mock_execute.return_value = expected + resource_id = ( + "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.ContainerService/" + "managedClusters/cluster/agentPools/pool" + ) + instance = self._make_instance() + + result = instance._refetch_settled_aks_result(resource_id, MagicMock()) + + self.assertIs(result, expected) + mock_execute.assert_called_once_with( + instance.cli_ctx, + "aks nodepool show --resource-group rg --cluster-name cluster --name pool", + expect_failure=False, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_loadbalancer.py b/src/aks-preview/azext_aks_preview/tests/latest/test_loadbalancer.py index 76873835928..4ab38c6d124 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_loadbalancer.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_loadbalancer.py @@ -102,6 +102,25 @@ def test_configure_load_balancer_profile(self): self.assertEqual(p.idle_timeout_in_minutes, 3600) self.assertEqual(p.backend_pool_type, "nodeIP") + def test_configure_ipv6_count_defaults_ipv4_count(self): + profile = self.load_balancer_models.ManagedClusterLoadBalancerProfile() + + result = loadbalancer.configure_load_balancer_profile( + None, + 2, + None, + None, + None, + None, + None, + None, + profile, + self.load_balancer_models, + ) + + self.assertEqual(result.managed_outbound_i_ps.count, 1) + self.assertEqual(result.managed_outbound_i_ps.count_ipv6, 2) + def test_configure_load_balancer_profile_error(self): managed_outbound_ip_count = 5 managed_outbound_ipv6_count = 3