From 764321a5998dd87ff1dd37d4b28a17f505b3666c Mon Sep 17 00:00:00 2001 From: Liunardy <54991798+Liunardy@users.noreply.github.com> Date: Wed, 19 Aug 2026 07:16:41 +0000 Subject: [PATCH] [AKS] Fix load balancer options ignored when outbound type is not loadBalancer `az aks update --load-balancer-backend-pool-type nodeIP` was silently ignored on clusters whose outboundType is not loadBalancer (userDefinedRouting, NAT gateway, none, block). update_load_balancer_profile read the outbound type through get_outbound_type, which since 073b19f05 falls back to the value on the existing cluster. An unchanged outbound type therefore looked like a request to switch away from loadBalancer, and the whole profile was set to None. The request carried no loadBalancerProfile at all, so AKS applied nothing and returned success, leaving the cluster on nodeIPConfiguration. Read outbound_type from the raw command parameters instead, so only an outbound type given on the command line counts as a switch, and clear just the five outbound fields rather than the whole profile. Inbound settings such as backendPoolType are valid for every outbound type and are now preserved, including when requested in the same command that changes --outbound-type. When no inbound settings remain the profile is still set to None, so the request body continues to omit loadBalancerProfile rather than sending an empty object. Affected versions: 21.0.0b10 through 22.0.0b1. --- src/aks-preview/HISTORY.rst | 4 + .../managed_cluster_decorator.py | 52 +++-- .../latest/test_managed_cluster_decorator.py | 182 ++++++++++++++++++ src/aks-preview/setup.py | 2 +- 4 files changed, 220 insertions(+), 20 deletions(-) diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index 1d305f61ead..acf1efbf0c2 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -12,6 +12,10 @@ To release a new version, please select a new version number (usually plus 1 to Pending +++++++ +22.0.0b2 +++++++++ +* `az aks update`: Fix load balancer options such as `--load-balancer-backend-pool-type` being silently ignored on clusters whose `outboundType` is not `loadBalancer` (for example `userDefinedRouting` or a NAT gateway). Passing `--outbound-type` to switch away from `loadBalancer` now only drops the load balancer outbound settings instead of the whole load balancer profile, so options requested in the same command are still applied. + 22.0.0b1 ++++++++ * [BREAKING CHANGE]: `az aks bastion`: This command does not connect to Azure Bastion anymore. A new subcommand is introduced for this purpose: `az aks bastion tunnel`. The `az aks bastion` command is now used to manage Azure Bastion resources for AKS clusters. 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 6952cc5963e..1d7fbf008ba 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -7143,25 +7143,39 @@ def update_load_balancer_profile(self, mc: ManagedCluster) -> ManagedCluster: raise UnknownError( "Unexpectedly get an empty network profile in the process of updating load balancer profile." ) - outbound_type = self.context.get_outbound_type() - if outbound_type and outbound_type != CONST_OUTBOUND_TYPE_LOAD_BALANCER: - mc.network_profile.load_balancer_profile = None - else: - # In the internal function "_update_load_balancer_profile", it will check whether the provided parameters - # have been assigned, and if there are any, the corresponding profile will be modified; otherwise, it will - # remain unchanged. - mc.network_profile.load_balancer_profile = _update_load_balancer_profile( - managed_outbound_ip_count=self.context.get_load_balancer_managed_outbound_ip_count(), - managed_outbound_ipv6_count=self.context.get_load_balancer_managed_outbound_ipv6_count(), - outbound_ips=self.context.get_load_balancer_outbound_ips(), - outbound_ip_prefixes=self.context.get_load_balancer_outbound_ip_prefixes(), - outbound_ports=self.context.get_load_balancer_outbound_ports(), - idle_timeout=self.context.get_load_balancer_idle_timeout(), - backend_pool_type=self.context.get_load_balancer_backend_pool_type(), - health_probe_mode=self.context.get_cluster_service_load_balancer_health_probe_mode(), - profile=mc.network_profile.load_balancer_profile, - models=self.models.load_balancer_models, - ) + # In the internal function "_update_load_balancer_profile", it will check whether the provided parameters + # have been assigned, and if there are any, the corresponding profile will be modified; otherwise, it will + # remain unchanged. + profile = _update_load_balancer_profile( + managed_outbound_ip_count=self.context.get_load_balancer_managed_outbound_ip_count(), + managed_outbound_ipv6_count=self.context.get_load_balancer_managed_outbound_ipv6_count(), + outbound_ips=self.context.get_load_balancer_outbound_ips(), + outbound_ip_prefixes=self.context.get_load_balancer_outbound_ip_prefixes(), + outbound_ports=self.context.get_load_balancer_outbound_ports(), + idle_timeout=self.context.get_load_balancer_idle_timeout(), + backend_pool_type=self.context.get_load_balancer_backend_pool_type(), + health_probe_mode=self.context.get_cluster_service_load_balancer_health_probe_mode(), + profile=mc.network_profile.load_balancer_profile, + models=self.models.load_balancer_models, + ) + # read the original value passed by the command + outbound_type = self.context.raw_param.get("outbound_type") + # The outbound fields only apply while the load balancer provides egress, so they are dropped + # when the command switches the cluster to a different outbound type. Inbound settings such as + # backendPoolType stay valid for every outbound type and must be preserved. + if profile and outbound_type and outbound_type != CONST_OUTBOUND_TYPE_LOAD_BALANCER: + profile.managed_outbound_i_ps = None + profile.outbound_i_ps = None + profile.outbound_ip_prefixes = None + profile.allocated_outbound_ports = None + profile.idle_timeout_in_minutes = None + if not any([ + profile.backend_pool_type, + profile.cluster_service_load_balancer_health_probe_mode, + profile.enable_multiple_standard_load_balancers, + ]): + profile = None + mc.network_profile.load_balancer_profile = profile return mc def update_nat_gateway_profile(self, mc: ManagedCluster) -> ManagedCluster: diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py index 2437261b3e9..3e6826eb422 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py @@ -35,6 +35,7 @@ CONST_INGRESS_APPGW_WATCH_NAMESPACE, CONST_KUBE_DASHBOARD_ADDON_NAME, CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IP, + CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IPCONFIGURATION, CONST_LOAD_BALANCER_SKU_STANDARD, CONST_LOAD_BALANCER_SKU_BASIC, CONST_MANAGED_GATEWAY_INSTALLATION_DISABLED, @@ -109,7 +110,9 @@ DecoratorMode, ) from azext_aks_preview._consts import ( + CONST_OUTBOUND_TYPE_BLOCK, CONST_OUTBOUND_TYPE_MANAGED_NAT_GATEWAY_V2, + CONST_OUTBOUND_TYPE_NONE, ) from dateutil.parser import parse from deepdiff import DeepDiff @@ -10433,6 +10436,185 @@ def test_update_load_balancer_profile(self): ) self.assertEqual(dec_mc_11, ground_truth_mc_11) + def test_update_load_balancer_profile__outbound_type_not_specified_keeps_backend_pool_type(self): + # `az aks update --load-balancer-backend-pool-type nodeIP` without --outbound-type: + # the outbound type is inherited from the existing cluster and must not cause the + # requested NodeIPConfiguration -> nodeIP change to be dropped, for any outbound type. + for outbound_type in [ + CONST_OUTBOUND_TYPE_LOAD_BALANCER, + CONST_OUTBOUND_TYPE_USER_DEFINED_ROUTING, + CONST_OUTBOUND_TYPE_MANAGED_NAT_GATEWAY, + CONST_OUTBOUND_TYPE_MANAGED_NAT_GATEWAY_V2, + CONST_OUTBOUND_TYPE_USER_ASSIGNED_NAT_GATEWAY, + CONST_OUTBOUND_TYPE_NONE, + CONST_OUTBOUND_TYPE_BLOCK, + ]: + with self.subTest(outbound_type=outbound_type): + dec = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "load_balancer_backend_pool_type": CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IP, + "outbound_type": None, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + outbound_type=outbound_type, + load_balancer_sku="standard", + load_balancer_profile=self.models.load_balancer_models.ManagedClusterLoadBalancerProfile( + backend_pool_type=CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IPCONFIGURATION, + ), + ), + ) + dec.context.attach_mc(mc) + dec_mc = dec.update_load_balancer_profile(mc) + + lb_profile = dec_mc.network_profile.load_balancer_profile + self.assertIsNotNone( + lb_profile, + "load balancer profile must not be dropped when outbound type is unchanged", + ) + self.assertEqual( + lb_profile.backend_pool_type, + CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IP, + ) + + def _lb_profile_with_every_outbound_field(self): + lb = self.models.load_balancer_models + return lb.ManagedClusterLoadBalancerProfile( + backend_pool_type=CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IPCONFIGURATION, + managed_outbound_i_ps=lb.ManagedClusterLoadBalancerProfileManagedOutboundIPs(count=2), + outbound_i_ps=lb.ManagedClusterLoadBalancerProfileOutboundIPs( + public_i_ps=[lb.ResourceReference(id="ip1")] + ), + outbound_ip_prefixes=lb.ManagedClusterLoadBalancerProfileOutboundIPPrefixes( + public_ip_prefixes=[lb.ResourceReference(id="prefix1")] + ), + allocated_outbound_ports=8000, + idle_timeout_in_minutes=10, + ) + + def test_update_load_balancer_profile__outbound_type_not_load_balancer_clears_outbound_fields(self): + # `--outbound-type userDefinedRouting` and `--load-balancer-backend-pool-type nodeIP` in the + # same command: every outbound field is dropped, the backend pool type is still applied + dec = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "outbound_type": CONST_OUTBOUND_TYPE_USER_DEFINED_ROUTING, + "load_balancer_backend_pool_type": CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IP, + # changing outbound type to userDefinedRouting requires a BYO vnet + "vnet_subnet_id": "test_vnet_subnet_id", + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + outbound_type=CONST_OUTBOUND_TYPE_LOAD_BALANCER, + load_balancer_sku="standard", + load_balancer_profile=self._lb_profile_with_every_outbound_field(), + ), + ) + dec.context.attach_mc(mc) + lb_profile = dec.update_load_balancer_profile(mc).network_profile.load_balancer_profile + + self.assertEqual(lb_profile.backend_pool_type, CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IP) + self.assertIsNone(lb_profile.managed_outbound_i_ps) + self.assertIsNone(lb_profile.outbound_i_ps) + self.assertIsNone(lb_profile.outbound_ip_prefixes) + self.assertIsNone(lb_profile.allocated_outbound_ports) + self.assertIsNone(lb_profile.idle_timeout_in_minutes) + + def test_update_load_balancer_profile__outbound_type_not_load_balancer_no_inbound_drops_profile(self): + # `--outbound-type userDefinedRouting` on a profile holding only outbound fields: nothing + # inbound is left to keep, so the profile is omitted entirely as before + dec = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "outbound_type": CONST_OUTBOUND_TYPE_USER_DEFINED_ROUTING, + "vnet_subnet_id": "test_vnet_subnet_id", + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + outbound_type=CONST_OUTBOUND_TYPE_LOAD_BALANCER, + load_balancer_sku="standard", + load_balancer_profile=self.models.load_balancer_models.ManagedClusterLoadBalancerProfile( + allocated_outbound_ports=8000, + ), + ), + ) + dec.context.attach_mc(mc) + dec_mc = dec.update_load_balancer_profile(mc) + + self.assertIsNone(dec_mc.network_profile.load_balancer_profile) + + def test_update_load_balancer_profile__outbound_type_load_balancer_keeps_outbound_fields(self): + # `--outbound-type loadBalancer` keeps egress on the load balancer, so every outbound field + # stays valid and must survive untouched + dec = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + {"outbound_type": CONST_OUTBOUND_TYPE_LOAD_BALANCER}, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + outbound_type=CONST_OUTBOUND_TYPE_LOAD_BALANCER, + load_balancer_sku="standard", + load_balancer_profile=self._lb_profile_with_every_outbound_field(), + ), + ) + dec.context.attach_mc(mc) + lb_profile = dec.update_load_balancer_profile(mc).network_profile.load_balancer_profile + + self.assertEqual(lb_profile.managed_outbound_i_ps.count, 2) + self.assertEqual(lb_profile.outbound_i_ps.public_i_ps[0].id, "ip1") + self.assertEqual(lb_profile.outbound_ip_prefixes.public_ip_prefixes[0].id, "prefix1") + self.assertEqual(lb_profile.allocated_outbound_ports, 8000) + self.assertEqual(lb_profile.idle_timeout_in_minutes, 10) + + def test_update_load_balancer_profile__outbound_type_changed_to_load_balancer_applies_outbound_fields(self): + # moving a userDefinedRouting cluster back onto the load balancer: outbound settings become + # valid again, so those requested in the same command must be applied + dec = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "outbound_type": CONST_OUTBOUND_TYPE_LOAD_BALANCER, + "load_balancer_managed_outbound_ip_count": 2, + "load_balancer_outbound_ports": 8000, + "load_balancer_idle_timeout": 25, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc = self.models.ManagedCluster( + location="test_location", + network_profile=self.models.ContainerServiceNetworkProfile( + outbound_type=CONST_OUTBOUND_TYPE_USER_DEFINED_ROUTING, + load_balancer_sku="standard", + # a userDefinedRouting cluster only carries the inbound settings + load_balancer_profile=self.models.load_balancer_models.ManagedClusterLoadBalancerProfile( + backend_pool_type=CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IPCONFIGURATION, + ), + ), + ) + dec.context.attach_mc(mc) + lb_profile = dec.update_load_balancer_profile(mc).network_profile.load_balancer_profile + + self.assertEqual(lb_profile.managed_outbound_i_ps.count, 2) + self.assertEqual(lb_profile.allocated_outbound_ports, 8000) + self.assertEqual(lb_profile.idle_timeout_in_minutes, 25) + self.assertEqual(lb_profile.backend_pool_type, CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IPCONFIGURATION) + def test_update_nat_gateway_profile(self): # default value in `aks_update` dec_1 = AKSPreviewManagedClusterUpdateDecorator( diff --git a/src/aks-preview/setup.py b/src/aks-preview/setup.py index 01440a7e2db..df53effcdbc 100644 --- a/src/aks-preview/setup.py +++ b/src/aks-preview/setup.py @@ -9,7 +9,7 @@ from setuptools import find_packages, setup -VERSION = "22.0.0b1" +VERSION = "22.0.0b2" CLASSIFIERS = [ "Development Status :: 4 - Beta",