Skip to content

Commit e3f5eb4

Browse files
authored
{AKS} Update validator for cluster auto-scaler profile (#20845)
* update validator for cluster_autoscaler_profile * remove old validator for cluster autoscaler profile * fix lint & sort import
1 parent 9f7d567 commit e3f5eb4

File tree

6 files changed

+195
-268
lines changed

6 files changed

+195
-268
lines changed

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from ._completers import (
1919
get_vm_size_completion_list, get_k8s_versions_completion_list, get_k8s_upgrades_completion_list, get_ossku_completion_list)
2020
from ._validators import (
21-
validate_cluster_autoscaler_profile, validate_create_parameters, validate_kubectl_version, validate_kubelogin_version, validate_k8s_version, validate_linux_host_name,
21+
validate_create_parameters, validate_kubectl_version, validate_kubelogin_version, validate_k8s_version, validate_linux_host_name,
2222
validate_list_of_integers, validate_ssh_key, validate_nodes_count,
2323
validate_nodepool_name, validate_vm_set_type, validate_load_balancer_sku, validate_load_balancer_outbound_ips,
2424
validate_priority, validate_eviction_policy, validate_spot_max_price,
@@ -234,8 +234,7 @@ def load_arguments(self, _):
234234
c.argument('outbound_type', arg_type=get_enum_type(outbound_types))
235235
c.argument('auto_upgrade_channel', arg_type=get_enum_type(auto_upgrade_channels))
236236
c.argument('enable_cluster_autoscaler', action='store_true')
237-
# TODO: replace "validate_cluster_autoscaler_profile" with "_extract_cluster_autoscaler_params"
238-
c.argument('cluster_autoscaler_profile', nargs='+', options_list=["--cluster-autoscaler-profile", "--ca-profile"], validator=validate_cluster_autoscaler_profile,
237+
c.argument('cluster_autoscaler_profile', nargs='+', options_list=["--cluster-autoscaler-profile", "--ca-profile"],
239238
help="Space-separated list of key=value pairs for configuring cluster autoscaler. Pass an empty string to clear the profile.")
240239
c.argument('min_count', type=int, validator=validate_nodes_count)
241240
c.argument('max_count', type=int, validator=validate_nodes_count)
@@ -316,8 +315,7 @@ def load_arguments(self, _):
316315
"--disable-cluster-autoscaler", "-d"], action='store_true')
317316
c.argument('update_cluster_autoscaler', options_list=[
318317
"--update-cluster-autoscaler", "-u"], action='store_true')
319-
# TODO: replace "validate_cluster_autoscaler_profile" with "_extract_cluster_autoscaler_params
320-
c.argument('cluster_autoscaler_profile', nargs='+', options_list=["--cluster-autoscaler-profile", "--ca-profile"], validator=validate_cluster_autoscaler_profile,
318+
c.argument('cluster_autoscaler_profile', nargs='+', options_list=["--cluster-autoscaler-profile", "--ca-profile"],
321319
help="Space-separated list of key=value pairs for configuring cluster autoscaler. Pass an empty string to clear the profile.")
322320
c.argument('min_count', type=int, validator=validate_nodes_count)
323321
c.argument('max_count', type=int, validator=validate_nodes_count)

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

Lines changed: 32 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,19 @@
44
# --------------------------------------------------------------------------------------------
55

66
from __future__ import unicode_literals
7+
78
import os
89
import os.path
910
import re
10-
from math import isnan, isclose
1111
from ipaddress import ip_network
12+
from math import isclose, isnan
1213

13-
# pylint: disable=no-name-in-module,import-error
14-
from knack.log import get_logger
15-
16-
from azure.cli.core.profiles import ResourceType
17-
14+
from azure.cli.core import keys
15+
from azure.cli.core.azclierror import InvalidArgumentValueError
1816
from azure.cli.core.commands.validators import validate_tag
1917
from azure.cli.core.util import CLIError
20-
from azure.cli.core.azclierror import InvalidArgumentValueError
21-
from azure.cli.core import keys
22-
18+
# pylint: disable=no-name-in-module,import-error
19+
from knack.log import get_logger
2320

2421
logger = get_logger(__name__)
2522

@@ -108,39 +105,6 @@ def validate_k8s_version(namespace):
108105
'such as "1.11.8" or "1.12.6"')
109106

110107

111-
def validate_cluster_autoscaler_profile(cmd, namespace):
112-
""" Validates that cluster autoscaler profile is acceptable by:
113-
1. Extracting the key[=value] format to map
114-
2. Validating that the key isn't empty and that the key is valid
115-
Empty strings pass validation
116-
"""
117-
_extract_cluster_autoscaler_params(namespace)
118-
if namespace.cluster_autoscaler_profile is not None:
119-
for key in namespace.cluster_autoscaler_profile.keys():
120-
_validate_cluster_autoscaler_key(cmd, key)
121-
122-
123-
def _validate_cluster_autoscaler_key(cmd, key):
124-
if not key:
125-
raise CLIError('Empty key specified for cluster-autoscaler-profile')
126-
ManagedClusterPropertiesAutoScalerProfile = cmd.get_models('ManagedClusterPropertiesAutoScalerProfile',
127-
resource_type=ResourceType.MGMT_CONTAINERSERVICE,
128-
operation_group='managed_clusters')
129-
valid_keys = list(k.replace("_", "-") for k, v in ManagedClusterPropertiesAutoScalerProfile._attribute_map.items()) # pylint: disable=protected-access
130-
if key not in valid_keys:
131-
raise CLIError("'{0}' is an invalid key for cluster-autoscaler-profile. "
132-
"Valid keys are {1}.".format(key, ', '.join(valid_keys)))
133-
134-
135-
def _extract_cluster_autoscaler_params(namespace):
136-
""" Extracts multiple space-separated cluster autoscaler parameters in key[=value] format """
137-
if isinstance(namespace.cluster_autoscaler_profile, list):
138-
params_dict = {}
139-
for item in namespace.cluster_autoscaler_profile:
140-
params_dict.update(validate_tag(item))
141-
namespace.cluster_autoscaler_profile = params_dict
142-
143-
144108
def validate_nodepool_name(namespace):
145109
"""Validates a nodepool name to be at most 12 characters, alphanumeric only."""
146110
if namespace.nodepool_name != "":
@@ -436,16 +400,18 @@ def extract_comma_separated_string(
436400
raw_string,
437401
enable_strip=False,
438402
extract_kv=False,
439-
key_only=False,
403+
allow_empty_value=False,
440404
keep_none=False,
441405
default_value=None,
442406
):
443407
"""Extract comma-separated string.
444408
445409
If enable_strip is specified, will remove leading and trailing whitespace before each operation on the string.
446-
If extract_kv is specified, will extract key value pair from the string, otherwise keep the entire string.
447-
Option key_only is valid since extract_kv is specified, when the number of string segments split by "=" is not 2,
448-
only the first segment is retained as the key.
410+
If extract_kv is specified, will extract key value pairs from the string with "=" as the delimiter and this would
411+
return a dictionary, otherwise keep the entire string.
412+
Option allow_empty_value is valid since extract_kv is specified. When the number of string segments split by "="
413+
is 1, the first segment is retained as the key and empty string would be set as its corresponding value without
414+
raising an exception.
449415
If keep_none is specified, will return None when input is None. Otherwise will return default_value if input is
450416
None or empty string.
451417
"""
@@ -463,25 +429,29 @@ def extract_comma_separated_string(
463429
if enable_strip:
464430
item = item.strip()
465431
if extract_kv:
466-
kv = item.split("=")
467-
if key_only:
468-
if enable_strip:
469-
result[kv[0].strip()] = ""
470-
else:
471-
result[kv[0]] = ""
472-
else:
473-
if len(kv) == 2:
474-
if enable_strip:
475-
result[kv[0].strip()] = kv[1].strip()
476-
else:
477-
result[kv[0]] = kv[1]
478-
else:
432+
kv_list = item.split("=")
433+
if len(kv_list) in [1, 2]:
434+
key = kv_list[0]
435+
value = ""
436+
if len(kv_list) == 2:
437+
value = kv_list[1]
438+
if not allow_empty_value and (value == "" or value.isspace()):
479439
raise InvalidArgumentValueError(
480-
"The format of '{}' in '{}' is incorrect, correct format should be "
481-
"'Key1=Value1,Key2=Value2'.".format(
482-
item, raw_string
440+
"Empty value not allowed. The value '{}' of key '{}' in '{}' is empty. Raw input '{}'.".format(
441+
value, key, item, raw_string
483442
)
484443
)
444+
if enable_strip:
445+
key = key.strip()
446+
value = value.strip()
447+
result[key] = value
448+
else:
449+
raise InvalidArgumentValueError(
450+
"The format of '{}' in '{}' is incorrect, correct format should be "
451+
"'Key1=Value1,Key2=Value2'.".format(
452+
item, raw_string
453+
)
454+
)
485455
else:
486456
result.append(item)
487457
return result

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

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,56 @@ def __validate_counts_in_autoscaler(
529529
)
530530
)
531531

532+
def __validate_cluster_autoscaler_profile(
533+
self, cluster_autoscaler_profile: Union[List, Dict, None]
534+
) -> Union[Dict, None]:
535+
"""Helper function to parse and verify cluster_autoscaler_profile.
536+
537+
If the user input is a list, parse it with function "extract_comma_separated_string". If the type of user input
538+
or parsed value is not a dictionary, raise an InvalidArgumentValueError. Otherwise, take the keys from the
539+
attribute map of ManagedClusterPropertiesAutoScalerProfile to verify whether the keys in the key-value pairs
540+
provided by the user are valid. If not, raise an InvalidArgumentValueError.
541+
542+
:return: dictionary or None
543+
"""
544+
if cluster_autoscaler_profile is not None:
545+
# convert list to dict
546+
if isinstance(cluster_autoscaler_profile, list):
547+
params_dict = {}
548+
for item in cluster_autoscaler_profile:
549+
params_dict.update(
550+
extract_comma_separated_string(
551+
item,
552+
extract_kv=True,
553+
allow_empty_value=True,
554+
default_value={},
555+
)
556+
)
557+
cluster_autoscaler_profile = params_dict
558+
# check if the type is dict
559+
if not isinstance(cluster_autoscaler_profile, dict):
560+
raise InvalidArgumentValueError(
561+
"Unexpected input cluster-autoscaler-profile, value: '{}', type '{}'.".format(
562+
cluster_autoscaler_profile,
563+
type(cluster_autoscaler_profile),
564+
)
565+
)
566+
# verify keys
567+
# pylint: disable=protected-access
568+
valid_keys = list(
569+
k.replace("_", "-") for k in self.models.ManagedClusterPropertiesAutoScalerProfile._attribute_map.keys()
570+
)
571+
for key in cluster_autoscaler_profile.keys():
572+
if not key:
573+
raise InvalidArgumentValueError("Empty key specified for cluster-autoscaler-profile")
574+
if key not in valid_keys:
575+
raise InvalidArgumentValueError(
576+
"'{}' is an invalid key for cluster-autoscaler-profile. Valid keys are {}.".format(
577+
key, ", ".join(valid_keys)
578+
)
579+
)
580+
return cluster_autoscaler_profile
581+
532582
def get_subscription_id(self):
533583
"""Helper function to obtain the value of subscription_id.
534584
@@ -4077,42 +4127,20 @@ def get_node_osdisk_diskencryptionset_id(self) -> Union[str, None]:
40774127
def _get_cluster_autoscaler_profile(self, read_only: bool = False) -> Union[Dict[str, str], None]:
40784128
"""Internal function to dynamically obtain the value of cluster_autoscaler_profile according to the context.
40794129
4130+
This function will call function "__validate_cluster_autoscaler_profile" to parse and verify the parameter
4131+
by default.
4132+
40804133
In update mode, when cluster_autoscaler_profile is assigned and auto_scaler_profile in the `mc` object has also
40814134
been set, dynamic completion will be triggerd. We will first make a copy of the original configuration
40824135
(extract the dictionary from the ManagedClusterPropertiesAutoScalerProfile object), and then update the copied
40834136
dictionary with the dictionary of new options.
40844137
4085-
This function will verify the parameter by default. If the user input is not empty, take the keys from the
4086-
attribute map of ManagedClusterPropertiesAutoScalerProfile to verify whether the keys in the key-value pairs
4087-
provided by the user are valid. If not, raise an InvalidArgumentValueError. The value of the raw parameter
4088-
should be a dictionary after being processed by the validator. If not, raise a CLIInternalError.
4089-
40904138
:return: dictionary or None
40914139
"""
40924140
# read the original value passed by the command
40934141
cluster_autoscaler_profile = self.raw_param.get("cluster_autoscaler_profile")
4094-
# validate user input (replace function "_validate_cluster_autoscaler_key" in file "_validators.py")
4095-
if cluster_autoscaler_profile is not None:
4096-
if not isinstance(cluster_autoscaler_profile, dict):
4097-
raise CLIInternalError(
4098-
"Unexpected input cluster-autoscaler-profile, value: '{}', type '{}'.".format(
4099-
cluster_autoscaler_profile,
4100-
type(cluster_autoscaler_profile),
4101-
)
4102-
)
4103-
# pylint: disable=protected-access
4104-
valid_keys = list(
4105-
k.replace("_", "-") for k in self.models.ManagedClusterPropertiesAutoScalerProfile._attribute_map.keys()
4106-
)
4107-
for key in cluster_autoscaler_profile.keys():
4108-
if not key:
4109-
raise InvalidArgumentValueError("Empty key specified for cluster-autoscaler-profile")
4110-
if key not in valid_keys:
4111-
raise InvalidArgumentValueError(
4112-
"'{}' is an invalid key for cluster-autoscaler-profile. Valid keys are {}.".format(
4113-
key, ", ".join(valid_keys)
4114-
)
4115-
)
4142+
# parse and validate user input
4143+
cluster_autoscaler_profile = self.__validate_cluster_autoscaler_profile(cluster_autoscaler_profile)
41164144

41174145
# In create mode, try to read the property value corresponding to the parameter from the `mc` object.
41184146
if self.decorator_mode == DecoratorMode.CREATE:
@@ -4141,16 +4169,14 @@ def _get_cluster_autoscaler_profile(self, read_only: bool = False) -> Union[Dict
41414169
def get_cluster_autoscaler_profile(self) -> Union[Dict[str, str], None]:
41424170
"""Dynamically obtain the value of cluster_autoscaler_profile according to the context.
41434171
4172+
This function will call function "__validate_cluster_autoscaler_profile" to parse and verify the parameter
4173+
by default.
4174+
41444175
In update mode, when cluster_autoscaler_profile is assigned and auto_scaler_profile in the `mc` object has also
41454176
been set, dynamic completion will be triggerd. We will first make a copy of the original configuration
41464177
(extract the dictionary from the ManagedClusterPropertiesAutoScalerProfile object), and then update the copied
41474178
dictionary with the dictionary of new options.
41484179
4149-
This function will verify the parameter by default. If the user input is not empty, take the keys from the
4150-
attribute map of ManagedClusterPropertiesAutoScalerProfile to verify whether the keys in the key-value pairs
4151-
provided by the user are valid. If not, raise an InvalidArgumentValueError. The value of the raw parameter
4152-
should be a dictionary after being processed by the validator. If not, raise a CLIInternalError.
4153-
41544180
:return: dictionary or None
41554181
"""
41564182
return self._get_cluster_autoscaler_profile()

src/azure-cli/azure/cli/command_modules/acs/tests/hybrid_2020_09_01/test_validators.py

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -96,59 +96,6 @@ def test_IPv6(self):
9696
self.assertEqual(str(cm.exception), err)
9797

9898

99-
class TestClusterAutoscalerParamsValidators(unittest.TestCase):
100-
def setUp(self):
101-
self.cli = MockCLI()
102-
103-
def test_empty_key_empty_value(self):
104-
cluster_autoscaler_profile = ["="]
105-
namespace = Namespace(cluster_autoscaler_profile=cluster_autoscaler_profile)
106-
err = "Empty key specified for cluster-autoscaler-profile"
107-
108-
with self.assertRaises(CLIError) as cm:
109-
validators.validate_cluster_autoscaler_profile(MockCmd(self.cli), namespace)
110-
self.assertEqual(str(cm.exception), err)
111-
112-
def test_non_empty_key_empty_value(self):
113-
cluster_autoscaler_profile = ["scan-interval="]
114-
namespace = Namespace(cluster_autoscaler_profile=cluster_autoscaler_profile)
115-
116-
validators.validate_cluster_autoscaler_profile(MockCmd(self.cli), namespace)
117-
118-
def test_two_empty_keys_empty_value(self):
119-
cluster_autoscaler_profile = ["=", "="]
120-
namespace = Namespace(cluster_autoscaler_profile=cluster_autoscaler_profile)
121-
err = "Empty key specified for cluster-autoscaler-profile"
122-
123-
with self.assertRaises(CLIError) as cm:
124-
validators.validate_cluster_autoscaler_profile(MockCmd(self.cli), namespace)
125-
self.assertEqual(str(cm.exception), err)
126-
127-
def test_one_empty_key_in_pair_one_non_empty(self):
128-
cluster_autoscaler_profile = ["scan-interval=20s", "="]
129-
namespace = Namespace(cluster_autoscaler_profile=cluster_autoscaler_profile)
130-
err = "Empty key specified for cluster-autoscaler-profile"
131-
132-
with self.assertRaises(CLIError) as cm:
133-
validators.validate_cluster_autoscaler_profile(MockCmd(self.cli), namespace)
134-
self.assertEqual(str(cm.exception), err)
135-
136-
def test_invalid_key(self):
137-
cluster_autoscaler_profile = ["bad-key=val"]
138-
namespace = Namespace(cluster_autoscaler_profile=cluster_autoscaler_profile)
139-
err = "'bad-key' is an invalid key for cluster-autoscaler-profile"
140-
141-
with self.assertRaises(CLIError) as cm:
142-
validators.validate_cluster_autoscaler_profile(MockCmd(self.cli), namespace)
143-
self.assertIn(err, str(cm.exception),)
144-
145-
def test_valid_parameters(self):
146-
cluster_autoscaler_profile = ["scan-interval=20s", "scale-down-delay-after-add=15m"]
147-
namespace = Namespace(cluster_autoscaler_profile=cluster_autoscaler_profile)
148-
149-
validators.validate_cluster_autoscaler_profile(MockCmd(self.cli), namespace)
150-
151-
15299
class Namespace:
153100
def __init__(self, api_server_authorized_ip_ranges=None, cluster_autoscaler_profile=None):
154101
self.api_server_authorized_ip_ranges = api_server_authorized_ip_ranges

0 commit comments

Comments
 (0)