Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/azure-cli/azure/cli/command_modules/acs/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <systemNodeSubnetID> --node-subnet-id <nodeSubnetID>
"""

helps["aks delete"] = """
Expand Down
3 changes: 3 additions & 0 deletions src/azure-cli/azure/cli/command_modules/acs/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
4 changes: 4 additions & 0 deletions src/azure-cli/azure/cli/command_modules/acs/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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")
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading