From 41c589d3457e0503dfcb59f7de112f14f67ba5cb Mon Sep 17 00:00:00 2001 From: reneeli123 Date: Tue, 18 Aug 2026 16:04:01 +1200 Subject: [PATCH 1/4] Relax validation so apiserver_subnet_id is not required during non-HOBO to HOBO conversion --- src/aks-preview/azext_aks_preview/_help.py | 28 ++++---- .../managed_cluster_decorator.py | 43 +++++++---- .../latest/test_managed_cluster_decorator.py | 72 +++++++++++++++++-- 3 files changed, 112 insertions(+), 31 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index 4b2f2023dd5..84fa526185e 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -1591,26 +1591,26 @@ type: bool short-summary: (Automatic SKU) Convert an existing Automatic cluster to use a Managed System Pool. long-summary: | - Only valid for clusters with the Automatic SKU. Optionally provide the bring-your-own - VNet subnet trio (`--system-node-subnet-id`, `--node-subnet-id`, `--apiserver-subnet-id`) - to move the cluster onto an existing VNet at the same time. Supplying the full trio also - implies this flag. + Only valid for clusters with the Automatic SKU. Required to request the conversion. + Use it on its own to convert the cluster while keeping its current AKS-managed + networking, or combine it with the bring-your-own VNet subnet flags + (`--system-node-subnet-id`, `--node-subnet-id`, `--apiserver-subnet-id`) to move the + cluster onto an existing VNet at the same time. - name: --system-node-subnet-id type: string short-summary: (Automatic SKU) The ID of a subnet in an existing VNet to be used by the Managed System Pool. long-summary: | - Bring-your-own VNet for an Automatic cluster requires three subnets supplied together: - `--system-node-subnet-id` (this flag, for the Managed System Pool), `--node-subnet-id` - (for user node pools), and `--apiserver-subnet-id` (for the control plane API server). - All three subnets must belong to the same VNet. + Requires `--enable-hosted-system`. Unlike `az aks create`, the other bring-your-own + VNet subnets are optional here: `--node-subnet-id` (for user node pools) and + `--apiserver-subnet-id` (for the control plane API server) can be omitted, in which + case the cluster keeps its current node and API server networking. Any subnets you do + supply must belong to the same VNet. - name: --node-subnet-id type: string short-summary: (Automatic SKU) The ID of a subnet in an existing VNet to be used by user node pools. long-summary: | - Bring-your-own VNet for an Automatic cluster requires three subnets supplied together: - `--system-node-subnet-id` (for the Managed System Pool), `--node-subnet-id` (this flag, - for user node pools), and `--apiserver-subnet-id` (for the control plane API server). - All three subnets must belong to the same VNet. + Requires `--enable-hosted-system` and `--system-node-subnet-id`, and all supplied + subnets must belong to the same VNet. examples: - name: Reconcile the cluster back to its current state. text: az aks update -g MyResourceGroup -n MyManagedCluster @@ -1706,7 +1706,9 @@ text: az aks update -g MyResourceGroup -n MyManagedCluster --disable-opentelemetry-logs-traces - name: Convert an existing Automatic cluster to use a Managed System Pool. text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-hosted-system - - name: Convert an existing Automatic cluster to use a Managed System Pool with a bring-your-own VNet. + - name: Convert an existing Automatic cluster to use a Managed System Pool in a bring-your-own VNet. + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-hosted-system --system-node-subnet-id + - name: Convert an existing Automatic cluster to use a Managed System Pool, also moving node pools and the API server onto a bring-your-own VNet. text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-hosted-system --system-node-subnet-id --node-subnet-id --apiserver-subnet-id """ 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 1d7fbf008ba..e5afa54ce8e 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -4275,7 +4275,8 @@ def get_enable_hosted_system(self) -> bool: if self.decorator_mode not in (DecoratorMode.CREATE, DecoratorMode.UPDATE): return False explicit = bool(self.raw_param.get("enable_hosted_system")) - implicit = all( + # on update the conversion must be requested explicitly; subnets never imply it + implicit = self.decorator_mode == DecoratorMode.CREATE and all( [ self.raw_param.get("system_node_subnet_id"), self.raw_param.get("node_subnet_id"), @@ -4289,8 +4290,7 @@ def get_enable_hosted_system(self) -> bool: def get_system_node_subnet_id(self) -> Union[str, None]: """Obtain the value of system_node_subnet_id. - Validates that BYO VNet subnet flags are used as a full triple - (system-node / node / apiserver). + Cross-validates the BYO VNet subnet flags for the current decorator mode. :return: str or None """ @@ -4346,15 +4346,36 @@ def _hosted_system_sku_error_message(self) -> str: def validate_byo_hobo_subnet_trio(self) -> None: """Cross-validate the BYO VNet HOBO subnet flags. - Rule: if either --system-node-subnet-id or --node-subnet-id is set, the - full BYO trio must be set and --sku must be automatic. A complete trio - implies hosted-system enablement. + On create, setting either --system-node-subnet-id or --node-subnet-id requires the + full trio (both of those plus --apiserver-subnet-id), and a complete trio implies + hosted-system enablement. On update the conversion is requested explicitly with + --enable-hosted-system, so the subnets only shape the networking it lands on: + --node-subnet-id requires --system-node-subnet-id and --apiserver-subnet-id stays + optional. Either way the cluster must use the Automatic SKU. """ system_node_subnet_id = self.raw_param.get("system_node_subnet_id") node_subnet_id = self.raw_param.get("node_subnet_id") apiserver_subnet_id = self.raw_param.get("apiserver_subnet_id") enable_hosted_system = bool(self.raw_param.get("enable_hosted_system")) + if self.decorator_mode == DecoratorMode.UPDATE: + if not self.has_byo_hobo_subnets(): + return + if node_subnet_id and not system_node_subnet_id: + raise RequiredArgumentMissingError( + '"--node-subnet-id" requires "--system-node-subnet-id".' + ) + if not enable_hosted_system: + raise RequiredArgumentMissingError( + '"--system-node-subnet-id" and "--node-subnet-id" require "--enable-hosted-system".' + ) + if self.get_sku_name() != CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: + raise RequiredArgumentMissingError( + '"--system-node-subnet-id" and "--node-subnet-id" are only supported on ' + "clusters with the Automatic SKU." + ) + return + if enable_hosted_system and self.get_sku_name() != CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: raise RequiredArgumentMissingError(self._hosted_system_sku_error_message()) @@ -4374,11 +4395,6 @@ def validate_byo_hobo_subnet_trio(self) -> None: f"Missing: {', '.join(missing)}." ) if self.get_sku_name() != CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: - if self.decorator_mode == DecoratorMode.UPDATE: - raise RequiredArgumentMissingError( - '"--system-node-subnet-id" and "--node-subnet-id" are only supported on ' - "clusters with the Automatic SKU." - ) raise RequiredArgumentMissingError( '"--system-node-subnet-id" and "--node-subnet-id" require "--sku automatic".' ) @@ -8891,9 +8907,8 @@ def update_hosted_system_profile(self, mc: ManagedCluster) -> ManagedCluster: mc.hosted_system_profile = self.models.ManagedClusterHostedSystemProfile() # pylint: disable=no-member mc.hosted_system_profile.enabled = True - # BYO VNet: all three subnets (system-node / node / apiserver) must share a VNet, - # but the server enforces that check. The trio is already validated above, so read - # the raw values rather than the getters, which would re-run that validation. + # Already validated above, so read the raw values rather than the getters, + # which would re-run that validation. system_node_subnet_id = self.context.raw_param.get("system_node_subnet_id") node_subnet_id = self.context.raw_param.get("node_subnet_id") if system_node_subnet_id: 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 3e6826eb422..1917ea746d5 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 @@ -17319,14 +17319,14 @@ def test_update_hosted_system_profile(self): self.assertIsNone(dec_mc_1.hosted_system_profile.system_node_subnet_id) self.assertIsNone(dec_mc_1.hosted_system_profile.node_subnet_id) - # BYO conversion, the full subnet trio implies enablement + # BYO conversion dec_2 = AKSPreviewManagedClusterUpdateDecorator( self.cmd, self.client, { + "enable_hosted_system": True, "system_node_subnet_id": system_node_subnet_id, "node_subnet_id": node_subnet_id, - "apiserver_subnet_id": apiserver_subnet_id, }, CUSTOM_MGMT_AKS_PREVIEW, ) @@ -17340,8 +17340,8 @@ def test_update_hosted_system_profile(self): self.assertEqual(dec_mc_2.hosted_system_profile.system_node_subnet_id, system_node_subnet_id) self.assertEqual(dec_mc_2.hosted_system_profile.node_subnet_id, node_subnet_id) - # partial subnet trio is rejected - dec_3 = AKSPreviewManagedClusterUpdateDecorator( + # --node-subnet-id and --apiserver-subnet-id are optional + dec_6 = AKSPreviewManagedClusterUpdateDecorator( self.cmd, self.client, { @@ -17350,6 +17350,44 @@ def test_update_hosted_system_profile(self): }, CUSTOM_MGMT_AKS_PREVIEW, ) + mc_6 = self.models.ManagedCluster( + location="test_location", + sku=self.models.ManagedClusterSKU(name="Automatic"), + ) + dec_6.context.attach_mc(mc_6) + dec_mc_6 = dec_6.update_hosted_system_profile(mc_6) + self.assertTrue(dec_mc_6.hosted_system_profile.enabled) + self.assertEqual(dec_mc_6.hosted_system_profile.system_node_subnet_id, system_node_subnet_id) + self.assertIsNone(dec_mc_6.hosted_system_profile.node_subnet_id) + + # the subnet flags require --enable-hosted-system + dec_9 = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "system_node_subnet_id": system_node_subnet_id, + "node_subnet_id": node_subnet_id, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc_9 = self.models.ManagedCluster( + location="test_location", + sku=self.models.ManagedClusterSKU(name="Automatic"), + ) + dec_9.context.attach_mc(mc_9) + with self.assertRaises(RequiredArgumentMissingError): + dec_9.update_hosted_system_profile(mc_9) + + # --node-subnet-id requires --system-node-subnet-id + dec_3 = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_hosted_system": True, + "node_subnet_id": node_subnet_id, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) mc_3 = self.models.ManagedCluster( location="test_location", sku=self.models.ManagedClusterSKU(name="Automatic"), @@ -17358,6 +17396,32 @@ def test_update_hosted_system_profile(self): with self.assertRaises(RequiredArgumentMissingError): dec_3.update_hosted_system_profile(mc_3) + # --apiserver-subnet-id may still be supplied alongside the node subnets + dec_5 = AKSPreviewManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_hosted_system": True, + "system_node_subnet_id": system_node_subnet_id, + "node_subnet_id": node_subnet_id, + "apiserver_subnet_id": apiserver_subnet_id, + }, + CUSTOM_MGMT_AKS_PREVIEW, + ) + mc_5 = self.models.ManagedCluster( + location="test_location", + sku=self.models.ManagedClusterSKU(name="Automatic"), + api_server_access_profile=self.models.ManagedClusterAPIServerAccessProfile( + enable_vnet_integration=True, + subnet_id=apiserver_subnet_id, + ), + ) + dec_5.context.attach_mc(mc_5) + dec_mc_5 = dec_5.update_hosted_system_profile(mc_5) + self.assertTrue(dec_mc_5.hosted_system_profile.enabled) + self.assertEqual(dec_mc_5.hosted_system_profile.system_node_subnet_id, system_node_subnet_id) + self.assertEqual(dec_mc_5.hosted_system_profile.node_subnet_id, node_subnet_id) + # non-Automatic SKU is rejected dec_4 = AKSPreviewManagedClusterUpdateDecorator( self.cmd, From d5271daaac30bebeb4e05bf75091a37c18181dc4 Mon Sep 17 00:00:00 2001 From: reneeli123 Date: Tue, 18 Aug 2026 16:08:30 +1200 Subject: [PATCH 2/4] Update version --- src/aks-preview/HISTORY.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index acf1efbf0c2..7416d3428ec 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.0b3 ++++++++++ +* `az aks update`: Relax the bring-your-own VNet subnet validation for converting non-HOBO to HOBO Automatic cluster. `--apiserver-subnet-id` is no longer required, and `--system-node-subnet-id` can be supplied on its own; omitted subnets keep their current networking. `--enable-hosted-system` is still required to request the conversion, and `--node-subnet-id` still requires `--system-node-subnet-id`. + 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. From 783061f54a9b2eb11fc11c1c8d0cd86acc883d38 Mon Sep 17 00:00:00 2001 From: reneeli123 Date: Thu, 20 Aug 2026 14:55:42 +1200 Subject: [PATCH 3/4] Address comments --- .../azext_aks_preview/managed_cluster_decorator.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) 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 e5afa54ce8e..ef2ed62d055 100644 --- a/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/managed_cluster_decorator.py @@ -4358,6 +4358,9 @@ def validate_byo_hobo_subnet_trio(self) -> None: apiserver_subnet_id = self.raw_param.get("apiserver_subnet_id") enable_hosted_system = bool(self.raw_param.get("enable_hosted_system")) + if enable_hosted_system and self.get_sku_name() != CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: + raise RequiredArgumentMissingError(self._hosted_system_sku_error_message()) + if self.decorator_mode == DecoratorMode.UPDATE: if not self.has_byo_hobo_subnets(): return @@ -4367,18 +4370,10 @@ def validate_byo_hobo_subnet_trio(self) -> None: ) if not enable_hosted_system: raise RequiredArgumentMissingError( - '"--system-node-subnet-id" and "--node-subnet-id" require "--enable-hosted-system".' - ) - if self.get_sku_name() != CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: - raise RequiredArgumentMissingError( - '"--system-node-subnet-id" and "--node-subnet-id" are only supported on ' - "clusters with the Automatic SKU." + 'Using "--system-node-subnet-id" and "--node-subnet-id" require "--enable-hosted-system".' ) return - if enable_hosted_system and self.get_sku_name() != CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: - raise RequiredArgumentMissingError(self._hosted_system_sku_error_message()) - if self.has_byo_hobo_subnets(): missing = [] if not system_node_subnet_id: From 637abc08544ea5934b04644d67c17bd61ca1c1eb Mon Sep 17 00:00:00 2001 From: reneeli123 Date: Thu, 20 Aug 2026 14:56:43 +1200 Subject: [PATCH 4/4] update version --- src/aks-preview/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aks-preview/setup.py b/src/aks-preview/setup.py index df53effcdbc..8e7c3d36a83 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.0b2" +VERSION = "22.0.0b3" CLASSIFIERS = [ "Development Status :: 4 - Beta",