diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index a35234b0134..000e3d0daad 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -487,6 +487,9 @@ - name: --public-ip-per-vm type: bool short-summary: Each node will have a public ip. + - name: --labels + type: string + short-summary: The node labels for the node pool. You can't change the node labels through CLI after the node pool is created. See https://aka.ms/node-labels for syntax of labels. """ helps['aks nodepool scale'] = """ diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index af372eb1bc2..338a4d947f6 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -21,7 +21,7 @@ validate_nodepool_name, validate_vm_set_type, validate_load_balancer_sku, validate_load_balancer_outbound_ips, validate_load_balancer_outbound_ip_prefixes, validate_taints, validate_priority, validate_eviction_policy, validate_spot_max_price, validate_acr, validate_user, - validate_load_balancer_outbound_ports, validate_load_balancer_idle_timeout, validate_nodepool_tags) + validate_load_balancer_outbound_ports, validate_load_balancer_idle_timeout, validate_nodepool_tags, validate_nodepool_labels) from ._consts import CONST_OUTBOUND_TYPE_LOAD_BALANCER, \ CONST_OUTBOUND_TYPE_USER_DEFINED_ROUTING, CONST_SCALE_SET_PRIORITY_REGULAR, CONST_SCALE_SET_PRIORITY_SPOT, \ CONST_SPOT_EVICTION_POLICY_DELETE, CONST_SPOT_EVICTION_POLICY_DEALLOCATE @@ -53,6 +53,7 @@ def load_arguments(self, _): c.argument('nodepool_name', type=str, default='nodepool1', help='Node pool name, upto 12 alphanumeric characters', validator=validate_nodepool_name) c.argument('nodepool_tags', nargs='*', validator=validate_nodepool_tags, help='space-separated tags: key[=value] [key[=value] ...]. Use "" to clear existing tags.') + c.argument('nodepool_labels', nargs='*', validator=validate_nodepool_labels, help='space-separated labels: key[=value] [key[=value] ...]. You can not change the node labels through CLI after creation. See https://aka.ms/node-labels for syntax of labels.') c.argument('ssh_key_value', required=False, type=file_type, default=os.path.join('~', '.ssh', 'id_rsa.pub'), completer=FilesCompleter(), validator=validate_ssh_key) c.argument('aad_client_app_id') @@ -138,6 +139,7 @@ def load_arguments(self, _): c.argument('priority', arg_type=get_enum_type([CONST_SCALE_SET_PRIORITY_REGULAR, CONST_SCALE_SET_PRIORITY_SPOT]), validator=validate_priority) c.argument('eviction_policy', arg_type=get_enum_type([CONST_SPOT_EVICTION_POLICY_DELETE, CONST_SPOT_EVICTION_POLICY_DEALLOCATE]), validator=validate_eviction_policy) c.argument('spot_max_price', type=float, validator=validate_spot_max_price) + c.argument('labels', nargs='*', validator=validate_nodepool_labels) for scope in ['aks nodepool show', 'aks nodepool delete', 'aks nodepool scale', 'aks nodepool upgrade', 'aks nodepool update']: with self.argument_context(scope) as c: diff --git a/src/aks-preview/azext_aks_preview/_validators.py b/src/aks-preview/azext_aks_preview/_validators.py index 07881e4fcaf..8776a9925e0 100644 --- a/src/aks-preview/azext_aks_preview/_validators.py +++ b/src/aks-preview/azext_aks_preview/_validators.py @@ -291,3 +291,75 @@ def _extract_cluster_autoscaler_params(namespace): for item in namespace.cluster_autoscaler_profile: params_dict.update(validate_tag(item)) namespace.cluster_autoscaler_profile = params_dict + + +def validate_nodepool_labels(namespace): + """Validates that provided node labels is a valid format""" + + if hasattr(namespace, 'nodepool_labels'): + labels = namespace.nodepool_labels + else: + labels = namespace.labels + + if labels is None: + return + + if isinstance(labels, list): + labels_dict = {} + for item in labels: + labels_dict.update(validate_label(item)) + after_validation_labels = labels_dict + else: + after_validation_labels = validate_label(labels) + + if hasattr(namespace, 'nodepool_labels'): + namespace.nodepool_labels = after_validation_labels + else: + namespace.labels = after_validation_labels + + +def validate_label(label): + """Validates that provided label is a valid format""" + prefix_regex = re.compile(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$") + name_regex = re.compile(r"^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$") + value_regex = re.compile(r"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$") + + if label == "": + return {} + kv = label.split('=') + if len(kv) != 2: + raise CLIError("Invalid label: %s. Label definition must be of format name=value." % label) + name_parts = kv[0].split('/') + if len(name_parts) == 1: + name = name_parts[0] + elif len(name_parts) == 2: + prefix = name_parts[0] + if not prefix or len(prefix) > 253: + raise CLIError("Invalid label: %s. Label prefix can't be empty or more than 253 chars." % label) + if not prefix_regex.match(prefix): + raise CLIError("Invalid label: %s. Prefix part a DNS-1123 label must consist of lower case alphanumeric " + "characters or '-', and must start and end with an alphanumeric character" % label) + name = name_parts[1] + else: + raise CLIError("Invalid label: %s. A qualified name must consist of alphanumeric characters, '-', '_' " + "or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or " + "'my.name', or '123-abc') with an optional DNS subdomain prefix and '/' " + "(e.g. 'example.com/MyName')" % label) + + # validate label name + if not name or len(name) > 63: + raise CLIError("Invalid label: %s. Label name can't be empty or more than 63 chars." % label) + if not name_regex.match(name): + raise CLIError("Invalid label: %s. A qualified name must consist of alphanumeric characters, '-', '_' " + "or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or " + "'my.name', or '123-abc') with an optional DNS subdomain prefix and '/' (e.g. " + "'example.com/MyName')" % label) + + # validate label value + if len(kv[1]) > 63: + raise CLIError("Invalid label: %s. Label must be more than 63 chars." % label) + if not value_regex.match(kv[1]): + raise CLIError("Invalid label: %s. A valid label must be an empty string or consist of alphanumeric " + "characters, '-', '_' or '.', and must start and end with an alphanumeric character" % label) + + return {kv[0]: kv[1]} diff --git a/src/aks-preview/azext_aks_preview/custom.py b/src/aks-preview/azext_aks_preview/custom.py index d0100da4a2f..c2b04a8e7f1 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -697,6 +697,7 @@ def aks_create(cmd, # pylint: disable=too-many-locals,too-many-statements,to node_count=3, nodepool_name="nodepool1", nodepool_tags=None, + nodepool_labels=None, service_principal=None, client_secret=None, no_ssh_key=False, disable_rbac=None, @@ -779,6 +780,7 @@ def aks_create(cmd, # pylint: disable=too-many-locals,too-many-statements,to agent_pool_profile = ManagedClusterAgentPoolProfile( name=_trim_nodepoolname(nodepool_name), # Must be 12 chars or less before ACS RP adds to it tags=nodepool_tags, + node_labels=nodepool_labels, count=int(node_count), vm_size=node_vm_size, os_type="Linux", @@ -2031,6 +2033,7 @@ def aks_agentpool_add(cmd, # pylint: disable=unused-argument,too-many-local eviction_policy=CONST_SPOT_EVICTION_POLICY_DELETE, spot_max_price=float('nan'), public_ip_per_vm=False, + labels=None, no_wait=False): instances = client.list(resource_group_name, cluster_name) for agentpool_profile in instances: @@ -2057,6 +2060,7 @@ def aks_agentpool_add(cmd, # pylint: disable=unused-argument,too-many-local agent_pool = AgentPool( name=nodepool_name, tags=tags, + node_labels=labels, count=int(node_count), vm_size=node_vm_size, os_type=os_type,