Skip to content

Commit 3718586

Browse files
authored
{AKS} Refactor aks nodepool update - part 1 (#22050)
* sort params * refactor auto scaler, label, tag, taint * add test cases for getters * add more test cases * add more tests * fix lint
1 parent 06ed91d commit 3718586

File tree

4 files changed

+947
-154
lines changed

4 files changed

+947
-154
lines changed

src/azure-cli/azure/cli/command_modules/acs/_params.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -448,16 +448,16 @@ def load_arguments(self, _):
448448
c.argument('enable_cluster_autoscaler', options_list=["--enable-cluster-autoscaler", "-e"], action='store_true')
449449
c.argument('min_count', type=int, validator=validate_nodes_count)
450450
c.argument('max_count', type=int, validator=validate_nodes_count)
451-
c.argument('scale_down_mode', arg_type=get_enum_type([CONST_SCALE_DOWN_MODE_DELETE, CONST_SCALE_DOWN_MODE_DEALLOCATE]))
452451
c.argument('priority', arg_type=get_enum_type(node_priorities), validator=validate_priority)
453452
c.argument('eviction_policy', arg_type=get_enum_type(node_eviction_policies), validator=validate_eviction_policy)
454453
c.argument('spot_max_price', type=float, validator=validate_spot_max_price)
455454
c.argument('labels', nargs='*', validator=validate_nodepool_labels)
456455
c.argument('tags', tags_type)
457456
c.argument('node_taints', validator=validate_taints)
458-
c.argument('mode', get_enum_type(node_mode_types))
459457
c.argument('node_osdisk_type', arg_type=get_enum_type(node_os_disk_types))
460458
c.argument('node_osdisk_size', type=int)
459+
c.argument('mode', get_enum_type(node_mode_types))
460+
c.argument('scale_down_mode', arg_type=get_enum_type([CONST_SCALE_DOWN_MODE_DELETE, CONST_SCALE_DOWN_MODE_DEALLOCATE]))
461461
c.argument('max_surge', validator=validate_max_surge)
462462
c.argument('max_pods', type=int, options_list=['--max-pods', '-m'])
463463
c.argument('zones', zones_type, options_list=['--zones', '-z'], help='Space-separated list of availability zones where agent nodes will be placed.')
@@ -478,11 +478,11 @@ def load_arguments(self, _):
478478
"--update-cluster-autoscaler", "-u"], action='store_true')
479479
c.argument('min_count', type=int, validator=validate_nodes_count)
480480
c.argument('max_count', type=int, validator=validate_nodes_count)
481-
c.argument('scale_down_mode', arg_type=get_enum_type([CONST_SCALE_DOWN_MODE_DELETE, CONST_SCALE_DOWN_MODE_DEALLOCATE]))
482481
c.argument('labels', nargs='*', validator=validate_nodepool_labels)
483482
c.argument('tags', tags_type)
484483
c.argument('node_taints', validator=validate_taints)
485484
c.argument('mode', get_enum_type(node_mode_types))
485+
c.argument('scale_down_mode', arg_type=get_enum_type([CONST_SCALE_DOWN_MODE_DELETE, CONST_SCALE_DOWN_MODE_DEALLOCATE]))
486486
c.argument('max_surge', validator=validate_max_surge)
487487

488488
with self.argument_context('aks nodepool upgrade') as c:

src/azure-cli/azure/cli/command_modules/acs/agentpool_decorator.py

Lines changed: 251 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,21 @@
2020
CONST_SPOT_EVICTION_POLICY_DELETE,
2121
CONST_VIRTUAL_MACHINE_SCALE_SETS,
2222
AgentPoolDecoratorMode,
23+
DecoratorEarlyExitException,
2324
DecoratorMode,
2425
)
2526
from azure.cli.command_modules.acs._helpers import get_snapshot_by_snapshot_id
2627
from azure.cli.command_modules.acs._validators import extract_comma_separated_string
2728
from azure.cli.command_modules.acs.base_decorator import BaseAKSContext, BaseAKSModels, BaseAKSParamDict
29+
from azure.cli.command_modules.acs.decorator import safe_list_get
2830
from azure.cli.core import AzCommandsLoader
29-
from azure.cli.core.azclierror import CLIInternalError, InvalidArgumentValueError, RequiredArgumentMissingError
31+
from azure.cli.core.azclierror import (
32+
ArgumentUsageError,
33+
CLIInternalError,
34+
InvalidArgumentValueError,
35+
MutuallyExclusiveArgumentError,
36+
RequiredArgumentMissingError,
37+
)
3038
from azure.cli.core.commands import AzCliCommand
3139
from azure.cli.core.profiles import ResourceType
3240
from azure.cli.core.util import get_file_json, sdk_no_wait
@@ -93,6 +101,7 @@ def __init__(
93101
super().__init__(cmd, raw_parameters, models, decorator_mode)
94102
self.agentpool_decorator_mode = agentpool_decorator_mode
95103
self.agentpool = None
104+
self._agentpools = []
96105

97106
# pylint: disable=no-self-use
98107
def __validate_counts_in_autoscaler(
@@ -218,7 +227,10 @@ def _get_nodepool_name(self, enable_validation: bool = False) -> str:
218227
# this parameter does not need dynamic completion
219228
# validation
220229
if enable_validation:
221-
if self.agentpool_decorator_mode == AgentPoolDecoratorMode.STANDALONE:
230+
if (
231+
self.agentpool_decorator_mode == AgentPoolDecoratorMode.STANDALONE and
232+
self.decorator_mode == DecoratorMode.CREATE
233+
):
222234
agentpool_client = cf_agent_pools(self.cmd.cli_ctx)
223235
instances = agentpool_client.list(self.get_resource_group_name(), self.get_cluster_name())
224236
for agentpool_profile in instances:
@@ -265,7 +277,6 @@ def get_max_surge(self):
265277
# this parameter does not need validation
266278
return max_surge
267279

268-
# pylint: disable=too-many-branches
269280
def get_node_count_and_enable_cluster_autoscaler_min_max_count(
270281
self,
271282
) -> Tuple[int, bool, Union[int, None], Union[int, None]]:
@@ -316,6 +327,93 @@ def get_node_count_and_enable_cluster_autoscaler_min_max_count(
316327
)
317328
return node_count, enable_cluster_autoscaler, min_count, max_count
318329

330+
def get_update_enable_disable_cluster_autoscaler_and_min_max_count(
331+
self,
332+
) -> Tuple[bool, bool, bool, Union[int, None], Union[int, None]]:
333+
"""Obtain the value of update_cluster_autoscaler, enable_cluster_autoscaler, disable_cluster_autoscaler,
334+
min_count and max_count.
335+
336+
This function will verify the parameters through function "__validate_counts_in_autoscaler" by default. Besides
337+
if both enable_cluster_autoscaler and update_cluster_autoscaler are specified, a MutuallyExclusiveArgumentError
338+
will be raised. If enable_cluster_autoscaler or update_cluster_autoscaler is specified and there are multiple
339+
agent pool profiles, an ArgumentUsageError will be raised. If enable_cluster_autoscaler is specified and
340+
autoscaler is already enabled in `mc`, it will output warning messages and exit with code 0. If
341+
update_cluster_autoscaler is specified and autoscaler is not enabled in `mc`, it will raise an
342+
InvalidArgumentValueError. If disable_cluster_autoscaler is specified and autoscaler is not enabled in `mc`,
343+
it will output warning messages and exit with code 0.
344+
345+
:return: a tuple containing four elements: update_cluster_autoscaler of bool type, enable_cluster_autoscaler
346+
of bool type, disable_cluster_autoscaler of bool type, min_count of int type or None and max_count of int type
347+
or None
348+
"""
349+
# update_cluster_autoscaler
350+
# read the original value passed by the command
351+
update_cluster_autoscaler = self.raw_param.get("update_cluster_autoscaler", False)
352+
353+
# enable_cluster_autoscaler
354+
# read the original value passed by the command
355+
enable_cluster_autoscaler = self.raw_param.get("enable_cluster_autoscaler", False)
356+
357+
# disable_cluster_autoscaler
358+
# read the original value passed by the command
359+
disable_cluster_autoscaler = self.raw_param.get("disable_cluster_autoscaler", False)
360+
361+
# min_count
362+
# read the original value passed by the command
363+
min_count = self.raw_param.get("min_count")
364+
365+
# max_count
366+
# read the original value passed by the command
367+
max_count = self.raw_param.get("max_count")
368+
369+
# these parameters do not need dynamic completion
370+
371+
# validation
372+
if self.agentpool_decorator_mode == AgentPoolDecoratorMode.MANAGED_CLUSTER:
373+
# For multi-agent pool, use the az aks nodepool command
374+
if (enable_cluster_autoscaler or update_cluster_autoscaler) and len(self._agentpools) > 1:
375+
raise ArgumentUsageError(
376+
'There are more than one node pool in the cluster. Please use "az aks nodepool" command '
377+
"to update per node pool auto scaler settings"
378+
)
379+
380+
if enable_cluster_autoscaler + update_cluster_autoscaler + disable_cluster_autoscaler > 1:
381+
raise MutuallyExclusiveArgumentError(
382+
"Can only specify one of --enable-cluster-autoscaler, --update-cluster-autoscaler and "
383+
"--disable-cluster-autoscaler"
384+
)
385+
386+
self.__validate_counts_in_autoscaler(
387+
None,
388+
enable_cluster_autoscaler or update_cluster_autoscaler,
389+
min_count,
390+
max_count,
391+
decorator_mode=DecoratorMode.UPDATE,
392+
)
393+
394+
if enable_cluster_autoscaler and self.agentpool.enable_auto_scaling:
395+
logger.warning(
396+
"Cluster autoscaler is already enabled for this node pool.\n"
397+
'Please run "az aks --update-cluster-autoscaler" '
398+
"if you want to update min-count or max-count."
399+
)
400+
raise DecoratorEarlyExitException()
401+
402+
if update_cluster_autoscaler and not self.agentpool.enable_auto_scaling:
403+
raise InvalidArgumentValueError(
404+
"Cluster autoscaler is not enabled for this node pool.\n"
405+
'Run "az aks nodepool update --enable-cluster-autoscaler" '
406+
"to enable cluster with min-count and max-count."
407+
)
408+
409+
if disable_cluster_autoscaler and not self.agentpool.enable_auto_scaling:
410+
logger.warning(
411+
"Cluster autoscaler is already disabled for this node pool."
412+
)
413+
raise DecoratorEarlyExitException()
414+
415+
return update_cluster_autoscaler, enable_cluster_autoscaler, disable_cluster_autoscaler, min_count, max_count
416+
319417
def get_node_osdisk_size(self) -> Union[int, None]:
320418
"""Obtain the value of node_osdisk_size.
321419
@@ -583,9 +681,10 @@ def get_nodepool_labels(self) -> Union[Dict[str, str], None]:
583681
else:
584682
nodepool_labels = self.raw_param.get("labels")
585683

586-
# try to read the property value corresponding to the parameter from the `agentpool` object
587-
if self.agentpool and self.agentpool.node_labels is not None:
588-
nodepool_labels = self.agentpool.node_labels
684+
# In create mode, try to read the property value corresponding to the parameter from the `agentpool` object
685+
if self.decorator_mode == DecoratorMode.CREATE:
686+
if self.agentpool and self.agentpool.node_labels is not None:
687+
nodepool_labels = self.agentpool.node_labels
589688

590689
# this parameter does not need dynamic completion
591690
# this parameter does not need validation
@@ -602,9 +701,10 @@ def get_nodepool_tags(self) -> Union[Dict[str, str], None]:
602701
else:
603702
nodepool_tags = self.raw_param.get("tags")
604703

605-
# try to read the property value corresponding to the parameter from the `agentpool` object
606-
if self.agentpool and self.agentpool.tags is not None:
607-
nodepool_tags = self.agentpool.tags
704+
# In create mode, try to read the property value corresponding to the parameter from the `agentpool` object
705+
if self.decorator_mode == DecoratorMode.CREATE:
706+
if self.agentpool and self.agentpool.tags is not None:
707+
nodepool_tags = self.agentpool.tags
608708

609709
# this parameter does not need dynamic completion
610710
# this parameter does not need validation
@@ -617,13 +717,17 @@ def get_node_taints(self) -> Union[List[str], None]:
617717
"""
618718
# read the original value passed by the command
619719
node_taints = self.raw_param.get("node_taints")
620-
# normalize, keep None as None
720+
# normalize, default is an empty list
621721
if node_taints is not None:
622722
node_taints = [x.strip() for x in (node_taints.split(",") if node_taints else [])]
723+
# keep None as None for update mode
724+
if node_taints is None and self.decorator_mode == DecoratorMode.CREATE:
725+
node_taints = []
623726

624-
# try to read the property value corresponding to the parameter from the `agentpool` object
625-
if self.agentpool and self.agentpool.node_taints is not None:
626-
node_taints = self.agentpool.node_taints
727+
# In create mode, try to read the property value corresponding to the parameter from the `agentpool` object
728+
if self.decorator_mode == DecoratorMode.CREATE:
729+
if self.agentpool and self.agentpool.node_taints is not None:
730+
node_taints = self.agentpool.node_taints
627731

628732
# this parameter does not need validation
629733
return node_taints
@@ -1228,8 +1332,8 @@ def set_up_custom_node_config(self, agentpool: AgentPool) -> AgentPool:
12281332
agentpool.linux_os_config = self.context.get_linux_os_config()
12291333
return agentpool
12301334

1231-
def construct_default_agentpool_profile(self) -> AgentPool:
1232-
"""The overall controller used to construct the default AgentPool profile.
1335+
def construct_agentpool_profile_default(self) -> AgentPool:
1336+
"""The overall controller used to construct the AgentPool profile by default.
12331337
12341338
The completely constructed AgentPool object will later be passed as a parameter to the underlying SDK
12351339
(mgmt-containerservice) to send the actual request.
@@ -1266,8 +1370,8 @@ def construct_default_agentpool_profile(self) -> AgentPool:
12661370
def add_agentpool(self, agentpool: AgentPool) -> AgentPool:
12671371
"""Send request to add a new agentpool.
12681372
1269-
The function "sdk_no_wait" will be called to use the ContainerServiceClient to send a reqeust to add a new agent
1270-
pool to the cluster.
1373+
The function "sdk_no_wait" will be called to use the Agentpool operations of ContainerServiceClient to send a
1374+
reqeust to add a new agent pool to the cluster.
12711375
12721376
:return: the AgentPool object
12731377
"""
@@ -1292,6 +1396,7 @@ def __init__(
12921396
client: AgentPoolsOperations,
12931397
raw_parameters: Dict,
12941398
resource_type: ResourceType,
1399+
agentpool_decorator_mode: AgentPoolDecoratorMode,
12951400
):
12961401
"""Internal controller of aks_agentpool_update.
12971402
@@ -1303,8 +1408,135 @@ def __init__(
13031408
"""
13041409
self.cmd = cmd
13051410
self.client = client
1306-
self.models = AKSAgentPoolModels(cmd, resource_type)
1411+
self.agentpool_decorator_mode = agentpool_decorator_mode
1412+
self.models = AKSAgentPoolModels(cmd, resource_type, agentpool_decorator_mode)
13071413
# store the context in the process of assemble the AgentPool object
13081414
self.context = AKSAgentPoolContext(
1309-
cmd, AKSAgentPoolParamDict(raw_parameters), self.models, decorator_mode=DecoratorMode.UPDATE
1415+
cmd, AKSAgentPoolParamDict(raw_parameters), self.models, DecoratorMode.UPDATE, agentpool_decorator_mode
1416+
)
1417+
1418+
def _ensure_agentpool(self, agentpool: AgentPool) -> None:
1419+
"""Internal function to ensure that the incoming `agentpool` object is valid and the same as the attached
1420+
`agentpool` object in the context.
1421+
1422+
If the incoming `agentpool` is not valid or is inconsistent with the `agentpool` in the context, raise a
1423+
CLIInternalError.
1424+
1425+
:return: None
1426+
"""
1427+
if not isinstance(agentpool, self.models.UnifiedAgentPoolModel):
1428+
raise CLIInternalError(
1429+
"Unexpected agentpool object with type '{}'.".format(type(agentpool))
1430+
)
1431+
1432+
if self.context.agentpool != agentpool:
1433+
raise CLIInternalError(
1434+
"Inconsistent state detected. The incoming `agentpool` "
1435+
"is not the same as the `agentpool` in the context."
1436+
)
1437+
1438+
# pylint: disable=protected-access
1439+
def fetch_agentpool(self, agentpools: List[AgentPool] = None) -> AgentPool:
1440+
"""Get the AgentPool object currently in use and attach it to internal context.
1441+
1442+
Internally send request using Agentpool operations of ContainerServiceClient.
1443+
1444+
:return: the AgentPool object
1445+
"""
1446+
if self.agentpool_decorator_mode == AgentPoolDecoratorMode.MANAGED_CLUSTER:
1447+
self.context._agentpools = agentpools
1448+
agentpool = safe_list_get(agentpools, 0)
1449+
else:
1450+
agentpool = self.client.get(
1451+
self.context.get_resource_group_name(),
1452+
self.context.get_cluster_name(),
1453+
self.context.get_nodepool_name(),
1454+
)
1455+
1456+
# attach agentpool to AKSAgentPoolContext
1457+
self.context.attach_agentpool(agentpool)
1458+
return agentpool
1459+
1460+
def update_auto_scaler_properties(self, agentpool: AgentPool) -> AgentPool:
1461+
"""Update auto scaler related properties for the Agentpool object.
1462+
1463+
:return: the Agentpool object
1464+
"""
1465+
self._ensure_agentpool(agentpool)
1466+
1467+
(
1468+
update_cluster_autoscaler,
1469+
enable_cluster_autoscaler,
1470+
disable_cluster_autoscaler,
1471+
min_count,
1472+
max_count,
1473+
) = (
1474+
self.context.get_update_enable_disable_cluster_autoscaler_and_min_max_count()
1475+
)
1476+
1477+
if update_cluster_autoscaler or enable_cluster_autoscaler:
1478+
agentpool.enable_auto_scaling = True
1479+
agentpool.min_count = min_count
1480+
agentpool.max_count = max_count
1481+
1482+
if disable_cluster_autoscaler:
1483+
agentpool.enable_auto_scaling = False
1484+
agentpool.min_count = None
1485+
agentpool.max_count = None
1486+
return agentpool
1487+
1488+
def update_label_tag_taint(self, agentpool: AgentPool) -> AgentPool:
1489+
"""Set up label, tag, taint for the AgentPool object.
1490+
1491+
:return: the AgentPool object
1492+
"""
1493+
self._ensure_agentpool(agentpool)
1494+
1495+
labels = self.context.get_nodepool_labels()
1496+
if labels is not None:
1497+
agentpool.node_labels = labels
1498+
1499+
tags = self.context.get_nodepool_tags()
1500+
if tags is not None:
1501+
agentpool.tags = tags
1502+
1503+
node_taints = self.context.get_node_taints()
1504+
if node_taints is not None:
1505+
agentpool.node_taints = node_taints
1506+
return agentpool
1507+
1508+
def update_agentpool_profile_default(self, agentpools: List[AgentPool] = None) -> AgentPool:
1509+
"""The overall controller used to update AgentPool profile by default.
1510+
1511+
The completely constructed AgentPool object will later be passed as a parameter to the underlying SDK
1512+
(mgmt-containerservice) to send the actual request.
1513+
1514+
:return: the AgentPool object
1515+
"""
1516+
# fetch the Agentpool object
1517+
agentpool = self.fetch_agentpool(agentpools)
1518+
# update auto scaler properties
1519+
agentpool = self.update_auto_scaler_properties(agentpool)
1520+
# update label, tag, taint
1521+
agentpool = self.update_label_tag_taint(agentpool)
1522+
return agentpool
1523+
1524+
def update_agentpool(self, agentpool: AgentPool) -> AgentPool:
1525+
"""Send request to add a new agentpool.
1526+
1527+
The function "sdk_no_wait" will be called to use the Agentpool operations of ContainerServiceClient to send a
1528+
reqeust to update an existing agent pool of the cluster.
1529+
1530+
:return: the AgentPool object
1531+
"""
1532+
self._ensure_agentpool(agentpool)
1533+
1534+
return sdk_no_wait(
1535+
self.context.get_no_wait(),
1536+
self.client.begin_create_or_update,
1537+
self.context.get_resource_group_name(),
1538+
self.context.get_cluster_name(),
1539+
self.context.get_nodepool_name(),
1540+
agentpool,
1541+
headers=self.context.get_aks_custom_headers(),
13101542
)

0 commit comments

Comments
 (0)