From 1e040ed849cf7699265ce4d02c30db52f30a4f1d Mon Sep 17 00:00:00 2001 From: reneeli123 Date: Wed, 19 Aug 2026 15:08:45 +1200 Subject: [PATCH 1/4] Add Add --enable-hosted-system, --system-node-subnet-id and --node-subnet-id to az aks update --- .../azure/cli/command_modules/acs/_help.py | 30 ++++ .../azure/cli/command_modules/acs/_params.py | 3 + .../azure/cli/command_modules/acs/custom.py | 4 + .../command_modules/acs/linter_exclusions.yml | 9 + .../acs/managed_cluster_decorator.py | 81 +++++++-- .../latest/test_managed_cluster_decorator.py | 155 ++++++++++++++++++ 6 files changed, 264 insertions(+), 18 deletions(-) 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 55889813821..2d2846f0987 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_help.py @@ -1072,6 +1072,32 @@ - name: --apiserver-subnet-id type: string short-summary: The ID of a subnet in an existing VNet into which to assign control plane apiserver pods(requires --enable-apiserver-vnet-integration) + - name: --enable-hosted-system + 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, and required to request the conversion. + The cluster keeps its existing networking: this flag does not onboard a cluster that + uses AKS-managed networking onto a VNet. For a cluster that already runs in a + bring-your-own VNet, combine it with `--system-node-subnet-id` (and optionally + `--node-subnet-id` and `--apiserver-subnet-id`) to place the Managed System Pool in + that same VNet. + - 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: | + Requires `--enable-hosted-system`, and only applies to a cluster that already runs in a + bring-your-own VNet. The subnet must belong to that VNet. 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. + - 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: | + Requires `--enable-hosted-system` and `--system-node-subnet-id`, and only applies to a + cluster that already runs in a bring-your-own VNet. All supplied subnets must belong to + that same VNet. - name: --enable-private-cluster type: bool short-summary: Enable private cluster for apiserver vnet integration cluster. @@ -1304,6 +1330,10 @@ text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-gateway-api - name: Update a kubernetes cluster to disable the managed installation of Gateway API CRDs. text: az aks update -g MyResourceGroup -n MyManagedCluster --disable-gateway-api + - name: Convert an existing non-HOBO Automatic cluster to use a Managed System Pool. + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-hosted-system + - name: Convert an existing non-HOBO Automatic cluster that runs in a bring-your-own VNet, placing the Managed System Pool in that VNet. + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-hosted-system --system-node-subnet-id --node-subnet-id """ helps["aks delete"] = """ 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 f14b779a725..e6b44e9efc1 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -724,6 +724,9 @@ def load_arguments(self, _): # private cluster parameters c.argument('enable_apiserver_vnet_integration', action='store_true') c.argument('apiserver_subnet_id', validator=validate_apiserver_subnet_id) + c.argument('system_node_subnet_id', validator=validate_system_node_subnet_id) + c.argument('node_subnet_id', validator=validate_node_subnet_id) + c.argument('enable_hosted_system', action='store_true') c.argument('enable_private_cluster', action='store_true') c.argument('disable_private_cluster', action='store_true') c.argument('enable_public_fqdn', action='store_true') 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 d169f67c593..b7d06bd4ade 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/custom.py +++ b/src/azure-cli/azure/cli/command_modules/acs/custom.py @@ -1244,6 +1244,10 @@ def aks_update( # apiserver vnet integration enable_apiserver_vnet_integration=False, apiserver_subnet_id=None, + # BYO VNet for Managed System Pool (Automatic SKU) + system_node_subnet_id=None, + node_subnet_id=None, + enable_hosted_system=False, enable_private_cluster=False, disable_private_cluster=False, # node provisioning diff --git a/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml b/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml index 10f1329cc9b..0395fb804ac 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml +++ b/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml @@ -131,6 +131,15 @@ aks enable-addons: - option_length_too_long aks update: parameters: + system_node_subnet_id: + rule_exclusions: + - missing_parameter_test_coverage + node_subnet_id: + rule_exclusions: + - missing_parameter_test_coverage + enable_hosted_system: + rule_exclusions: + - missing_parameter_test_coverage disable_secret_rotation: rule_exclusions: - option_length_too_long 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 7ab7ec01dc2..3b0bed5d049 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 @@ -4342,14 +4342,15 @@ def get_enable_hosted_system(self) -> bool: """Obtain the value of enable_hosted_system. Returns True when the user explicitly opts in via --enable-hosted-system, - or implicitly via the BYO VNet subnet trio for Managed System Pool. + or, on create only, implicitly via the BYO VNet subnet trio. :return: bool """ - if self.decorator_mode != DecoratorMode.CREATE: + 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"), @@ -4358,20 +4359,23 @@ def get_enable_hosted_system(self) -> bool: ) return explicit or implicit - def validate_byo_hosted_system_subnets(self) -> None: - """Validate the BYO VNet subnet trio and the --enable-hosted-system flag. + def _hosted_system_sku_error_message(self) -> str: + """Build the SKU requirement message for hosted-system flags, per decorator mode.""" + if self.decorator_mode == DecoratorMode.UPDATE: + return '"--enable-hosted-system" is only supported on clusters with the Automatic SKU.' + return '"--enable-hosted-system" requires "--sku automatic".' - BYO VNet for a Managed System Pool is triggered by --system-node-subnet-id / - --node-subnet-id. --apiserver-subnet-id is intentionally NOT part of the trigger - because it keeps its existing general-purpose meaning for - --enable-apiserver-vnet-integration flows on non-Automatic clusters. + def validate_byo_hosted_system_subnets(self) -> None: + """Cross-validate the BYO VNet subnet flags for the current decorator mode. - - If either --system-node-subnet-id or --node-subnet-id is set, the full trio - (--system-node-subnet-id, --node-subnet-id, --apiserver-subnet-id) must be - provided and --sku must be automatic. - - --enable-hosted-system is only valid with --sku automatic. + 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. """ - if self.decorator_mode != DecoratorMode.CREATE: + if self.decorator_mode not in (DecoratorMode.CREATE, DecoratorMode.UPDATE): return system_node_subnet_id = self.raw_param.get("system_node_subnet_id") node_subnet_id = self.raw_param.get("node_subnet_id") @@ -4380,11 +4384,23 @@ def validate_byo_hosted_system_subnets(self) -> None: byo_specific_set = bool(system_node_subnet_id or node_subnet_id) - # --enable-hosted-system requires --sku automatic. + # --enable-hosted-system requires the Automatic SKU. if enable_hosted_system and self.get_sku_name() != CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC: - raise RequiredArgumentMissingError( - '"--enable-hosted-system" requires "--sku automatic".' - ) + raise RequiredArgumentMissingError(self._hosted_system_sku_error_message()) + + if self.decorator_mode == DecoratorMode.UPDATE: + if not byo_specific_set: + 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( + 'Using "--system-node-subnet-id" or "--node-subnet-id" require ' + '"--enable-hosted-system".' + ) + return # Partial trio: if any BYO subnet is set, require the full trio. if byo_specific_set: @@ -10548,6 +10564,33 @@ def update_node_provisioning_profile(self, mc: ManagedCluster) -> ManagedCluster return mc + def update_hosted_system_profile(self, mc: ManagedCluster) -> ManagedCluster: + """Update hosted_system_profile for the ManagedCluster object. + + Supports converting an existing Automatic cluster to a Managed System Pool (hosted + system) cluster, optionally with BYO VNet subnets. + + :return: the ManagedCluster object + """ + self._ensure_mc(mc) + + self.context.validate_byo_hosted_system_subnets() + if not self.context.get_enable_hosted_system(): + return mc + + if mc.hosted_system_profile is None: + mc.hosted_system_profile = self.models.ManagedClusterHostedSystemProfile() # pylint: disable=no-member + mc.hosted_system_profile.enabled = True + + system_node_subnet_id = self.context.get_system_node_subnet_id() + node_subnet_id = self.context.get_node_subnet_id() + if system_node_subnet_id: + mc.hosted_system_profile.system_node_subnet_id = system_node_subnet_id + if node_subnet_id: + mc.hosted_system_profile.node_subnet_id = node_subnet_id + + return mc + def update_mc_profile_default(self) -> ManagedCluster: """The overall controller used to update the default ManagedCluster profile. @@ -10651,6 +10694,8 @@ def update_mc_profile_default(self) -> ManagedCluster: mc = self.update_vmas_to_vms(mc) # update node provisioning profile mc = self.update_node_provisioning_profile(mc) + # update hosted system profile (Managed System Pool conversion) + mc = self.update_hosted_system_profile(mc) return mc def update_kubernetes_version_and_orchestrator_version(self, mc: ManagedCluster) -> ManagedCluster: 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 02f18b2e6d2..578edca54ec 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 @@ -16358,6 +16358,161 @@ def test_update_node_provisioning_profile(self): ) self.assertEqual(dec_mc_2, ground_truth_mc_2) + def test_update_hosted_system_profile(self): + system_node_subnet_id = "/subscriptions/fakesub/resourceGroups/fakerg/providers/Microsoft.Network/virtualNetworks/fakevnet/subnets/systemnode" + node_subnet_id = "/subscriptions/fakesub/resourceGroups/fakerg/providers/Microsoft.Network/virtualNetworks/fakevnet/subnets/node" + apiserver_subnet_id = "/subscriptions/fakesub/resourceGroups/fakerg/providers/Microsoft.Network/virtualNetworks/fakevnet/subnets/apiserver" + + # not specified, no change + dec_0 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + {}, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_0 = self.models.ManagedCluster( + location="test_location", + sku=self.models.ManagedClusterSKU(name="Automatic"), + ) + dec_0.context.attach_mc(mc_0) + dec_mc_0 = dec_0.update_hosted_system_profile(mc_0) + self.assertIsNone(dec_mc_0.hosted_system_profile) + + # non-BYO conversion + dec_1 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + {"enable_hosted_system": True}, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_1 = self.models.ManagedCluster( + location="test_location", + sku=self.models.ManagedClusterSKU(name="Automatic"), + ) + dec_1.context.attach_mc(mc_1) + dec_mc_1 = dec_1.update_hosted_system_profile(mc_1) + self.assertTrue(dec_mc_1.hosted_system_profile.enabled) + 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 + dec_2 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_hosted_system": True, + "system_node_subnet_id": system_node_subnet_id, + "node_subnet_id": node_subnet_id, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_2 = self.models.ManagedCluster( + location="test_location", + sku=self.models.ManagedClusterSKU(name="Automatic"), + ) + dec_2.context.attach_mc(mc_2) + dec_mc_2 = dec_2.update_hosted_system_profile(mc_2) + self.assertTrue(dec_mc_2.hosted_system_profile.enabled) + 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) + + # --node-subnet-id and --apiserver-subnet-id are optional + dec_3 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_hosted_system": True, + "system_node_subnet_id": system_node_subnet_id, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_3 = self.models.ManagedCluster( + location="test_location", + sku=self.models.ManagedClusterSKU(name="Automatic"), + ) + dec_3.context.attach_mc(mc_3) + dec_mc_3 = dec_3.update_hosted_system_profile(mc_3) + self.assertTrue(dec_mc_3.hosted_system_profile.enabled) + self.assertEqual(dec_mc_3.hosted_system_profile.system_node_subnet_id, system_node_subnet_id) + self.assertIsNone(dec_mc_3.hosted_system_profile.node_subnet_id) + + # --apiserver-subnet-id may still be supplied alongside the node subnets + dec_4 = AKSManagedClusterUpdateDecorator( + 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, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_4 = 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_4.context.attach_mc(mc_4) + dec_mc_4 = dec_4.update_hosted_system_profile(mc_4) + self.assertTrue(dec_mc_4.hosted_system_profile.enabled) + self.assertEqual(dec_mc_4.hosted_system_profile.system_node_subnet_id, system_node_subnet_id) + self.assertEqual(dec_mc_4.hosted_system_profile.node_subnet_id, node_subnet_id) + + # the subnet flags require --enable-hosted-system + dec_5 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "system_node_subnet_id": system_node_subnet_id, + "node_subnet_id": node_subnet_id, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_5 = self.models.ManagedCluster( + location="test_location", + sku=self.models.ManagedClusterSKU(name="Automatic"), + ) + dec_5.context.attach_mc(mc_5) + with self.assertRaises(RequiredArgumentMissingError): + dec_5.update_hosted_system_profile(mc_5) + + # --node-subnet-id requires --system-node-subnet-id + dec_6 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + { + "enable_hosted_system": True, + "node_subnet_id": node_subnet_id, + }, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_6 = self.models.ManagedCluster( + location="test_location", + sku=self.models.ManagedClusterSKU(name="Automatic"), + ) + dec_6.context.attach_mc(mc_6) + with self.assertRaises(RequiredArgumentMissingError): + dec_6.update_hosted_system_profile(mc_6) + + # non-Automatic SKU is rejected + dec_7 = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + {"enable_hosted_system": True}, + ResourceType.MGMT_CONTAINERSERVICE, + ) + mc_7 = self.models.ManagedCluster( + location="test_location", + sku=self.models.ManagedClusterSKU(name="Base"), + ) + dec_7.context.attach_mc(mc_7) + with self.assertRaises(RequiredArgumentMissingError): + dec_7.update_hosted_system_profile(mc_7) + def test_enable_container_network_logs(self): # Case 1: enable_acns, enable monitoring addons_profile, enable container_network_logs dec_1 = AKSManagedClusterUpdateDecorator( From 9643cf0d6f82ad29a17b21cd7e817c65502cd4e3 Mon Sep 17 00:00:00 2001 From: reneeli123 Date: Wed, 19 Aug 2026 16:16:26 +1200 Subject: [PATCH 2/4] Trigger CI From 53b16c508a79add3c0a0875e0c56f2d7ef000805 Mon Sep 17 00:00:00 2001 From: reneeli123 Date: Wed, 19 Aug 2026 21:41:39 +1200 Subject: [PATCH 3/4] Trigger CI From b8181d3ff759f3c1f7a040a1c3d785790eceb1f7 Mon Sep 17 00:00:00 2001 From: reneeli123 Date: Thu, 20 Aug 2026 12:33:41 +1200 Subject: [PATCH 4/4] Trigger CI