diff --git a/src/azure-cli/azure/cli/command_modules/acs/_help.py b/src/azure-cli/azure/cli/command_modules/acs/_help.py index c8d4eeb32a4..833b9b5ad4b 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_help.py @@ -1112,6 +1112,9 @@ type: string short-summary: Set transit encryption type for ACNS security. long-summary: Configures pod-to-pod encryption for Cilium-based clusters. Once enabled, all traffic between Cilium managed pods will be encrypted when it leaves the node boundary. Valid values are "WireGuard" and "None". When creating a cluster, this option must be used together with "--enable-acns"; when updating a cluster, it can be used on its own to modify the transit encryption type for an existing ACNS-enabled cluster. + - name: --enable-high-log-scale-mode + type: bool + short-summary: Enable High Log Scale Mode for Container Logs. Auto-enabled when --enable-container-network-logs is specified. - name: --nrg-lockdown-restriction-level type: string short-summary: Restriction level on the managed node resource group. diff --git a/src/azure-cli/azure/cli/command_modules/acs/_helpers.py b/src/azure-cli/azure/cli/command_modules/acs/_helpers.py index 13c9c66b4f0..4a41bcb356e 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_helpers.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_helpers.py @@ -28,6 +28,30 @@ ManagedCluster = TypeVar("ManagedCluster") +def get_monitoring_addon_key(addon_profiles, monitoring_addon_name): + """Return the canonical key for the monitoring addon, normalizing non-standard casing. + + The API response may return the monitoring addon key in any casing (e.g. + "omsagent", "omsAgent", "oMSaGent"). This helper performs a + case-insensitive lookup and, when a non-standard key is found, re-keys + addon_profiles in-place so that subsequent code always uses the canonical + ``monitoring_addon_name`` (lowercase) form. + """ + if addon_profiles is None: + return monitoring_addon_name + # Exact match on the canonical lowercase name – preferred form. + if monitoring_addon_name in addon_profiles: + return monitoring_addon_name + # Case-insensitive fallback: catch any casing the server may return. + target_lower = monitoring_addon_name.lower() + for key in list(addon_profiles): + if key.lower() == target_lower: + # Normalize: move the profile to the canonical key. + addon_profiles[monitoring_addon_name] = addon_profiles.pop(key) + return monitoring_addon_name + return monitoring_addon_name + + def format_parameter_name_to_option_name(parameter_name: str) -> str: """Convert a name in parameter format to option format. diff --git a/src/azure-cli/azure/cli/command_modules/acs/_params.py b/src/azure-cli/azure/cli/command_modules/acs/_params.py index c999e4c581f..1901346e5a3 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -690,6 +690,8 @@ def load_arguments(self, _): help="Set the datapath acceleration mode for Azure Container Networking Solution (ACNS). Valid values are 'BpfVeth' and 'None'." ) c.argument('acns_transit_encryption_type', arg_type=get_enum_type(transit_encryption_types)) + # monitoring addons + c.argument('enable_high_log_scale_mode', arg_type=get_three_state_flag()) # private cluster parameters c.argument('enable_apiserver_vnet_integration', action='store_true') c.argument('apiserver_subnet_id', validator=validate_apiserver_subnet_id) diff --git a/src/azure-cli/azure/cli/command_modules/acs/custom.py b/src/azure-cli/azure/cli/command_modules/acs/custom.py index afb442031aa..3d4aed5d91a 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/custom.py +++ b/src/azure-cli/azure/cli/command_modules/acs/custom.py @@ -80,7 +80,7 @@ CONST_VIRTUAL_MACHINES, ) from azure.cli.command_modules.acs._polling import RunCommandLocationPolling -from azure.cli.command_modules.acs._helpers import get_snapshot_by_snapshot_id, check_is_private_link_cluster, build_etag_kwargs +from azure.cli.command_modules.acs._helpers import get_snapshot_by_snapshot_id, get_monitoring_addon_key, check_is_private_link_cluster, build_etag_kwargs from azure.cli.command_modules.acs._resourcegroup import get_rg_location from azure.cli.command_modules.acs.managednamespace import aks_managed_namespace_add, aks_managed_namespace_update from azure.cli.command_modules.acs._validators import extract_comma_separated_string @@ -1168,6 +1168,8 @@ def aks_update( disable_container_network_logs=None, acns_datapath_acceleration_mode=None, acns_transit_encryption_type=None, + # monitoring addons + enable_high_log_scale_mode=None, # network isoalted cluster bootstrap_artifact_source=None, bootstrap_container_registry_resource_id=None, @@ -1516,15 +1518,18 @@ def _remove_nulls(managed_clusters): def aks_disable_addons(cmd, client, resource_group_name, name, addons, no_wait=False): instance = client.get(resource_group_name, name) subscription_id = get_subscription_id(cmd.cli_ctx) + monitoring_addon_key = get_monitoring_addon_key( + instance.addon_profiles, CONST_MONITORING_ADDON_NAME + ) try: - if addons == "monitoring" and CONST_MONITORING_ADDON_NAME in instance.addon_profiles and \ - instance.addon_profiles[CONST_MONITORING_ADDON_NAME].enabled and \ - CONST_MONITORING_USING_AAD_MSI_AUTH in instance.addon_profiles[CONST_MONITORING_ADDON_NAME].config and \ - str(instance.addon_profiles[CONST_MONITORING_ADDON_NAME].config[CONST_MONITORING_USING_AAD_MSI_AUTH]).lower() == 'true': + if addons == "monitoring" and monitoring_addon_key in instance.addon_profiles and \ + instance.addon_profiles[monitoring_addon_key].enabled and \ + CONST_MONITORING_USING_AAD_MSI_AUTH in instance.addon_profiles[monitoring_addon_key].config and \ + str(instance.addon_profiles[monitoring_addon_key].config[CONST_MONITORING_USING_AAD_MSI_AUTH]).lower() == 'true': # remove the DCR association because otherwise the DCR can't be deleted ensure_container_insights_for_monitoring( cmd, - instance.addon_profiles[CONST_MONITORING_ADDON_NAME], + instance.addon_profiles[monitoring_addon_key], subscription_id, resource_group_name, name, @@ -1614,12 +1619,20 @@ def aks_enable_addons(cmd, client, resource_group_name, name, addons, if need_pull_for_result: if enable_monitoring: - if CONST_MONITORING_USING_AAD_MSI_AUTH in instance.addon_profiles[CONST_MONITORING_ADDON_NAME].config and \ - str(instance.addon_profiles[CONST_MONITORING_ADDON_NAME].config[CONST_MONITORING_USING_AAD_MSI_AUTH]).lower() == 'true': + monitoring_addon_key = get_monitoring_addon_key( + instance.addon_profiles, CONST_MONITORING_ADDON_NAME + ) + if CONST_MONITORING_USING_AAD_MSI_AUTH in instance.addon_profiles[monitoring_addon_key].config and \ + str(instance.addon_profiles[monitoring_addon_key].config[CONST_MONITORING_USING_AAD_MSI_AUTH]).lower() == 'true': if msi_auth: + # Auto-enable HLSM when CNL is active and HLSM not explicitly set + if enable_high_log_scale_mode is None and \ + (instance.addon_profiles[monitoring_addon_key].config or {}).get( + "enableRetinaNetworkFlags", "").lower() == "true": + enable_high_log_scale_mode = True # create a Data Collection Rule (DCR) and associate it with the cluster ensure_container_insights_for_monitoring( - cmd, instance.addon_profiles[CONST_MONITORING_ADDON_NAME], + cmd, instance.addon_profiles[monitoring_addon_key], subscription_id, resource_group_name, name, @@ -1650,7 +1663,7 @@ def aks_enable_addons(cmd, client, resource_group_name, name, addons, raise ArgumentUsageError( "--ampls-resource-id supported only in MSI auth mode.") ensure_container_insights_for_monitoring( - cmd, instance.addon_profiles[CONST_MONITORING_ADDON_NAME], subscription_id, resource_group_name, name, instance.location, aad_route=False) + cmd, instance.addon_profiles[monitoring_addon_key], subscription_id, resource_group_name, name, instance.location, aad_route=False) # adding a wait here since we rely on the result for role assignment result = LongRunningOperation(cmd.cli_ctx)( @@ -4078,8 +4091,11 @@ def is_monitoring_addon_enabled(addons, instance): break addon_profiles = instance.addon_profiles or {} - monitoring_addon_enabled = is_monitoring_addon and CONST_MONITORING_ADDON_NAME in addon_profiles and addon_profiles[ - CONST_MONITORING_ADDON_NAME].enabled + monitoring_addon_key = get_monitoring_addon_key( + addon_profiles, CONST_MONITORING_ADDON_NAME + ) + monitoring_addon_enabled = is_monitoring_addon and monitoring_addon_key in addon_profiles and addon_profiles[ + monitoring_addon_key].enabled except Exception as ex: # pylint: disable=broad-except logger.debug("failed to check monitoring addon enabled: %s", ex) return monitoring_addon_enabled diff --git a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py index fa1794e4edc..21de7adc88c 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py @@ -63,6 +63,7 @@ check_is_apiserver_vnet_integration_cluster, check_is_private_cluster, format_parameter_name_to_option_name, + get_monitoring_addon_key, get_user_assigned_identity_by_resource_id, get_shared_control_plane_identity, map_azure_error_to_cli_error, @@ -160,6 +161,14 @@ ManagedClusterIngressProfileNginx = TypeVar("ManagedClusterIngressProfileNginx") ServiceMeshProfile = TypeVar("ServiceMeshProfile") + +def _get_monitoring_addon_key_from_consts(addon_profiles, addon_consts): + """Thin wrapper around get_monitoring_addon_key that unpacks addon_consts dict.""" + return get_monitoring_addon_key( + addon_profiles, + addon_consts.get("CONST_MONITORING_ADDON_NAME"), + ) + # TODO # 1. remove enable_rbac related implementation # 2. add validation for all/some of the parameters involved in the getter of outbound_type/enable_addons @@ -2642,10 +2651,12 @@ def get_container_network_logs(self, mc: ManagedCluster) -> Union[bool, None]: monitoring_via_enable_addons = enable_addons and "monitoring" in enable_addons # Check if monitoring is already enabled on the cluster + addon_consts = self.get_addon_consts() + monitoring_addon_key = _get_monitoring_addon_key_from_consts(mc.addon_profiles, addon_consts) monitoring_on_cluster = ( mc.addon_profiles and - mc.addon_profiles.get("omsagent") and - mc.addon_profiles["omsagent"].enabled + mc.addon_profiles.get(monitoring_addon_key) and + mc.addon_profiles[monitoring_addon_key].enabled ) # Check if ACNS is being enabled or already enabled @@ -3115,11 +3126,16 @@ def get_enable_msi_auth_for_monitoring(self) -> Union[bool, None]: """ # determine the value of constants addon_consts = self.get_addon_consts() - CONST_MONITORING_ADDON_NAME = addon_consts.get("CONST_MONITORING_ADDON_NAME") CONST_MONITORING_USING_AAD_MSI_AUTH = addon_consts.get("CONST_MONITORING_USING_AAD_MSI_AUTH") # read the original value passed by the command enable_msi_auth_for_monitoring = self.raw_param.get("enable_msi_auth_for_monitoring") + + # Use helper to find the correct monitoring addon key (handles omsagent/omsAgent variants) + monitoring_addon_key = _get_monitoring_addon_key_from_consts( + self.mc.addon_profiles if self.mc else None, addon_consts + ) + if ( self.mc and self.mc.service_principal_profile and @@ -3130,14 +3146,14 @@ def get_enable_msi_auth_for_monitoring(self) -> Union[bool, None]: if ( self.mc and self.mc.addon_profiles and - CONST_MONITORING_ADDON_NAME in self.mc.addon_profiles and + monitoring_addon_key in self.mc.addon_profiles and self.mc.addon_profiles.get( - CONST_MONITORING_ADDON_NAME + monitoring_addon_key ).config.get(CONST_MONITORING_USING_AAD_MSI_AUTH) is not None ): enable_msi_auth_for_monitoring = ( safe_lower( - self.mc.addon_profiles.get(CONST_MONITORING_ADDON_NAME).config.get( + self.mc.addon_profiles.get(monitoring_addon_key).config.get( CONST_MONITORING_USING_AAD_MSI_AUTH ) ) == "true" @@ -7563,10 +7579,10 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: elif self.context.raw_param.get("enable_addons") is not None: # Create the DCR Association here addon_consts = self.context.get_addon_consts() - CONST_MONITORING_ADDON_NAME = addon_consts.get("CONST_MONITORING_ADDON_NAME") + monitoring_addon_key = _get_monitoring_addon_key_from_consts(cluster.addon_profiles, addon_consts) self.context.external_functions.ensure_container_insights_for_monitoring( self.cmd, - cluster.addon_profiles[CONST_MONITORING_ADDON_NAME], + cluster.addon_profiles[monitoring_addon_key], self.context.get_subscription_id(), self.context.get_resource_group_name(), self.context.get_name(), @@ -7854,7 +7870,8 @@ def check_raw_parameters(self): self.context.get_load_balancer_idle_timeout() is None and self.context.get_load_balancer_outbound_ports() is None and self.context.get_nat_gateway_managed_outbound_ip_count() is None and - self.context.get_nat_gateway_idle_timeout() is None + self.context.get_nat_gateway_idle_timeout() is None and + self.context.raw_param.get("enable_high_log_scale_mode") is None ) if not is_changed and is_default: @@ -8411,22 +8428,63 @@ def update_monitoring_profile_flow_logs(self, mc: ManagedCluster) -> ManagedClus """ self._ensure_mc(mc) + addon_consts = self.context.get_addon_consts() + CONST_MONITORING_USING_AAD_MSI_AUTH = addon_consts.get("CONST_MONITORING_USING_AAD_MSI_AUTH") + monitoring_addon_key = _get_monitoring_addon_key_from_consts(mc.addon_profiles, addon_consts) + + enable_high_log_scale_mode = self.context.get_enable_high_log_scale_mode() + enable_cnl = self.context.raw_param.get("enable_container_network_logs") + # Trigger validation for high log scale mode when container network logs are enabled. # This ensures proper error messages are raised before cluster update if the user # explicitly disables high log scale mode while enabling container network logs. - if self.context.raw_param.get("enable_container_network_logs"): + if enable_cnl: self.context.get_enable_high_log_scale_mode() + # Validate HLSM on the update path + if enable_high_log_scale_mode is True and not enable_cnl: + # HLSM requires monitoring addon with MSI auth to be enabled + monitoring_addon_profile = mc.addon_profiles.get(monitoring_addon_key) if mc.addon_profiles else None + if ( + not monitoring_addon_profile or + not monitoring_addon_profile.enabled or + safe_lower( + (monitoring_addon_profile.config or {}).get(CONST_MONITORING_USING_AAD_MSI_AUTH) + ) != "true" + ): + raise RequiredArgumentMissingError( + "--enable-high-log-scale-mode requires the monitoring addon to be enabled with MSI auth " + "(useAADAuth=true). Please enable the monitoring addon with --enable-addons monitoring first." + ) + + if enable_high_log_scale_mode is False: + # Check if CNL is already enabled on the cluster — cannot disable HLSM while CNL is active + monitoring_addon_profile = mc.addon_profiles.get(monitoring_addon_key) if mc.addon_profiles else None + if monitoring_addon_profile and monitoring_addon_profile.config: + existing_cnl = safe_lower( + monitoring_addon_profile.config.get("enableRetinaNetworkFlags") + ) + if existing_cnl == "true": + raise MutuallyExclusiveArgumentError( + "Cannot disable --enable-high-log-scale-mode while container network logs are enabled. " + "Please disable container network logs first with --disable-container-network-logs." + ) + container_network_logs_enabled = self.context.get_container_network_logs(mc) if container_network_logs_enabled is not None: if mc.addon_profiles: - addon_consts = self.context.get_addon_consts() - CONST_MONITORING_ADDON_NAME = addon_consts.get("CONST_MONITORING_ADDON_NAME") - monitoring_addon_profile = mc.addon_profiles.get(CONST_MONITORING_ADDON_NAME) + monitoring_addon_profile = mc.addon_profiles.get(monitoring_addon_key) if monitoring_addon_profile: config = monitoring_addon_profile.config or {} config["enableRetinaNetworkFlags"] = str(container_network_logs_enabled) - mc.addon_profiles[CONST_MONITORING_ADDON_NAME].config = config + mc.addon_profiles[monitoring_addon_key].config = config + + # When CNL or HLSM flags are provided, mark that monitoring postprocessing is needed + # so the DCR gets updated with the correct streams + if container_network_logs_enabled is not None or enable_high_log_scale_mode is not None: + self.context.set_intermediate( + "monitoring_addon_postprocessing_required", True, overwrite_exists=True + ) return mc def update_http_proxy_config(self, mc: ManagedCluster) -> ManagedCluster: @@ -8592,9 +8650,6 @@ def update_addon_profiles(self, mc: ManagedCluster) -> ManagedCluster: # determine the value of constants addon_consts = self.context.get_addon_consts() - CONST_MONITORING_ADDON_NAME = addon_consts.get( - "CONST_MONITORING_ADDON_NAME" - ) CONST_INGRESS_APPGW_ADDON_NAME = addon_consts.get( "CONST_INGRESS_APPGW_ADDON_NAME" ) @@ -8607,9 +8662,10 @@ def update_addon_profiles(self, mc: ManagedCluster) -> ManagedCluster: azure_keyvault_secrets_provider_addon_profile = None if mc.addon_profiles is not None: + monitoring_addon_key = _get_monitoring_addon_key_from_consts(mc.addon_profiles, addon_consts) monitoring_addon_enabled = ( - CONST_MONITORING_ADDON_NAME in mc.addon_profiles and - mc.addon_profiles[CONST_MONITORING_ADDON_NAME].enabled + monitoring_addon_key in mc.addon_profiles and + mc.addon_profiles[monitoring_addon_key].enabled ) ingress_appgw_addon_enabled = ( CONST_INGRESS_APPGW_ADDON_NAME in mc.addon_profiles and @@ -9810,6 +9866,9 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: from azure.cli.command_modules.acs._consts import CONST_AZURE_KEYVAULT_SECRETS_PROVIDER_ADDON_NAME # some addons require post cluster creation role assigment monitoring_addon_enabled = self.context.get_intermediate("monitoring_addon_enabled", default_value=False) + monitoring_addon_postprocessing_required = self.context.get_intermediate( + "monitoring_addon_postprocessing_required", default_value=False + ) ingress_appgw_addon_enabled = self.context.get_intermediate("ingress_appgw_addon_enabled", default_value=False) virtual_node_addon_enabled = self.context.get_intermediate("virtual_node_addon_enabled", default_value=False) enable_managed_identity = check_is_msi_cluster(mc) @@ -9827,6 +9886,7 @@ def check_is_postprocessing_required(self, mc: ManagedCluster) -> bool: # pylint: disable=too-many-boolean-expressions if ( monitoring_addon_enabled or + monitoring_addon_postprocessing_required or ingress_appgw_addon_enabled or virtual_node_addon_enabled or (enable_managed_identity and attach_acr) or @@ -9853,6 +9913,9 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: """ # monitoring addon monitoring_addon_enabled = self.context.get_intermediate("monitoring_addon_enabled", default_value=False) + monitoring_addon_postprocessing_required = self.context.get_intermediate( + "monitoring_addon_postprocessing_required", default_value=False + ) if monitoring_addon_enabled: enable_msi_auth_for_monitoring = self.context.get_enable_msi_auth_for_monitoring() if not enable_msi_auth_for_monitoring: @@ -9872,23 +9935,22 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: self.context.external_functions.add_monitoring_role_assignment( cluster, cluster_resource_id, self.cmd ) - elif ( - self.context.raw_param.get("enable_addons") is not None - ): - # Create the DCR Association here + if ( + enable_msi_auth_for_monitoring and self.context.raw_param.get("enable_addons") is not None + ) or monitoring_addon_postprocessing_required: addon_consts = self.context.get_addon_consts() - CONST_MONITORING_ADDON_NAME = addon_consts.get("CONST_MONITORING_ADDON_NAME") + monitoring_addon_key = _get_monitoring_addon_key_from_consts(cluster.addon_profiles, addon_consts) self.context.external_functions.ensure_container_insights_for_monitoring( self.cmd, - cluster.addon_profiles[CONST_MONITORING_ADDON_NAME], + cluster.addon_profiles[monitoring_addon_key], self.context.get_subscription_id(), self.context.get_resource_group_name(), self.context.get_name(), self.context.get_location(), remove_monitoring=False, - aad_route=self.context.get_enable_msi_auth_for_monitoring(), - create_dcr=False, - create_dcra=True, + aad_route=True, + create_dcr=monitoring_addon_postprocessing_required, + create_dcra=enable_msi_auth_for_monitoring, enable_syslog=self.context.get_enable_syslog(), data_collection_settings=self.context.get_data_collection_settings(), is_private_cluster=self.context.get_enable_private_cluster(), 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 63094d2808b..3214f0d5bf1 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 @@ -7884,6 +7884,11 @@ def test_aks_update_with_azuremonitormetrics(self, resource_group, resource_grou self.check('azureMonitorProfile.metrics.enabled', True), ]) + # wait for cluster to fully settle before issuing next update + self.cmd('aks wait --resource-group={resource_group} --name={name} --updated --interval 30 --timeout 600') + if self.is_live or self.in_recording: + time.sleep(60) + # update: disable-azure-monitor-metrics update_cmd = 'aks update --resource-group={resource_group} --name={name} --yes --output=json ' \ '--disable-azure-monitor-metrics' @@ -9094,8 +9099,22 @@ def create_new_cluster_with_monitoring_aad_auth(self, resource_group, resource_g self.check('properties.provisioningState', f'Succeeded') ]) - # make sure monitoring can be smoothly disabled - self.cmd(f'aks disable-addons -a monitoring -g={resource_group} -n={aks_name}') + # wait for any in-progress cluster operation to finish before disabling + self.cmd(f'aks wait -g {resource_group} -n {aks_name} --updated --interval 30 --timeout 600') + if self.is_live or self.in_recording: + time.sleep(60) + + # make sure monitoring can be smoothly disabled, retry on 409 (in-progress addon operation) + for attempt in range(6): + try: + self.cmd(f'aks disable-addons -a monitoring -g={resource_group} -n={aks_name}') + break + except Exception: + if attempt < 5: + if self.is_live or self.in_recording: + time.sleep(60) + else: + raise # delete self.cmd(f'aks delete -g {resource_group} -n {aks_name} --yes --no-wait', checks=[self.is_empty()]) @@ -9186,8 +9205,22 @@ def enable_monitoring_existing_cluster_aad_auth(self, resource_group, resource_g self.check('properties.dataCollectionRuleId', f'{dcr_resource_id}') ]) - # make sure monitoring can be smoothly disabled - self.cmd(f'aks disable-addons -a monitoring -g={resource_group} -n={aks_name}') + # wait for any in-progress cluster operation to finish before disabling + self.cmd(f'aks wait -g {resource_group} -n {aks_name} --updated --interval 30 --timeout 600') + if self.is_live or self.in_recording: + time.sleep(60) + + # make sure monitoring can be smoothly disabled, retry on 409 (in-progress addon operation) + for attempt in range(6): + try: + self.cmd(f'aks disable-addons -a monitoring -g={resource_group} -n={aks_name}') + break + except Exception: + if attempt < 5: + if self.is_live or self.in_recording: + time.sleep(60) + else: + raise # delete self.cmd(f'aks delete -g {resource_group} -n {aks_name} --yes --no-wait', checks=[self.is_empty()]) @@ -9238,9 +9271,22 @@ def test_aks_create_with_monitoring_legacy_auth(self, resource_group, resource_g except Exception as err: pass # this is expected + # wait for any in-progress cluster operation to finish before disabling + self.cmd(f'aks wait -g {resource_group} -n {aks_name} --updated --interval 30 --timeout 600') + if self.is_live or self.in_recording: + time.sleep(60) - # make sure monitoring can be smoothly disabled - self.cmd(f'aks disable-addons -a monitoring -g={resource_group} -n={aks_name}') + # make sure monitoring can be smoothly disabled, retry on 409 (in-progress addon operation) + for attempt in range(6): + try: + self.cmd(f'aks disable-addons -a monitoring -g={resource_group} -n={aks_name}') + break + except Exception: + if attempt < 5: + if self.is_live or self.in_recording: + time.sleep(60) + else: + raise # delete self.cmd(f'aks delete -g {resource_group} -n {aks_name} --yes --no-wait', checks=[self.is_empty()]) @@ -13323,6 +13369,46 @@ def test_aks_create_acns_with_flow_logs( checks=[ self.check("provisioningState", "Succeeded"), self.check("networkProfile.advancedNetworking.observability.enabled", True), + self.check("addonProfiles.omsagent.enabled", True), + self.check("addonProfiles.omsagent.config.enableRetinaNetworkFlags", "True"), + ], + ) + + # update: disable container network logs + disable_cnl_cmd = ( + "aks update --resource-group={resource_group} --name={name} " + "--disable-container-network-logs " + ) + self.cmd( + disable_cnl_cmd, + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("addonProfiles.omsagent.enabled", True), + self.check("addonProfiles.omsagent.config.enableRetinaNetworkFlags", "False"), + ], + ) + + # update: enable high log scale mode independently via aks update + self.cmd( + "aks update --resource-group={resource_group} --name={name} " + "--enable-high-log-scale-mode ", + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("addonProfiles.omsagent.enabled", True), + ], + ) + + # update: re-enable container network logs (should auto-enable HLSM) + enable_cnl_cmd = ( + "aks update --resource-group={resource_group} --name={name} " + "--enable-container-network-logs " + ) + self.cmd( + enable_cnl_cmd, + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("addonProfiles.omsagent.enabled", True), + self.check("addonProfiles.omsagent.config.enableRetinaNetworkFlags", "True"), ], ) diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_custom.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_custom.py index df6c68a8b96..4844bb7ca3c 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_custom.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_custom.py @@ -17,6 +17,7 @@ CONST_HTTP_APPLICATION_ROUTING_ADDON_NAME, CONST_KUBE_DASHBOARD_ADDON_NAME, CONST_MONITORING_ADDON_NAME, + CONST_MONITORING_USING_AAD_MSI_AUTH, ) from azure.cli.command_modules.acs.addonconfiguration import ( ensure_default_log_analytics_workspace_for_monitoring, @@ -24,7 +25,9 @@ from azure.cli.command_modules.acs.custom import ( _get_command_context, _update_addons, + aks_enable_addons, aks_stop, + is_monitoring_addon_enabled, k8s_install_kubectl, k8s_install_kubelogin, merge_kubernetes_configurations, @@ -45,6 +48,7 @@ ) from azure.cli.core.util import CLIError from azure.cli.core.profiles import ResourceType +from azure.core.exceptions import HttpResponseError from azure.mgmt.containerservice.models import ( ManagedClusterAddonProfile, ) @@ -612,6 +616,78 @@ def test_update_addons(self, rg_def, get_resource_groups_client, get_resources_c addon_profile = instance.addon_profiles['ingressApplicationGateway'] self.assertFalse(addon_profile.enabled) + # monitoring enable with camelCase addon key does NOT preserve enableRetinaNetworkFlags + instance = mock.MagicMock() + instance.addon_profiles = { + "omsAgent": ManagedClusterAddonProfile( + enabled=False, + config={ + 'logAnalyticsWorkspaceResourceID': '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/ws', + CONST_MONITORING_USING_AAD_MSI_AUTH: 'true', + 'enableRetinaNetworkFlags': 'True', + }, + ), + } + instance = _update_addons(MockCmd(self.cli), instance, '00000000-0000-0000-0000-000000000000', + 'clitest000001', 'clitest000001', 'monitoring', enable=True) + monitoring_profile = instance.addon_profiles[CONST_MONITORING_ADDON_NAME] + self.assertTrue(monitoring_profile.enabled) + self.assertIsNone(monitoring_profile.config.get('enableRetinaNetworkFlags')) + + # monitoring disable sets config to None + instance = mock.MagicMock() + instance.addon_profiles = { + CONST_MONITORING_ADDON_NAME: ManagedClusterAddonProfile( + enabled=True, + config={ + 'logAnalyticsWorkspaceResourceID': '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/ws', + CONST_MONITORING_USING_AAD_MSI_AUTH: 'true', + 'enableRetinaNetworkFlags': 'True', + }, + ), + } + instance = _update_addons(MockCmd(self.cli), instance, '00000000-0000-0000-0000-000000000000', + 'clitest000001', 'clitest000001', 'monitoring', enable=False) + monitoring_profile = instance.addon_profiles[CONST_MONITORING_ADDON_NAME] + self.assertFalse(monitoring_profile.enabled) + self.assertIsNone(monitoring_profile.config) + + # monitoring disable without CNL also sets config to None + instance = mock.MagicMock() + instance.addon_profiles = { + CONST_MONITORING_ADDON_NAME: ManagedClusterAddonProfile( + enabled=True, + config={ + 'logAnalyticsWorkspaceResourceID': '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/ws', + CONST_MONITORING_USING_AAD_MSI_AUTH: 'true', + }, + ), + } + instance = _update_addons(MockCmd(self.cli), instance, '00000000-0000-0000-0000-000000000000', + 'clitest000001', 'clitest000001', 'monitoring', enable=False) + monitoring_profile = instance.addon_profiles[CONST_MONITORING_ADDON_NAME] + self.assertFalse(monitoring_profile.enabled) + self.assertIsNone(monitoring_profile.config) + + # monitoring disable with camelCase key (omsAgent) sets config to None + instance = mock.MagicMock() + instance.addon_profiles = { + "omsAgent": ManagedClusterAddonProfile( + enabled=True, + config={ + 'logAnalyticsWorkspaceResourceID': '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/ws', + CONST_MONITORING_USING_AAD_MSI_AUTH: 'true', + 'enableRetinaNetworkFlags': 'True', + }, + ), + } + # The addon key normalization in _update_addons remaps camelCase to lowercase + instance = _update_addons(MockCmd(self.cli), instance, '00000000-0000-0000-0000-000000000000', + 'clitest000001', 'clitest000001', 'monitoring', enable=False) + monitoring_profile = instance.addon_profiles[CONST_MONITORING_ADDON_NAME] + self.assertFalse(monitoring_profile.enabled) + self.assertIsNone(monitoring_profile.config) + @mock.patch('azure.cli.command_modules.acs.custom._urlretrieve') @mock.patch('azure.cli.command_modules.acs.custom.logger') def test_k8s_install_kubectl_emit_warnings(self, logger_mock, mock_url_retrieve): @@ -1172,5 +1248,177 @@ def test_unknown_delos_region_defaults_to_deloscloudgermanycentral(self, mock_ge self.assertIn(f'DefaultWorkspace-{subscription_id}-DELOSC', result) +class TestIsMonitoringAddonEnabled(unittest.TestCase): + """Tests for the is_monitoring_addon_enabled helper in custom.py.""" + + def test_monitoring_enabled_with_lowercase_key(self): + instance = mock.Mock() + instance.addon_profiles = { + CONST_MONITORING_ADDON_NAME: ManagedClusterAddonProfile(enabled=True, config={}), + } + self.assertTrue(is_monitoring_addon_enabled("monitoring", instance)) + + def test_monitoring_enabled_with_camelcase_key(self): + instance = mock.Mock() + instance.addon_profiles = { + "omsAgent": ManagedClusterAddonProfile(enabled=True, config={}), + } + self.assertTrue(is_monitoring_addon_enabled("monitoring", instance)) + + def test_monitoring_disabled_with_camelcase_key(self): + instance = mock.Mock() + instance.addon_profiles = { + "omsAgent": ManagedClusterAddonProfile(enabled=False, config={}), + } + self.assertFalse(is_monitoring_addon_enabled("monitoring", instance)) + + def test_no_monitoring_addon_at_all(self): + instance = mock.Mock() + instance.addon_profiles = {} + self.assertFalse(is_monitoring_addon_enabled("monitoring", instance)) + + def test_non_monitoring_addon(self): + instance = mock.Mock() + instance.addon_profiles = { + CONST_MONITORING_ADDON_NAME: ManagedClusterAddonProfile(enabled=True, config={}), + } + self.assertFalse(is_monitoring_addon_enabled("http_application_routing", instance)) + + +class TestAksEnableAddonsAutoHLSM(unittest.TestCase): + """Tests for auto-detection of HLSM when CNL is active in aks_enable_addons.""" + + def setUp(self): + self.cli = MockCLI() + self.cmd = MockCmd(self.cli) + + def _build_instance(self, cnl_flag=None, addon_key=CONST_MONITORING_ADDON_NAME): + """Build a mock cluster instance with monitoring addon.""" + config = { + 'logAnalyticsWorkspaceResourceID': '/subscriptions/sub/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/ws', + CONST_MONITORING_USING_AAD_MSI_AUTH: 'true', + } + if cnl_flag is not None: + config['enableRetinaNetworkFlags'] = cnl_flag + instance = mock.MagicMock() + instance.addon_profiles = { + addon_key: ManagedClusterAddonProfile(enabled=True, config=config), + } + instance.service_principal_profile.client_id = "msi" + instance.api_server_access_profile = None + instance.location = "eastus" + return instance + + @mock.patch("azure.cli.command_modules.acs.custom.ensure_container_insights_for_monitoring") + @mock.patch("azure.cli.command_modules.acs.custom.LongRunningOperation") + @mock.patch("azure.cli.command_modules.acs.custom._update_addons") + @mock.patch("azure.cli.command_modules.acs.custom.get_subscription_id", return_value="00000000-0000-0000-0000-000000000000") + def test_hlsm_auto_enabled_when_cnl_active(self, _mock_sub, mock_update, mock_lro, mock_ensure): + """When CNL is active and HLSM not set, HLSM should auto-enable.""" + instance = self._build_instance(cnl_flag="True") + mock_update.return_value = instance + mock_lro.return_value = lambda x: instance + client = mock.Mock() + client.get.return_value = instance + + aks_enable_addons(self.cmd, client, "rg", "cluster", "monitoring") + + mock_ensure.assert_called_once() + _, kwargs = mock_ensure.call_args + self.assertTrue(kwargs.get("enable_high_log_scale_mode")) + + @mock.patch("azure.cli.command_modules.acs.custom.ensure_container_insights_for_monitoring") + @mock.patch("azure.cli.command_modules.acs.custom.LongRunningOperation") + @mock.patch("azure.cli.command_modules.acs.custom._update_addons") + @mock.patch("azure.cli.command_modules.acs.custom.get_subscription_id", return_value="00000000-0000-0000-0000-000000000000") + def test_hlsm_not_auto_enabled_when_cnl_inactive(self, _mock_sub, mock_update, mock_lro, mock_ensure): + """When CNL is not active and HLSM not set, HLSM should remain None.""" + instance = self._build_instance(cnl_flag=None) + mock_update.return_value = instance + mock_lro.return_value = lambda x: instance + client = mock.Mock() + client.get.return_value = instance + + aks_enable_addons(self.cmd, client, "rg", "cluster", "monitoring") + + mock_ensure.assert_called_once() + _, kwargs = mock_ensure.call_args + self.assertIsNone(kwargs.get("enable_high_log_scale_mode")) + + @mock.patch("azure.cli.command_modules.acs.custom.ensure_container_insights_for_monitoring") + @mock.patch("azure.cli.command_modules.acs.custom.LongRunningOperation") + @mock.patch("azure.cli.command_modules.acs.custom._update_addons") + @mock.patch("azure.cli.command_modules.acs.custom.get_subscription_id", return_value="00000000-0000-0000-0000-000000000000") + def test_hlsm_explicit_true_not_overridden(self, _mock_sub, mock_update, mock_lro, mock_ensure): + """When HLSM is explicitly True, auto-detection should not change it.""" + instance = self._build_instance(cnl_flag="True") + mock_update.return_value = instance + mock_lro.return_value = lambda x: instance + client = mock.Mock() + client.get.return_value = instance + + aks_enable_addons(self.cmd, client, "rg", "cluster", "monitoring", + enable_high_log_scale_mode=True) + + mock_ensure.assert_called_once() + _, kwargs = mock_ensure.call_args + self.assertTrue(kwargs.get("enable_high_log_scale_mode")) + + @mock.patch("azure.cli.command_modules.acs.custom.ensure_container_insights_for_monitoring") + @mock.patch("azure.cli.command_modules.acs.custom.LongRunningOperation") + @mock.patch("azure.cli.command_modules.acs.custom._update_addons") + @mock.patch("azure.cli.command_modules.acs.custom.get_subscription_id", return_value="00000000-0000-0000-0000-000000000000") + def test_hlsm_explicit_false_not_overridden_by_cnl(self, _mock_sub, mock_update, mock_lro, mock_ensure): + """When HLSM is explicitly False, auto-detection should not override even with CNL active.""" + instance = self._build_instance(cnl_flag="True") + mock_update.return_value = instance + mock_lro.return_value = lambda x: instance + client = mock.Mock() + client.get.return_value = instance + + aks_enable_addons(self.cmd, client, "rg", "cluster", "monitoring", + enable_high_log_scale_mode=False) + + mock_ensure.assert_called_once() + _, kwargs = mock_ensure.call_args + self.assertFalse(kwargs.get("enable_high_log_scale_mode")) + + @mock.patch("azure.cli.command_modules.acs.custom.ensure_container_insights_for_monitoring") + @mock.patch("azure.cli.command_modules.acs.custom.LongRunningOperation") + @mock.patch("azure.cli.command_modules.acs.custom._update_addons") + @mock.patch("azure.cli.command_modules.acs.custom.get_subscription_id", return_value="00000000-0000-0000-0000-000000000000") + def test_hlsm_auto_enabled_with_cnl_lowercase_true(self, _mock_sub, mock_update, mock_lro, mock_ensure): + """CNL flag value 'true' (lowercase) should also trigger auto-HLSM.""" + instance = self._build_instance(cnl_flag="true") + mock_update.return_value = instance + mock_lro.return_value = lambda x: instance + client = mock.Mock() + client.get.return_value = instance + + aks_enable_addons(self.cmd, client, "rg", "cluster", "monitoring") + + mock_ensure.assert_called_once() + _, kwargs = mock_ensure.call_args + self.assertTrue(kwargs.get("enable_high_log_scale_mode")) + + @mock.patch("azure.cli.command_modules.acs.custom.ensure_container_insights_for_monitoring") + @mock.patch("azure.cli.command_modules.acs.custom.LongRunningOperation") + @mock.patch("azure.cli.command_modules.acs.custom._update_addons") + @mock.patch("azure.cli.command_modules.acs.custom.get_subscription_id", return_value="00000000-0000-0000-0000-000000000000") + def test_hlsm_not_auto_enabled_when_cnl_false(self, _mock_sub, mock_update, mock_lro, mock_ensure): + """When CNL flag is 'false', HLSM should not auto-enable.""" + instance = self._build_instance(cnl_flag="false") + mock_update.return_value = instance + mock_lro.return_value = lambda x: instance + client = mock.Mock() + client.get.return_value = instance + + aks_enable_addons(self.cmd, client, "rg", "cluster", "monitoring") + + mock_ensure.assert_called_once() + _, kwargs = mock_ensure.call_args + self.assertIsNone(kwargs.get("enable_high_log_scale_mode")) + + if __name__ == "__main__": unittest.main() diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_helpers.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_helpers.py index dccfe1edec8..f5211a1b94d 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_helpers.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_helpers.py @@ -13,6 +13,7 @@ check_is_private_cluster, check_is_private_link_cluster, format_parameter_name_to_option_name, + get_monitoring_addon_key, get_property_from_dict_or_object, get_snapshot, get_snapshot_by_snapshot_id, @@ -347,5 +348,47 @@ def test_get_user_assigned_identity(self): get_user_assigned_identity("mock_cli_ctx", "mock_sub_id", "mock_rg", "mock_identity_name") +class TestGetMonitoringAddonKey(unittest.TestCase): + """Tests for the shared get_monitoring_addon_key helper.""" + + def test_returns_default_when_addon_profiles_is_none(self): + result = get_monitoring_addon_key(None, "omsagent") + self.assertEqual(result, "omsagent") + + def test_returns_lowercase_key_when_present(self): + addon_profiles = {"omsagent": object()} + result = get_monitoring_addon_key(addon_profiles, "omsagent") + self.assertEqual(result, "omsagent") + + def test_normalizes_camelcase_key(self): + profile = object() + addon_profiles = {"omsAgent": profile} + result = get_monitoring_addon_key(addon_profiles, "omsagent") + self.assertEqual(result, "omsagent") + self.assertIn("omsagent", addon_profiles) + self.assertNotIn("omsAgent", addon_profiles) + + def test_prefers_lowercase_when_both_present(self): + addon_profiles = {"omsagent": object(), "omsAgent": object()} + result = get_monitoring_addon_key(addon_profiles, "omsagent") + self.assertEqual(result, "omsagent") + + def test_returns_default_when_key_not_present(self): + addon_profiles = {"someOtherAddon": object()} + result = get_monitoring_addon_key(addon_profiles, "omsagent") + self.assertEqual(result, "omsagent") + + def test_normalizes_nonstandard_casing(self): + """A key like 'oMSaGent' should be re-keyed to the canonical form.""" + profile = object() + addon_profiles = {"oMSaGent": profile} + result = get_monitoring_addon_key(addon_profiles, "omsagent") + self.assertEqual(result, "omsagent") + # The dict should now contain the canonical key, not the old one. + self.assertIn("omsagent", addon_profiles) + self.assertNotIn("oMSaGent", addon_profiles) + self.assertIs(addon_profiles["omsagent"], profile) + + if __name__ == "__main__": unittest.main() diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py index 0c75d9a6054..c94cc6718ec 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py @@ -2827,6 +2827,27 @@ def test_get_enable_msi_auth_for_monitoring(self): ctx_1.attach_mc(mc) self.assertEqual(ctx_1.get_enable_msi_auth_for_monitoring(), True) + # camelCase key (omsAgent) should also be resolved + ctx_2 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_msi_auth_for_monitoring": False, + } + ), + self.models, + DecoratorMode.CREATE, + ) + addon_profiles_2 = { + "omsAgent": self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, + ) + } + mc_2 = self.models.ManagedCluster(location="test_location", addon_profiles=addon_profiles_2) + ctx_2.attach_mc(mc_2) + self.assertEqual(ctx_2.get_enable_msi_auth_for_monitoring(), True) + def test_get_virtual_node_addon_os_type(self): # default ctx_1 = AKSManagedClusterContext(self.cmd, AKSManagedClusterParamDict({}), self.models, DecoratorMode.CREATE) @@ -8633,6 +8654,58 @@ def test_postprocessing_after_mc_created(self): assignee_principal_type=None, ) + # Case 5: Create with enable_high_log_scale_mode=True + CNL + # Verifies HLSM is passed through to ensure_container_insights_for_monitoring + dec_5 = AKSManagedClusterCreateDecorator( + self.cmd, + self.client, + { + "resource_group_name": "test_rg_name", + "name": "test_name", + "enable_msi_auth_for_monitoring": True, + "enable_addons": "monitoring", + "enable_container_network_logs": True, + "enable_high_log_scale_mode": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + monitoring_addon_profile_5 = self.models.ManagedClusterAddonProfile( + enabled=True, + config={}, + ) + mc_5 = self.models.ManagedCluster( + location="test_location", + addon_profiles={ + CONST_MONITORING_ADDON_NAME: monitoring_addon_profile_5, + }, + ) + dec_5.context.attach_mc(mc_5) + dec_5.context.set_intermediate("monitoring_addon_enabled", True) + mock_profile_5 = Mock(get_subscription_id=Mock(return_value="1234-5678-9012")) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.Profile", return_value=mock_profile_5 + ), patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.ensure_container_insights_for_monitoring" + ) as mock_ensure_5: + dec_5.postprocessing_after_mc_created(mc_5) + mock_ensure_5.assert_called_once_with( + self.cmd, + monitoring_addon_profile_5, + "1234-5678-9012", + "test_rg_name", + "test_name", + "test_location", + remove_monitoring=False, + aad_route=True, + create_dcr=False, + create_dcra=True, + enable_syslog=None, + data_collection_settings=None, + is_private_cluster=None, + ampls_resource_id=None, + enable_high_log_scale_mode=True, + ) + def test_put_mc(self): dec_1 = AKSManagedClusterCreateDecorator( self.cmd, @@ -12443,6 +12516,10 @@ def test_check_is_postprocessing_required(self): self.assertEqual(dec_1.check_is_postprocessing_required(mc_1), True) dec_1.context.remove_intermediate("monitoring_addon_enabled") + dec_1.context.set_intermediate("monitoring_addon_postprocessing_required", True) + self.assertEqual(dec_1.check_is_postprocessing_required(mc_1), True) + + dec_1.context.remove_intermediate("monitoring_addon_postprocessing_required") dec_1.context.set_intermediate("ingress_appgw_addon_enabled", True) self.assertEqual(dec_1.check_is_postprocessing_required(mc_1), True) @@ -12581,6 +12658,225 @@ def test_postprocessing_after_mc_created(self): assignee_principal_type=None, ) + # Case 5: Update with HLSM=True via monitoring_addon_postprocessing_required + # Verifies enable_high_log_scale_mode=True is passed and create_dcr=True for DCR update + dec_5 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "resource_group_name": "test_rg_name", + "name": "test_name", + "enable_msi_auth_for_monitoring": True, + "enable_high_log_scale_mode": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + monitoring_addon_profile_5 = self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, + ) + mc_5 = self.models.ManagedCluster( + location="test_location", + addon_profiles={ + CONST_MONITORING_ADDON_NAME: monitoring_addon_profile_5, + }, + ) + dec_5.context.attach_mc(mc_5) + dec_5.context.set_intermediate("monitoring_addon_enabled", True) + dec_5.context.set_intermediate("monitoring_addon_postprocessing_required", True) + mock_profile_5 = Mock(get_subscription_id=Mock(return_value="1234-5678-9012")) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.Profile", return_value=mock_profile_5 + ), patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.ensure_container_insights_for_monitoring" + ) as mock_ensure_5: + dec_5.postprocessing_after_mc_created(mc_5) + mock_ensure_5.assert_called_once_with( + self.cmd, + monitoring_addon_profile_5, + "1234-5678-9012", + "test_rg_name", + "test_name", + "test_location", + remove_monitoring=False, + aad_route=True, + create_dcr=True, + create_dcra=True, + enable_syslog=None, + data_collection_settings=None, + is_private_cluster=None, + ampls_resource_id=None, + enable_high_log_scale_mode=True, + ) + + # Case 6: Update postprocessing with camelCase addon key (omsAgent) + dec_6 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "resource_group_name": "test_rg_name", + "name": "test_name", + "enable_msi_auth_for_monitoring": True, + "enable_high_log_scale_mode": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + monitoring_addon_profile_6 = self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, + ) + mc_6 = self.models.ManagedCluster( + location="test_location", + addon_profiles={ + "omsAgent": monitoring_addon_profile_6, + }, + ) + dec_6.context.attach_mc(mc_6) + dec_6.context.set_intermediate("monitoring_addon_enabled", True) + dec_6.context.set_intermediate("monitoring_addon_postprocessing_required", True) + mock_profile_6 = Mock(get_subscription_id=Mock(return_value="1234-5678-9012")) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.Profile", return_value=mock_profile_6 + ), patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.ensure_container_insights_for_monitoring" + ) as mock_ensure_6: + dec_6.postprocessing_after_mc_created(mc_6) + mock_ensure_6.assert_called_once_with( + self.cmd, + monitoring_addon_profile_6, + "1234-5678-9012", + "test_rg_name", + "test_name", + "test_location", + remove_monitoring=False, + aad_route=True, + create_dcr=True, + create_dcra=True, + enable_syslog=None, + data_collection_settings=None, + is_private_cluster=None, + ampls_resource_id=None, + enable_high_log_scale_mode=True, + ) + + # Case 7: Update with CNL enabled on an MSI cluster where monitoring was already enabled. + # In the update flow, update_addon_profiles sets monitoring_addon_enabled=True + # (because the addon is already present and enabled on the cluster). + # get_enable_msi_auth_for_monitoring() returns False for MSI clusters where + # service_principal_profile.client_id="msi", so the code enters the + # "if not enable_msi_auth_for_monitoring:" branch. The fix ensures that when + # monitoring_addon_postprocessing_required=True, the DCR is still updated + # with aad_route=True inside that branch. + dec_7 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "resource_group_name": "test_rg_name", + "name": "test_name", + "enable_msi_auth_for_monitoring": False, + "enable_container_network_logs": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + monitoring_addon_profile_7 = self.models.ManagedClusterAddonProfile( + enabled=True, + config={ + CONST_MONITORING_USING_AAD_MSI_AUTH: "true", + "enableRetinaNetworkFlags": "True", + }, + ) + mc_7 = self.models.ManagedCluster( + location="test_location", + addon_profiles={ + CONST_MONITORING_ADDON_NAME: monitoring_addon_profile_7, + }, + service_principal_profile=self.models.ManagedClusterServicePrincipalProfile( + client_id="msi" + ), + ) + dec_7.context.attach_mc(mc_7) + # monitoring_addon_enabled is True — set by update_addon_profiles because addon already exists + dec_7.context.set_intermediate("monitoring_addon_enabled", True) + dec_7.context.set_intermediate("monitoring_addon_postprocessing_required", True) + mock_profile_7 = Mock(get_subscription_id=Mock(return_value="1234-5678-9012")) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.Profile", return_value=mock_profile_7 + ), patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.ensure_container_insights_for_monitoring" + ) as mock_ensure_7: + dec_7.postprocessing_after_mc_created(mc_7) + mock_ensure_7.assert_called_once_with( + self.cmd, + monitoring_addon_profile_7, + "1234-5678-9012", + "test_rg_name", + "test_name", + "test_location", + remove_monitoring=False, + aad_route=True, + create_dcr=True, + create_dcra=False, + enable_syslog=None, + data_collection_settings=None, + is_private_cluster=None, + ampls_resource_id=None, + enable_high_log_scale_mode=True, + ) + + # Case 8: Update with HLSM-only on an MSI cluster where monitoring was already enabled + dec_8 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "resource_group_name": "test_rg_name", + "name": "test_name", + "enable_msi_auth_for_monitoring": False, + "enable_high_log_scale_mode": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + monitoring_addon_profile_8 = self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, + ) + mc_8 = self.models.ManagedCluster( + location="test_location", + addon_profiles={ + CONST_MONITORING_ADDON_NAME: monitoring_addon_profile_8, + }, + service_principal_profile=self.models.ManagedClusterServicePrincipalProfile( + client_id="msi" + ), + ) + dec_8.context.attach_mc(mc_8) + # monitoring_addon_enabled is True — set by update_addon_profiles because addon already exists + dec_8.context.set_intermediate("monitoring_addon_enabled", True) + dec_8.context.set_intermediate("monitoring_addon_postprocessing_required", True) + mock_profile_8 = Mock(get_subscription_id=Mock(return_value="1234-5678-9012")) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.Profile", return_value=mock_profile_8 + ), patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.ensure_container_insights_for_monitoring" + ) as mock_ensure_8: + dec_8.postprocessing_after_mc_created(mc_8) + mock_ensure_8.assert_called_once_with( + self.cmd, + monitoring_addon_profile_8, + "1234-5678-9012", + "test_rg_name", + "test_name", + "test_location", + remove_monitoring=False, + aad_route=True, + create_dcr=True, + create_dcra=False, + enable_syslog=None, + data_collection_settings=None, + is_private_cluster=None, + ampls_resource_id=None, + enable_high_log_scale_mode=True, + ) + def test_put_mc(self): dec_1 = AKSManagedClusterUpdateDecorator( self.cmd, @@ -14827,6 +15123,306 @@ def test_enable_container_network_logs(self): ): dec_8.set_up_addon_profiles(mc_8) + # Case 9: UPDATE - enable HLSM only (no CNL), monitoring with MSI auth enabled + dec_9 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_high_log_scale_mode": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_9 = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + network_plugin="azure", + network_plugin_mode="overlay", + network_dataplane="cilium", + advanced_networking=self.models.AdvancedNetworking( + enabled=True, + ), + ), + addon_profiles={ + "omsagent": self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, + ) + }, + ) + dec_9.context.attach_mc(mc_9) + dec_mc_9 = dec_9.update_monitoring_profile_flow_logs(mc_9) + # HLSM should be enabled but CNL remains unset — no enableRetinaNetworkFlags change + # The monitoring_addon_postprocessing_required intermediate should be set + self.assertTrue( + dec_9.context.get_intermediate("monitoring_addon_postprocessing_required") + ) + # Verify HLSM is resolved to True + self.assertEqual(dec_9.context.get_enable_high_log_scale_mode(), True) + # Verify CNL flag was NOT added to addon config (HLSM alone doesn't set it) + self.assertNotIn( + "enableRetinaNetworkFlags", + dec_mc_9.addon_profiles["omsagent"].config or {}, + ) + + # Case 10: UPDATE - disable HLSM while CNL is active -> should ERROR + dec_10 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_high_log_scale_mode": False, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_10 = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + network_plugin="azure", + network_plugin_mode="overlay", + network_dataplane="cilium", + advanced_networking=self.models.AdvancedNetworking( + enabled=True, + ), + ), + addon_profiles={ + "omsagent": self.models.ManagedClusterAddonProfile( + enabled=True, + config={ + CONST_MONITORING_USING_AAD_MSI_AUTH: "true", + "enableRetinaNetworkFlags": "True", + }, + ) + }, + ) + dec_10.context.attach_mc(mc_10) + with self.assertRaises(MutuallyExclusiveArgumentError): + dec_10.update_monitoring_profile_flow_logs(mc_10) + + # Case 11: UPDATE - enable CNL + HLSM=true together + dec_11 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_container_network_logs": True, + "enable_high_log_scale_mode": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_11 = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + network_plugin="azure", + network_plugin_mode="overlay", + network_dataplane="cilium", + advanced_networking=self.models.AdvancedNetworking( + enabled=True, + ), + ), + addon_profiles={ + "omsagent": self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, + ) + }, + ) + dec_11.context.attach_mc(mc_11) + dec_mc_11 = dec_11.update_monitoring_profile_flow_logs(mc_11) + self.assertEqual( + dec_mc_11.addon_profiles["omsagent"].config["enableRetinaNetworkFlags"], + "True", + ) + self.assertTrue( + dec_11.context.get_intermediate("monitoring_addon_postprocessing_required") + ) + # Verify HLSM is resolved to True + self.assertEqual(dec_11.context.get_enable_high_log_scale_mode(), True) + + # Case 12: UPDATE - enable HLSM without monitoring addon -> should ERROR + dec_12 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_high_log_scale_mode": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_12 = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + network_plugin="azure", + network_plugin_mode="overlay", + network_dataplane="cilium", + advanced_networking=self.models.AdvancedNetworking( + enabled=True, + ), + ), + ) + dec_12.context.attach_mc(mc_12) + with self.assertRaises(RequiredArgumentMissingError): + dec_12.update_monitoring_profile_flow_logs(mc_12) + + # Case 13: UPDATE - enable HLSM without MSI auth -> should ERROR + dec_13 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_high_log_scale_mode": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_13 = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + network_plugin="azure", + network_plugin_mode="overlay", + network_dataplane="cilium", + advanced_networking=self.models.AdvancedNetworking( + enabled=True, + ), + ), + addon_profiles={ + "omsagent": self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "false"}, + ) + }, + ) + dec_13.context.attach_mc(mc_13) + with self.assertRaises(RequiredArgumentMissingError): + dec_13.update_monitoring_profile_flow_logs(mc_13) + + # Case 14: UPDATE - enable CNL + HLSM=false -> should ERROR + dec_14 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_container_network_logs": True, + "enable_high_log_scale_mode": False, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_14 = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + network_plugin="azure", + network_plugin_mode="overlay", + network_dataplane="cilium", + advanced_networking=self.models.AdvancedNetworking( + enabled=True, + ), + ), + addon_profiles={ + "omsagent": self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, + ) + }, + ) + dec_14.context.attach_mc(mc_14) + with self.assertRaises(MutuallyExclusiveArgumentError): + dec_14.update_monitoring_profile_flow_logs(mc_14) + + # Case 15: UPDATE - enable HLSM with camelCase key (omsAgent) + dec_15 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_high_log_scale_mode": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_15 = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + network_plugin="azure", + network_plugin_mode="overlay", + network_dataplane="cilium", + advanced_networking=self.models.AdvancedNetworking( + enabled=True, + ), + ), + addon_profiles={ + "omsAgent": self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, + ) + }, + ) + dec_15.context.attach_mc(mc_15) + dec_mc_15 = dec_15.update_monitoring_profile_flow_logs(mc_15) + self.assertTrue( + dec_15.context.get_intermediate("monitoring_addon_postprocessing_required") + ) + self.assertEqual(dec_15.context.get_enable_high_log_scale_mode(), True) + + # Case 16: UPDATE - enable CNL with camelCase key (omsAgent) + dec_16 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_container_network_logs": True, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_16 = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + network_plugin="azure", + network_plugin_mode="overlay", + network_dataplane="cilium", + advanced_networking=self.models.AdvancedNetworking( + enabled=True, + ), + ), + addon_profiles={ + "omsAgent": self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, + ) + }, + ) + dec_16.context.attach_mc(mc_16) + dec_mc_16 = dec_16.update_monitoring_profile_flow_logs(mc_16) + self.assertEqual( + dec_mc_16.addon_profiles[CONST_MONITORING_ADDON_NAME].config["enableRetinaNetworkFlags"], + "True", + ) + self.assertTrue( + dec_16.context.get_intermediate("monitoring_addon_postprocessing_required") + ) + + # Case 17: UPDATE - disable HLSM with camelCase key, CNL active -> should ERROR + dec_17 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_high_log_scale_mode": False, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_17 = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + network_plugin="azure", + network_plugin_mode="overlay", + network_dataplane="cilium", + advanced_networking=self.models.AdvancedNetworking( + enabled=True, + ), + ), + addon_profiles={ + "omsAgent": self.models.ManagedClusterAddonProfile( + enabled=True, + config={ + CONST_MONITORING_USING_AAD_MSI_AUTH: "true", + "enableRetinaNetworkFlags": "True", + }, + ) + }, + ) + dec_17.context.attach_mc(mc_17) + with self.assertRaises(MutuallyExclusiveArgumentError): + dec_17.update_monitoring_profile_flow_logs(mc_17) + if __name__ == "__main__": unittest.main()