diff --git a/linter_exclusions.yml b/linter_exclusions.yml index 9a603f25ac7..3ddcd7800ed 100644 --- a/linter_exclusions.yml +++ b/linter_exclusions.yml @@ -37,6 +37,9 @@ aks create: node_public_ip_prefix_id: rule_exclusions: - option_length_too_long + node_public_ip_prefix_ids: + rule_exclusions: + - option_length_too_long enable_private_cluster: rule_exclusions: - option_length_too_long @@ -130,6 +133,11 @@ aks create: service_account_image_pull_default_managed_identity_id: rule_exclusions: - option_length_too_long +aks nodepool add: + parameters: + node_public_ip_prefix_ids: + rule_exclusions: + - option_length_too_long aks update: parameters: aad_admin_group_object_ids: diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index 792ef11e661..44ffe3eeb36 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -13,6 +13,11 @@ Pending +++++++ * `az aks create`: Add `--control-plane-scaling-size` parameter to configure control plane scaling profile with available sizes 'H2', 'H4', and 'H8'. +20.0.0b5 ++++++++ +* `az aks create`: Add `--node-public-ip-prefix-ids` parameter for specifying dual-stack (IPv4/IPv6) public IP prefixes for instance-level public IPs. +* `az aks nodepool add`: Add `--node-public-ip-prefix-ids` parameter for specifying dual-stack (IPv4/IPv6) public IP prefixes for instance-level public IPs. + 20.0.0b4 +++++++ * `az aks nodepool update`: Support `--node-vm-size` to resize VM size of an existing VMSS-based agent pool (preview). Requires AFEC registration `Microsoft.ContainerService/AgentPoolVMSSResize`. diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index 300f74b5791..bbec4bb6c7b 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -358,6 +358,10 @@ - name: --node-public-ip-prefix-id type: string short-summary: Public IP prefix ID used to assign public IPs to VMSS nodes. + - name: --node-public-ip-prefix-ids + type: string + short-summary: Comma-separated list of public IP prefix resource IDs for dual-stack node public IPs (IPv4 and/or IPv6). + long-summary: At most one IPv4 and one IPv6 prefix may be specified. Automatically enables --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. Requires the NodePublicIPv6PrefixPreview feature flag. - name: --enable-managed-identity type: bool short-summary: Using managed identity to manage cluster resource group. You can explicitly specify "--service-principal" and "--client-secret" to disable managed identity, otherwise it will be enabled. @@ -2155,6 +2159,10 @@ - name: --node-public-ip-prefix-id type: string short-summary: Public IP prefix ID used to assign public IPs to VMSS nodes. Must use VMSS agent pool type. + - name: --node-public-ip-prefix-ids + type: string + short-summary: Comma-separated list of public IP prefix resource IDs for dual-stack node public IPs (IPv4 and/or IPv6). + long-summary: At most one IPv4 and one IPv6 prefix may be specified. Automatically enables --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. Requires the NodePublicIPv6PrefixPreview feature flag. - name: --labels type: string short-summary: The node labels for the node pool. See https://aka.ms/node-labels for syntax of labels. diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index b78493e8286..d009c4b7dd8 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -211,6 +211,7 @@ validate_load_balancer_sku, validate_max_surge, validate_message_of_the_day, + validate_node_public_ip_prefix_ids, validate_node_public_ip_tags, validate_nodepool_id, validate_nodepool_labels, @@ -868,6 +869,14 @@ def load_arguments(self, _): ) c.argument("enable_node_public_ip", action="store_true") c.argument("node_public_ip_prefix_id") + c.argument( + "node_public_ip_prefix_ids", + validator=validate_node_public_ip_prefix_ids, + help="Comma-separated list of public IP prefix resource IDs for dual-stack node public IPs " + "(IPv4 and/or IPv6). At most one IPv4 and one IPv6 prefix may be specified. " + "Automatically enables --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. " + "Requires the NodePublicIPv6PrefixPreview feature flag to be registered.", + ) c.argument("enable_cluster_autoscaler", action="store_true") c.argument("min_count", type=int, validator=validate_nodes_count) c.argument("max_count", type=int, validator=validate_nodes_count) @@ -2042,6 +2051,14 @@ def load_arguments(self, _): ) c.argument("enable_node_public_ip", action="store_true") c.argument("node_public_ip_prefix_id") + c.argument( + "node_public_ip_prefix_ids", + validator=validate_node_public_ip_prefix_ids, + help="Comma-separated list of public IP prefix resource IDs for dual-stack node public IPs " + "(IPv4 and/or IPv6). At most one IPv4 and one IPv6 prefix may be specified. " + "Automatically enables --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. " + "Requires the NodePublicIPv6PrefixPreview feature flag to be registered.", + ) c.argument( "enable_cluster_autoscaler", options_list=["--enable-cluster-autoscaler", "-e"], diff --git a/src/aks-preview/azext_aks_preview/_validators.py b/src/aks-preview/azext_aks_preview/_validators.py index aa43b9052a2..d359952cf13 100644 --- a/src/aks-preview/azext_aks_preview/_validators.py +++ b/src/aks-preview/azext_aks_preview/_validators.py @@ -393,6 +393,32 @@ def validate_node_public_ip_tags(ns): ns.node_public_ip_tags = tags_dict +def validate_node_public_ip_prefix_ids(ns): + """Validate --node-public-ip-prefix-ids value and mutual exclusion with --node-public-ip-prefix-id.""" + ids_value = getattr(ns, "node_public_ip_prefix_ids", None) + singular_value = getattr(ns, "node_public_ip_prefix_id", None) + if ids_value and singular_value: + raise MutuallyExclusiveArgumentError( + "--node-public-ip-prefix-ids and --node-public-ip-prefix-id cannot be used at the same time." + ) + if ids_value is not None: + parsed = [x.strip() for x in ids_value.split(",") if x.strip()] if isinstance(ids_value, str) else ids_value + if not parsed: + raise InvalidArgumentValueError( + "--node-public-ip-prefix-ids must contain at least one public IP prefix resource ID." + ) + if len(parsed) > 2: + raise InvalidArgumentValueError( + "--node-public-ip-prefix-ids accepts at most two public IP prefix resource IDs " + "(one IPv4 and one IPv6)." + ) + for prefix_id in parsed: + if not is_valid_resource_id(prefix_id): + raise InvalidArgumentValueError( + f"'{prefix_id}' is not a valid Azure resource ID for --node-public-ip-prefix-ids." + ) + + def validate_nodepool_labels(namespace): """Validates that provided node labels is a valid format""" diff --git a/src/aks-preview/azext_aks_preview/agentpool_decorator.py b/src/aks-preview/azext_aks_preview/agentpool_decorator.py index a61272f076b..80c5c2d1eac 100644 --- a/src/aks-preview/azext_aks_preview/agentpool_decorator.py +++ b/src/aks-preview/azext_aks_preview/agentpool_decorator.py @@ -338,6 +338,29 @@ def get_ip_tags(self) -> Union[List[IPTag], None]: )) return res + def get_node_public_ip_prefix_ids(self) -> Union[List[str], None]: + """Obtain the value of node_public_ip_prefix_ids. + + Parse the comma-separated string into a list of resource IDs. + + :return: list of strings or None + """ + node_public_ip_prefix_ids = self.raw_param.get("node_public_ip_prefix_ids") + if node_public_ip_prefix_ids is None: + return None + if isinstance(node_public_ip_prefix_ids, str): + parsed = [x.strip() for x in node_public_ip_prefix_ids.split(",") if x.strip()] + if not parsed: + raise InvalidArgumentValueError( + "--node-public-ip-prefix-ids must contain at least one public IP prefix resource ID." + ) + return parsed + if isinstance(node_public_ip_prefix_ids, list) and not node_public_ip_prefix_ids: + raise InvalidArgumentValueError( + "--node-public-ip-prefix-ids must contain at least one public IP prefix resource ID." + ) + return node_public_ip_prefix_ids + def get_node_taints(self) -> Union[List[str], None]: """Obtain the value of node_taints. @@ -1306,6 +1329,11 @@ def set_up_agentpool_network_profile(self, agentpool: AgentPool) -> AgentPool: if ip_tags: agentpool.network_profile.node_public_ip_tags = ip_tags + node_public_ip_prefix_ids = self.context.get_node_public_ip_prefix_ids() + if node_public_ip_prefix_ids: + agentpool.network_profile["nodePublicIPPrefixIDs"] = node_public_ip_prefix_ids + agentpool.enable_node_public_ip = True + return agentpool def set_up_taints(self, agentpool: AgentPool) -> AgentPool: diff --git a/src/aks-preview/azext_aks_preview/custom.py b/src/aks-preview/azext_aks_preview/custom.py index 59422a1fb1c..ead47015e22 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -1054,6 +1054,7 @@ def aks_create( pod_ip_allocation_mode=None, enable_node_public_ip=False, node_public_ip_prefix_id=None, + node_public_ip_prefix_ids=None, enable_cluster_autoscaler=False, min_count=None, max_count=None, @@ -1908,6 +1909,7 @@ def aks_agentpool_add( pod_ip_allocation_mode=None, enable_node_public_ip=False, node_public_ip_prefix_id=None, + node_public_ip_prefix_ids=None, enable_cluster_autoscaler=False, min_count=None, max_count=None, diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py index e51e73c6c20..441e7869ec3 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py @@ -346,6 +346,47 @@ def common_get_pod_ip_allocation_mode(self): ctx_2.attach_agentpool(agentpool_2) self.assertEqual(ctx_2.get_pod_ip_allocation_mode(), "StaticBlock") + def common_get_node_public_ip_prefix_ids(self): + # default - None + ctx_1 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"node_public_ip_prefix_ids": None}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual(ctx_1.get_node_public_ip_prefix_ids(), None) + + # comma-separated string + ctx_2 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({ + "node_public_ip_prefix_ids": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPPrefixes/ipv4-prefix," + "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPPrefixes/ipv6-prefix" + }), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual( + ctx_2.get_node_public_ip_prefix_ids(), + [ + "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPPrefixes/ipv4-prefix", + "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPPrefixes/ipv6-prefix", + ], + ) + + # empty string should raise error + ctx_3 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"node_public_ip_prefix_ids": ""}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + with self.assertRaises(InvalidArgumentValueError): + ctx_3.get_node_public_ip_prefix_ids() + def common_get_skip_gpu_driver_install(self): # default ctx_1 = AKSPreviewAgentPoolContext( @@ -1164,6 +1205,9 @@ def test_get_enable_managed_gpu(self): def test_get_pod_ip_allocation_mode(self): self.common_get_pod_ip_allocation_mode() + def test_get_node_public_ip_prefix_ids(self): + self.common_get_node_public_ip_prefix_ids() + def test_get_os_sku(self): self.common_get_os_sku() @@ -1264,6 +1308,9 @@ def test_get_enable_managed_gpu(self): def test_get_pod_ip_allocation_mode(self): self.common_get_pod_ip_allocation_mode() + def test_get_node_public_ip_prefix_ids(self): + self.common_get_node_public_ip_prefix_ids() + def test_get_os_sku(self): self.common_get_os_sku() diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py index bc3870af232..c9d818a2f54 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py @@ -17293,6 +17293,122 @@ def test_node_public_ip_tags(self, resource_group, resource_group_location): ], ) + # live_only: NodePublicIPv6PrefixPreview feature flag must be registered on the + # test subscription. Recording-based tests will be added once the feature is GA. + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer( + random_name_length=17, name_prefix="clitest", location="eastus2euap" + ) + def test_node_public_ip_prefix_ids(self, resource_group, resource_group_location): + aks_name = self.create_random_name("cliakstest", 16) + nodepool_name = self.create_random_name("n", 6) + nodepool_name_1 = self.create_random_name("n", 6) + + self.kwargs.update( + { + "resource_group": resource_group, + "name": aks_name, + "location": resource_group_location, + "ssh_key_value": self.generate_ssh_keys(), + "node_pool_name": nodepool_name, + "node_vm_size": "standard_d2a_v4", + "ipv4_prefix": self.create_random_name("ipv4prefix", 20), + "ipv6_prefix": self.create_random_name("ipv6prefix", 20), + } + ) + + # Create IPv4 and IPv6 public IP prefixes + ipv4_result = self.cmd( + "network public-ip prefix create " + "--resource-group={resource_group} " + "--name={ipv4_prefix} " + "--location={location} " + "--length=28 " + "--version=IPv4" + ).get_output_in_json() + ipv4_prefix_id = ipv4_result["id"] + + ipv6_result = self.cmd( + "network public-ip prefix create " + "--resource-group={resource_group} " + "--name={ipv6_prefix} " + "--location={location} " + "--length=124 " + "--version=IPv6" + ).get_output_in_json() + ipv6_prefix_id = ipv6_result["id"] + + self.kwargs.update( + { + "prefix_ids": f"{ipv4_prefix_id},{ipv6_prefix_id}", + } + ) + + # Create cluster with --node-public-ip-prefix-ids + self.cmd( + "aks create " + "--resource-group={resource_group} " + "--name={name} " + "--location={location} " + "--ssh-key-value={ssh_key_value} " + "--nodepool-name={node_pool_name} " + "--node-count=1 " + "--node-vm-size={node_vm_size} " + "--node-public-ip-prefix-ids={prefix_ids} " + "--aks-custom-headers=AKSHTTPCustomFeatures=Microsoft.ContainerService/NodePublicIPv6PrefixPreview", + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("agentPoolProfiles[0].enableNodePublicIp", True), + self.check( + "agentPoolProfiles[0].networkProfile.nodePublicIPPrefixIDs[0]", + ipv4_prefix_id, + ), + self.check( + "agentPoolProfiles[0].networkProfile.nodePublicIPPrefixIDs[1]", + ipv6_prefix_id, + ), + ], + ) + + # Add nodepool with --node-public-ip-prefix-ids + self.kwargs.update( + { + "node_pool_name": nodepool_name_1, + } + ) + + self.cmd( + "aks nodepool add " + "--resource-group={resource_group} " + "--cluster-name={name} " + "--name={node_pool_name} " + "--node-count=1 " + "--node-vm-size={node_vm_size} " + "--node-public-ip-prefix-ids={prefix_ids} " + "--aks-custom-headers=AKSHTTPCustomFeatures=Microsoft.ContainerService/NodePublicIPv6PrefixPreview", + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("enableNodePublicIp", True), + self.check( + "networkProfile.nodePublicIPPrefixIDs[0]", + ipv4_prefix_id, + ), + self.check( + "networkProfile.nodePublicIPPrefixIDs[1]", + ipv6_prefix_id, + ), + ], + ) + + # delete + self.cmd( + "aks delete --resource-group={resource_group} --name={name} --yes --no-wait", + checks=[ + self.is_empty(), + ], + ) + @AllowLargeResponse() @AKSCustomResourceGroupPreparer( random_name_length=17, name_prefix="clitest", location="westus2" diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py index 5e94ae51e06..81307edec15 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py @@ -5959,6 +5959,7 @@ def test_set_up_agentpool_profile(self): "pod_ip_allocation_mode": "DynamicIndividual", "enable_node_public_ip": True, "node_public_ip_prefix_id": "test_node_public_ip_prefix_id", + "node_public_ip_prefix_ids": None, "enable_cluster_autoscaler": True, "min_count": 5, "max_count": 20, diff --git a/src/aks-preview/setup.py b/src/aks-preview/setup.py index 44030af6dc3..785e8a41d65 100644 --- a/src/aks-preview/setup.py +++ b/src/aks-preview/setup.py @@ -9,7 +9,7 @@ from setuptools import find_packages, setup -VERSION = "20.0.0b4" +VERSION = "20.0.0b5" CLASSIFIERS = [ "Development Status :: 4 - Beta",