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",