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 e0b7ae94fcd..14127def524 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_help.py @@ -1075,6 +1075,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. @@ -1313,6 +1339,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 09056477334..796d7130415 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -725,6 +725,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 a0008c954d2..6ee4d14be5f 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/custom.py +++ b/src/azure-cli/azure/cli/command_modules/acs/custom.py @@ -1246,6 +1246,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 b3eb404069f..99b32fa4f0a 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 @@ -134,6 +134,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 6a0480a8850..78d5a27a6c3 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: @@ -10593,29 +10609,30 @@ def update_node_provisioning_profile(self, mc: ManagedCluster) -> ManagedCluster return mc - def update_upstream_kubescheduler_user_configuration(self, mc: ManagedCluster) -> ManagedCluster: - """Update user-defined scheduler configuration for kube-scheduler upstream for the ManagedCluster object. + 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) - # this getter also validates that the enable and disable options are mutually exclusive - disable_user_configuration = self.context.get_disable_upstream_kubescheduler_user_configuration() - enable_user_configuration = self.context.get_enable_upstream_kubescheduler_user_configuration() + self.context.validate_byo_hosted_system_subnets() + if not self.context.get_enable_hosted_system(): + return mc - scheduler_config_mode = None - if enable_user_configuration: - scheduler_config_mode = self.models.SchedulerConfigMode.MANAGED_BY_CRD # pylint: disable=no-member - elif disable_user_configuration: - scheduler_config_mode = self.models.SchedulerConfigMode.DEFAULT # pylint: disable=no-member + if mc.hosted_system_profile is None: + mc.hosted_system_profile = self.models.ManagedClusterHostedSystemProfile() # pylint: disable=no-member + mc.hosted_system_profile.enabled = True - if scheduler_config_mode is not None: - if mc.scheduler_profile is None: - mc.scheduler_profile = self.models.SchedulerProfile() # pylint: disable=no-member - if mc.scheduler_profile.upstream is None: - mc.scheduler_profile.upstream = self.models.SchedulerInstanceProfile() # pylint: disable=no-member - mc.scheduler_profile.upstream.scheduler_config_mode = scheduler_config_mode + 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 @@ -10722,8 +10739,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 user-defined scheduler configuration for kube-scheduler upstream - mc = self.update_upstream_kubescheduler_user_configuration(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 ba118f12ae8..228b9b9d22e 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 @@ -16423,6 +16423,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(