From 1baa91488ead518ec173393e29c3f4c4aa5c6742 Mon Sep 17 00:00:00 2001 From: cdossa Date: Tue, 1 Sep 2026 19:25:50 -0700 Subject: [PATCH 1/7] {AKS} az aks: add --outbound-type-sku for managed NAT Gateway V2 GA Ports the aks-preview NAT Gateway V2 GA surface (extension PR #10264) into core az aks now that the 2026-06-01 SDK (azure-mgmt-containerservice 41.6.0) is vendored. az aks create / az aks update: add --outbound-type-sku (Standard|StandardV2) to select the managed NAT gateway SKU with --outbound-type managedNATGateway (the GA shape of NAT Gateway V2), plus the StandardV2-only --nat-gateway-managed-outbound-ipv6-count, --nat-gateway-outbound-ips and --nat-gateway-outbound-ip-prefixes. Create requires an explicit --outbound-type managedNATGateway; update verifies the cluster's existing outbound type before applying. Adds builder + validator unit tests. --- src/azure-cli/HISTORY.rst | 1 + .../azure/cli/command_modules/acs/_consts.py | 4 + .../azure/cli/command_modules/acs/_help.py | 32 ++++++ .../cli/command_modules/acs/_natgateway.py | 103 +++++++++++++++--- .../azure/cli/command_modules/acs/_params.py | 20 +++- .../cli/command_modules/acs/_validators.py | 85 +++++++++++++++ .../azure/cli/command_modules/acs/custom.py | 8 ++ .../acs/managed_cluster_decorator.py | 72 ++++++++++++ .../acs/tests/latest/test_natgateway.py | 50 +++++++++ .../acs/tests/latest/test_validators.py | 48 ++++++++ 10 files changed, 407 insertions(+), 16 deletions(-) diff --git a/src/azure-cli/HISTORY.rst b/src/azure-cli/HISTORY.rst index c222025df06..a2491a3159d 100644 --- a/src/azure-cli/HISTORY.rst +++ b/src/azure-cli/HISTORY.rst @@ -24,6 +24,7 @@ Release History * `az aks nodepool rollback`: Show an accurate warning when only the node OS upgrade channel is enabled (#33854) * Implement enable/disable flags for user-defined scheduler configuration (#33934) * `az aks update`: Fix Azure Container Storage configuration detection for lowercase and boolean extension settings (#33938) +* `az aks create`, `az aks update`: Add `--outbound-type-sku` to select the managed NAT gateway SKU (`Standard` or `StandardV2`) with `--outbound-type managedNATGateway`, the GA shape of NAT Gateway V2. `StandardV2` also supports IPv6 (`--nat-gateway-managed-outbound-ipv6-count`), user-provided public IPs (`--nat-gateway-outbound-ips`), and user-provided IP prefixes (`--nat-gateway-outbound-ip-prefixes`). **App Config** diff --git a/src/azure-cli/azure/cli/command_modules/acs/_consts.py b/src/azure-cli/azure/cli/command_modules/acs/_consts.py index 227790750ee..b3248a70e78 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_consts.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_consts.py @@ -88,6 +88,10 @@ CONST_OUTBOUND_TYPE_USER_ASSIGNED_NAT_GATEWAY = "userAssignedNATGateway" CONST_OUTBOUND_TYPE_NONE = "none" +# managed NAT gateway SKU +CONST_NAT_GATEWAY_SKU_STANDARD = "Standard" +CONST_NAT_GATEWAY_SKU_STANDARD_V2 = "StandardV2" + # load balancer backend pool type CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IP = "nodeIP" CONST_LOAD_BALANCER_BACKEND_POOL_TYPE_NODE_IP_CONFIGURATION = "nodeIPConfiguration" diff --git a/src/azure-cli/azure/cli/command_modules/acs/_help.py b/src/azure-cli/azure/cli/command_modules/acs/_help.py index e0b7ae94fcd..7e8b00d6598 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_help.py @@ -186,6 +186,22 @@ type: int short-summary: NAT gateway idle timeout in minutes. long-summary: Desired idle timeout for NAT gateway outbound flows, default is 4 minutes. Please specify a value in the range of [4, 120]. Valid for Standard SKU load balancer cluster with managedNATGateway outbound type only. + - name: --outbound-type-sku + type: string + short-summary: SKU of the managed NAT Gateway (Standard or StandardV2). + long-summary: Only valid with --outbound-type managedNATGateway. Omit to default to StandardV2 where the region supports it, otherwise Standard. StandardV2 adds zone resiliency, IPv6 support, and higher bandwidth. + - name: --nat-gateway-managed-outbound-ipv6-count + type: int + short-summary: NAT gateway managed outbound IPv6 count. + long-summary: Desired number of managed IPv6 outbound IPs for NAT gateway outbound connection on dual-stack clusters. Please specify a value in the range of [0, 16]. Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. + - name: --nat-gateway-outbound-ips + type: string + short-summary: Comma-separated public IP resource IDs for the cluster NAT gateway. + long-summary: Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. + - name: --nat-gateway-outbound-ip-prefixes + type: string + short-summary: Comma-separated public IP prefix resource IDs for the cluster NAT gateway. + long-summary: Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. - name: --outbound-type type: string short-summary: How outbound traffic will be configured for a cluster. @@ -867,6 +883,22 @@ type: int short-summary: NAT gateway idle timeout in minutes. long-summary: Desired idle timeout for NAT gateway outbound flows, default is 4 minutes. Please specify a value in the range of [4, 120]. Valid for Standard SKU load balancer cluster with managedNATGateway outbound type only. + - name: --outbound-type-sku + type: string + short-summary: SKU of the managed NAT Gateway (Standard or StandardV2). + long-summary: Only valid with --outbound-type managedNATGateway. Migrate an existing Standard (V1) cluster to StandardV2 by passing StandardV2. StandardV2 adds zone resiliency, IPv6 support, and higher bandwidth. Downgrade from StandardV2 to Standard is not supported. + - name: --nat-gateway-managed-outbound-ipv6-count + type: int + short-summary: NAT gateway managed outbound IPv6 count. + long-summary: Desired number of managed IPv6 outbound IPs for NAT gateway outbound connection on dual-stack clusters. Please specify a value in the range of [0, 16]. Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. + - name: --nat-gateway-outbound-ips + type: string + short-summary: Comma-separated public IP resource IDs for the cluster NAT gateway. + long-summary: Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. + - name: --nat-gateway-outbound-ip-prefixes + type: string + short-summary: Comma-separated public IP prefix resource IDs for the cluster NAT gateway. + long-summary: Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. - name: --outbound-type type: string short-summary: How outbound traffic will be configured for a cluster. diff --git a/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py b/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py index 7ab8e6f1f52..c4b68598cf1 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py @@ -6,37 +6,114 @@ from types import SimpleNamespace -def create_nat_gateway_profile(managed_outbound_ip_count, idle_timeout, models: SimpleNamespace): +def create_nat_gateway_profile( + managed_outbound_ip_count, + idle_timeout, + models: SimpleNamespace, + managed_outbound_ipv6_count=None, + outbound_ip_ids=None, + outbound_ip_prefix_ids=None, + nat_gateway_sku=None, +): """parse and build NAT gateway profile""" - if not is_nat_gateway_profile_provided(managed_outbound_ip_count, idle_timeout): + if not is_nat_gateway_profile_provided( + managed_outbound_ip_count, idle_timeout, + managed_outbound_ipv6_count, outbound_ip_ids, outbound_ip_prefix_ids, + nat_gateway_sku, + ): return None profile = models.ManagedClusterNATGatewayProfile() - return configure_nat_gateway_profile(managed_outbound_ip_count, idle_timeout, profile, models) + return configure_nat_gateway_profile( + managed_outbound_ip_count, idle_timeout, profile, models, + managed_outbound_ipv6_count, outbound_ip_ids, outbound_ip_prefix_ids, + nat_gateway_sku, + ) -def update_nat_gateway_profile(managed_outbound_ip_count, idle_timeout, profile, models: SimpleNamespace): +def update_nat_gateway_profile( + managed_outbound_ip_count, + idle_timeout, + profile, + models: SimpleNamespace, + managed_outbound_ipv6_count=None, + outbound_ip_ids=None, + outbound_ip_prefix_ids=None, + nat_gateway_sku=None, +): """parse and update an existing NAT gateway profile""" - if not is_nat_gateway_profile_provided(managed_outbound_ip_count, idle_timeout): + if not is_nat_gateway_profile_provided( + managed_outbound_ip_count, idle_timeout, + managed_outbound_ipv6_count, outbound_ip_ids, outbound_ip_prefix_ids, + nat_gateway_sku, + ): return profile if not profile: profile = models.ManagedClusterNATGatewayProfile() - return configure_nat_gateway_profile(managed_outbound_ip_count, idle_timeout, profile, models) + return configure_nat_gateway_profile( + managed_outbound_ip_count, idle_timeout, profile, models, + managed_outbound_ipv6_count, outbound_ip_ids, outbound_ip_prefix_ids, + nat_gateway_sku, + ) -def is_nat_gateway_profile_provided(managed_outbound_ip_count, idle_timeout): - return any([managed_outbound_ip_count is not None, idle_timeout]) +def is_nat_gateway_profile_provided( + managed_outbound_ip_count, + idle_timeout, + managed_outbound_ipv6_count=None, + outbound_ip_ids=None, + outbound_ip_prefix_ids=None, + nat_gateway_sku=None, +): + return any([ + managed_outbound_ip_count is not None, + idle_timeout, + managed_outbound_ipv6_count is not None, + outbound_ip_ids is not None, + outbound_ip_prefix_ids is not None, + nat_gateway_sku is not None, + ]) -def configure_nat_gateway_profile(managed_outbound_ip_count, idle_timeout, profile, models: SimpleNamespace): +def configure_nat_gateway_profile( + managed_outbound_ip_count, + idle_timeout, + profile, + models: SimpleNamespace, + managed_outbound_ipv6_count=None, + outbound_ip_ids=None, + outbound_ip_prefix_ids=None, + nat_gateway_sku=None, +): """configure a NAT Gateway with customer supplied values""" - if managed_outbound_ip_count is not None: + if managed_outbound_ip_count is not None or managed_outbound_ipv6_count is not None: ManagedClusterManagedOutboundIPProfile = models.ManagedClusterManagedOutboundIPProfile - profile.managed_outbound_ip_profile = ManagedClusterManagedOutboundIPProfile( - count=managed_outbound_ip_count - ) + if not profile.managed_outbound_ip_profile: + profile.managed_outbound_ip_profile = ManagedClusterManagedOutboundIPProfile() + if managed_outbound_ip_count is not None: + profile.managed_outbound_ip_profile.count = managed_outbound_ip_count + if managed_outbound_ipv6_count is not None: + profile.managed_outbound_ip_profile.count_ipv6 = managed_outbound_ipv6_count if idle_timeout: profile.idle_timeout_in_minutes = idle_timeout + if outbound_ip_ids is not None: + ManagedClusterNATGatewayProfileOutboundIPs = models.ManagedClusterNATGatewayProfileOutboundIPs + ip_id_list = [x.strip() for x in outbound_ip_ids.split(',') if x.strip()] + profile.outbound_i_ps = ManagedClusterNATGatewayProfileOutboundIPs( + public_i_ps=ip_id_list + ) + + if outbound_ip_prefix_ids is not None: + ManagedClusterNATGatewayProfileOutboundIpPrefixes = models.ManagedClusterNATGatewayProfileOutboundIpPrefixes + prefix_id_list = [x.strip() for x in outbound_ip_prefix_ids.split(',') if x.strip()] + profile.outbound_ip_prefixes = ManagedClusterNATGatewayProfileOutboundIpPrefixes( + public_ip_prefixes=prefix_id_list + ) + + if nat_gateway_sku is not None: + # GA shape: V2 is expressed as outboundType=managedNATGateway + natGatewayProfile.sku. + profile.sku = nat_gateway_sku + return profile diff --git a/src/azure-cli/azure/cli/command_modules/acs/_params.py b/src/azure-cli/azure/cli/command_modules/acs/_params.py index 09056477334..05c508318ca 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -46,6 +46,7 @@ CONST_OS_SKU_CBLMARINER, CONST_OS_SKU_MARINER, CONST_OS_SKU_UBUNTU, CONST_OS_SKU_UBUNTU2204, CONST_OS_SKU_UBUNTU2404, CONST_OS_SKU_WINDOWS2019, CONST_OS_SKU_WINDOWS2022, CONST_OS_SKU_WINDOWS2025, + CONST_NAT_GATEWAY_SKU_STANDARD, CONST_NAT_GATEWAY_SKU_STANDARD_V2, CONST_OUTBOUND_TYPE_LOAD_BALANCER, CONST_OUTBOUND_TYPE_MANAGED_NAT_GATEWAY, CONST_OUTBOUND_TYPE_USER_ASSIGNED_NAT_GATEWAY, CONST_OUTBOUND_TYPE_USER_DEFINED_ROUTING, CONST_OUTBOUND_TYPE_NONE, @@ -119,7 +120,11 @@ validate_load_balancer_outbound_ips, validate_load_balancer_outbound_ports, validate_load_balancer_sku, validate_max_surge, validate_max_unavailable, validate_nat_gateway_idle_timeout, - validate_nat_gateway_managed_outbound_ip_count, validate_network_policy, + validate_nat_gateway_managed_outbound_ip_count, + validate_nat_gateway_managed_outbound_ipv6_count, + validate_nat_gateway_v2_params, validate_nat_gateway_v2_params_for_update, + validate_network_policy, + validate_outbound_type_sku, validate_outbound_type_sku_for_update, validate_nodepool_id, validate_nodepool_labels, validate_nodepool_name, validate_nodepool_tags, validate_nodes_count, validate_os_sku, validate_pod_subnet_id, validate_ppg, validate_priority, @@ -197,6 +202,7 @@ # consts for ManagedCluster load_balancer_skus = [CONST_LOAD_BALANCER_SKU_BASIC, CONST_LOAD_BALANCER_SKU_STANDARD] +nat_gateway_skus = [CONST_NAT_GATEWAY_SKU_STANDARD, CONST_NAT_GATEWAY_SKU_STANDARD_V2] sku_names = [CONST_MANAGED_CLUSTER_SKU_NAME_BASE, CONST_MANAGED_CLUSTER_SKU_NAME_AUTOMATIC] sku_tiers = [CONST_MANAGED_CLUSTER_SKU_TIER_FREE, CONST_MANAGED_CLUSTER_SKU_TIER_STANDARD, CONST_MANAGED_CLUSTER_SKU_TIER_PREMIUM] network_plugins = [CONST_NETWORK_PLUGIN_KUBENET, CONST_NETWORK_PLUGIN_AZURE, CONST_NETWORK_PLUGIN_NONE] @@ -428,8 +434,12 @@ def load_arguments(self, _): c.argument('load_balancer_backend_pool_type', arg_type=get_enum_type(backend_pool_types)) c.argument('nrg_lockdown_restriction_level', arg_type=get_enum_type(nrg_lockdown_restriction_levels)) c.argument('nat_gateway_managed_outbound_ip_count', type=int, validator=validate_nat_gateway_managed_outbound_ip_count) + c.argument('nat_gateway_managed_outbound_ipv6_count', type=int, validator=validate_nat_gateway_managed_outbound_ipv6_count) c.argument('nat_gateway_idle_timeout', type=int, validator=validate_nat_gateway_idle_timeout) - c.argument('outbound_type', arg_type=get_enum_type(outbound_types)) + c.argument('nat_gateway_sku', options_list=['--outbound-type-sku'], arg_type=get_enum_type(nat_gateway_skus), validator=validate_outbound_type_sku) + c.argument('nat_gateway_outbound_ip_ids', options_list=['--nat-gateway-outbound-ips', '--nat-gw-ips']) + c.argument('nat_gateway_outbound_ip_prefix_ids', options_list=['--nat-gateway-outbound-ip-prefixes', '--nat-gw-prefixes']) + c.argument('outbound_type', arg_type=get_enum_type(outbound_types), validator=validate_nat_gateway_v2_params) c.argument('network_plugin', arg_type=get_enum_type(network_plugins)) c.argument('network_plugin_mode', arg_type=get_enum_type(network_plugin_modes)) c.argument('network_policy', validator=validate_network_policy) @@ -695,11 +705,15 @@ def load_arguments(self, _): c.argument("load_balancer_sku", arg_type=get_enum_type([CONST_LOAD_BALANCER_SKU_STANDARD]), validator=validate_load_balancer_sku) c.argument('nrg_lockdown_restriction_level', arg_type=get_enum_type(nrg_lockdown_restriction_levels)) c.argument('nat_gateway_managed_outbound_ip_count', type=int, validator=validate_nat_gateway_managed_outbound_ip_count) + c.argument('nat_gateway_managed_outbound_ipv6_count', type=int, validator=validate_nat_gateway_managed_outbound_ipv6_count) c.argument('nat_gateway_idle_timeout', type=int, validator=validate_nat_gateway_idle_timeout) + c.argument('nat_gateway_sku', options_list=['--outbound-type-sku'], arg_type=get_enum_type(nat_gateway_skus), validator=validate_outbound_type_sku_for_update) + c.argument('nat_gateway_outbound_ip_ids', options_list=['--nat-gateway-outbound-ips', '--nat-gw-ips']) + c.argument('nat_gateway_outbound_ip_prefix_ids', options_list=['--nat-gateway-outbound-ip-prefixes', '--nat-gw-prefixes']) c.argument('network_dataplane', arg_type=get_enum_type(network_dataplanes)) c.argument('network_plugin', arg_type=get_enum_type(network_plugins)) c.argument('network_policy', arg_type=get_enum_type(network_policies)) - c.argument('outbound_type', arg_type=get_enum_type(outbound_types)) + c.argument('outbound_type', arg_type=get_enum_type(outbound_types), validator=validate_nat_gateway_v2_params_for_update) c.argument('auto_upgrade_channel', arg_type=get_enum_type(auto_upgrade_channels)) c.argument('cluster_autoscaler_profile', nargs='+', options_list=["--cluster-autoscaler-profile", "--ca-profile"], help="Comma-separated list of key=value pairs for configuring cluster autoscaler. Pass an empty string to clear the profile.") diff --git a/src/azure-cli/azure/cli/command_modules/acs/_validators.py b/src/azure-cli/azure/cli/command_modules/acs/_validators.py index 05bde5da70c..c2b7f06ebcf 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_validators.py @@ -356,6 +356,91 @@ def validate_nat_gateway_idle_timeout(namespace): raise InvalidArgumentValueError("--nat-gateway-idle-timeout must be in the range [4,120]") +def validate_nat_gateway_managed_outbound_ipv6_count(namespace): + """validate NAT gateway profile managed outbound IPv6 count (StandardV2, dual-stack only)""" + ipv6_count = namespace.nat_gateway_managed_outbound_ipv6_count + if ipv6_count is not None: + if ipv6_count < 0 or ipv6_count > 16: + raise InvalidArgumentValueError( + "--nat-gateway-managed-outbound-ipv6-count must be in the range [0,16]" + ) + + +def validate_outbound_type_sku(namespace): + """Validate --outbound-type-sku on create (managed NAT gateway SKU). + + The SKU only applies to the managed NAT gateway outbound type and, on create, drives building a + NAT gateway profile, so --outbound-type must be set explicitly to managedNATGateway; omitting it + defaults the cluster to loadBalancer and produces an incompatible request. + """ + if getattr(namespace, 'nat_gateway_sku', None) is None: + return + if namespace.outbound_type != 'managedNATGateway': + raise InvalidArgumentValueError( + "--outbound-type-sku is only valid with --outbound-type managedNATGateway; " + "specify --outbound-type managedNATGateway explicitly." + ) + + +def validate_outbound_type_sku_for_update(namespace): + """Validate --outbound-type-sku on update (managed NAT gateway SKU). + + Unlike create, --outbound-type may be omitted when the cluster is already managed NAT gateway; + only an explicit non-managed-NAT-gateway outbound type is rejected here. The update decorator + additionally verifies the cluster's existing outbound type. + """ + if getattr(namespace, 'nat_gateway_sku', None) is None: + return + if namespace.outbound_type is not None and namespace.outbound_type != 'managedNATGateway': + raise InvalidArgumentValueError( + "--outbound-type-sku is only valid with --outbound-type managedNATGateway." + ) + + +def validate_nat_gateway_v2_params(namespace): + """Validate StandardV2-only NAT gateway params on create. + + The V2-only params (managed IPv6 count, BYO outbound IPs / IP prefixes) require the managed NAT + gateway outbound type with the StandardV2 SKU; the Standard (V1) SKU cannot carry them. On create + --outbound-type must be set explicitly to managedNATGateway. + """ + v2_params = [ + getattr(namespace, 'nat_gateway_managed_outbound_ipv6_count', None), + getattr(namespace, 'nat_gateway_outbound_ip_ids', None), + getattr(namespace, 'nat_gateway_outbound_ip_prefix_ids', None), + ] + if not any(p is not None for p in v2_params): + return + if namespace.outbound_type != 'managedNATGateway' or getattr(namespace, 'nat_gateway_sku', None) == 'Standard': + raise InvalidArgumentValueError( + "--nat-gateway-managed-outbound-ipv6-count, --nat-gateway-outbound-ips and " + "--nat-gateway-outbound-ip-prefixes are only valid with --outbound-type managedNATGateway " + "and --outbound-type-sku StandardV2; specify --outbound-type managedNATGateway explicitly." + ) + + +def validate_nat_gateway_v2_params_for_update(namespace): + """Validate StandardV2-only NAT gateway params on update. + + Unlike create, --outbound-type may be omitted when the cluster is already managed NAT gateway; + only an explicit non-managed-NAT-gateway outbound type or the Standard SKU is rejected here. + """ + v2_params = [ + getattr(namespace, 'nat_gateway_managed_outbound_ipv6_count', None), + getattr(namespace, 'nat_gateway_outbound_ip_ids', None), + getattr(namespace, 'nat_gateway_outbound_ip_prefix_ids', None), + ] + if not any(p is not None for p in v2_params): + return + if (namespace.outbound_type is not None and namespace.outbound_type != 'managedNATGateway') or \ + getattr(namespace, 'nat_gateway_sku', None) == 'Standard': + raise InvalidArgumentValueError( + "--nat-gateway-managed-outbound-ipv6-count, --nat-gateway-outbound-ips and " + "--nat-gateway-outbound-ip-prefixes are only valid with --outbound-type managedNATGateway " + "and --outbound-type-sku StandardV2." + ) + + def validate_nodes_count(namespace): """Validates that min_count and max_count is set between 0-1000""" if namespace.min_count is not None: diff --git a/src/azure-cli/azure/cli/command_modules/acs/custom.py b/src/azure-cli/azure/cli/command_modules/acs/custom.py index a0008c954d2..7ed30032ead 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/custom.py +++ b/src/azure-cli/azure/cli/command_modules/acs/custom.py @@ -882,7 +882,11 @@ def aks_create( load_balancer_idle_timeout=None, load_balancer_backend_pool_type=None, nat_gateway_managed_outbound_ip_count=None, + nat_gateway_managed_outbound_ipv6_count=None, nat_gateway_idle_timeout=None, + nat_gateway_sku=None, + nat_gateway_outbound_ip_ids=None, + nat_gateway_outbound_ip_prefix_ids=None, outbound_type=None, network_plugin=None, network_plugin_mode=None, @@ -1113,7 +1117,11 @@ def aks_update( load_balancer_backend_pool_type=None, load_balancer_sku=None, nat_gateway_managed_outbound_ip_count=None, + nat_gateway_managed_outbound_ipv6_count=None, nat_gateway_idle_timeout=None, + nat_gateway_sku=None, + nat_gateway_outbound_ip_ids=None, + nat_gateway_outbound_ip_prefix_ids=None, outbound_type=None, auto_upgrade_channel=None, node_os_upgrade_channel=None, diff --git a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py index 6a0480a8850..98bebf062d6 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py @@ -238,6 +238,16 @@ def nat_gateway_models(self) -> SimpleNamespace: if hasattr(self, "ManagedClusterManagedOutboundIPProfile") else None ) # backward compatibility + nat_gateway_models["ManagedClusterNATGatewayProfileOutboundIPs"] = ( + self.ManagedClusterNATGatewayProfileOutboundIPs + if hasattr(self, "ManagedClusterNATGatewayProfileOutboundIPs") + else None + ) # backward compatibility + nat_gateway_models["ManagedClusterNATGatewayProfileOutboundIpPrefixes"] = ( + self.ManagedClusterNATGatewayProfileOutboundIpPrefixes + if hasattr(self, "ManagedClusterNATGatewayProfileOutboundIpPrefixes") + else None + ) # backward compatibility self.__nat_gateway_models = SimpleNamespace(**nat_gateway_models) return self.__nat_gateway_models @@ -2289,6 +2299,44 @@ def get_nat_gateway_idle_timeout(self) -> Union[int, None]: # this parameter does not need validation return nat_gateway_idle_timeout + def get_nat_gateway_sku(self) -> Union[str, None]: + """Obtain the value of nat_gateway_sku (--outbound-type-sku). + + The managed NAT gateway SKU (Standard or StandardV2). GA shape: V2 is expressed via + outboundType=managedNATGateway + natGatewayProfile.sku=StandardV2. Region availability + and downgrade rules are enforced server-side by the RP. + + :return: str or None + """ + return self.raw_param.get("nat_gateway_sku") + + def get_nat_gateway_managed_outbound_ipv6_count(self) -> Union[int, None]: + """Obtain the value of nat_gateway_managed_outbound_ipv6_count. + + Only valid with the StandardV2 SKU on dual-stack clusters. + + :return: int or None + """ + return self.raw_param.get("nat_gateway_managed_outbound_ipv6_count") + + def get_nat_gateway_outbound_ip_ids(self) -> Union[str, None]: + """Obtain the value of nat_gateway_outbound_ip_ids (--nat-gateway-outbound-ips). + + Only valid with the StandardV2 SKU. + + :return: str or None + """ + return self.raw_param.get("nat_gateway_outbound_ip_ids") + + def get_nat_gateway_outbound_ip_prefix_ids(self) -> Union[str, None]: + """Obtain the value of nat_gateway_outbound_ip_prefix_ids (--nat-gateway-outbound-ip-prefixes). + + Only valid with the StandardV2 SKU. + + :return: str or None + """ + return self.raw_param.get("nat_gateway_outbound_ip_prefix_ids") + def get_pod_cidrs_and_service_cidrs_and_ip_families(self) -> Tuple[ Union[List[str], None], Union[List[str], None], @@ -7064,6 +7112,10 @@ def set_up_network_profile(self, mc: ManagedCluster) -> ManagedCluster: self.context.get_nat_gateway_managed_outbound_ip_count(), self.context.get_nat_gateway_idle_timeout(), models=self.models.nat_gateway_models, + managed_outbound_ipv6_count=self.context.get_nat_gateway_managed_outbound_ipv6_count(), + outbound_ip_ids=self.context.get_nat_gateway_outbound_ip_ids(), + outbound_ip_prefix_ids=self.context.get_nat_gateway_outbound_ip_prefix_ids(), + nat_gateway_sku=self.context.get_nat_gateway_sku(), ) load_balancer_sku = self.context.get_load_balancer_sku() if load_balancer_sku != CONST_LOAD_BALANCER_SKU_BASIC: @@ -8842,6 +8894,22 @@ def update_nat_gateway_profile(self, mc: ManagedCluster) -> ManagedCluster: "Unexpectedly get an empty network profile in the process of updating nat gateway profile." ) outbound_type = self.context.get_outbound_type() + # The managed NAT gateway SKU and V2 params build a NAT gateway profile, so they are only + # valid when the cluster's effective outbound type is managedNATGateway. --outbound-type may + # be omitted on update, so reject here against the resolved type instead of silently dropping + # them on e.g. a loadBalancer cluster. + if outbound_type != CONST_OUTBOUND_TYPE_MANAGED_NAT_GATEWAY and ( + self.context.get_nat_gateway_sku() is not None or + self.context.get_nat_gateway_managed_outbound_ipv6_count() is not None or + self.context.get_nat_gateway_outbound_ip_ids() is not None or + self.context.get_nat_gateway_outbound_ip_prefix_ids() is not None + ): + raise InvalidArgumentValueError( + "--outbound-type-sku, --nat-gateway-managed-outbound-ipv6-count, " + "--nat-gateway-outbound-ips and --nat-gateway-outbound-ip-prefixes are only valid " + "when the cluster's outbound type is managedNATGateway; set " + "--outbound-type managedNATGateway to change the outbound type." + ) if outbound_type and outbound_type != CONST_OUTBOUND_TYPE_MANAGED_NAT_GATEWAY: mc.network_profile.nat_gateway_profile = None else: @@ -8850,6 +8918,10 @@ def update_nat_gateway_profile(self, mc: ManagedCluster) -> ManagedCluster: idle_timeout=self.context.get_nat_gateway_idle_timeout(), profile=mc.network_profile.nat_gateway_profile, models=self.models.nat_gateway_models, + managed_outbound_ipv6_count=self.context.get_nat_gateway_managed_outbound_ipv6_count(), + outbound_ip_ids=self.context.get_nat_gateway_outbound_ip_ids(), + outbound_ip_prefix_ids=self.context.get_nat_gateway_outbound_ip_prefix_ids(), + nat_gateway_sku=self.context.get_nat_gateway_sku(), ) return mc diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_natgateway.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_natgateway.py index 55a638ce7c9..f7cb67d9001 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_natgateway.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_natgateway.py @@ -90,5 +90,55 @@ def test_nonempty_arguments(self): result = natgateway.is_nat_gateway_profile_provided(0, None) self.assertTrue(result) + +class TestNatGatewayV2(unittest.TestCase): + def setUp(self): + self.cli_ctx = MockCLI() + self.cmd = MockCmd(self.cli_ctx) + self.nat_gateway_models = AKSManagedClusterModels( + self.cmd, ResourceType.MGMT_CONTAINERSERVICE + ).nat_gateway_models + + def test_is_provided_by_sku_and_v2_params(self): + self.assertTrue(natgateway.is_nat_gateway_profile_provided(None, None, nat_gateway_sku="StandardV2")) + self.assertTrue(natgateway.is_nat_gateway_profile_provided(None, None, managed_outbound_ipv6_count=1)) + self.assertTrue(natgateway.is_nat_gateway_profile_provided(None, None, outbound_ip_ids="/sub/ip1")) + self.assertTrue(natgateway.is_nat_gateway_profile_provided(None, None, outbound_ip_prefix_ids="/sub/pfx1")) + self.assertFalse(natgateway.is_nat_gateway_profile_provided(None, None)) + + def test_create_with_sku_and_v2_params(self): + profile = natgateway.create_nat_gateway_profile( + 2, 10, models=self.nat_gateway_models, + managed_outbound_ipv6_count=1, + outbound_ip_ids="/sub/ip1, /sub/ip2", + outbound_ip_prefix_ids="/sub/pfx1", + nat_gateway_sku="StandardV2", + ) + self.assertEqual(profile.sku, "StandardV2") + self.assertEqual(profile.managed_outbound_ip_profile.count, 2) + self.assertEqual(profile.managed_outbound_ip_profile.count_ipv6, 1) + self.assertEqual(profile.outbound_i_ps.public_i_ps, ["/sub/ip1", "/sub/ip2"]) + self.assertEqual(profile.outbound_ip_prefixes.public_ip_prefixes, ["/sub/pfx1"]) + self.assertEqual(profile.idle_timeout_in_minutes, 10) + + def test_create_sku_only(self): + profile = natgateway.create_nat_gateway_profile( + None, None, models=self.nat_gateway_models, nat_gateway_sku="StandardV2" + ) + self.assertIsNotNone(profile) + self.assertEqual(profile.sku, "StandardV2") + + def test_update_sets_sku_on_existing_profile(self): + origin = self.nat_gateway_models.ManagedClusterNATGatewayProfile( + managed_outbound_ip_profile=self.nat_gateway_models.ManagedClusterManagedOutboundIPProfile(count=1), + idle_timeout_in_minutes=4, + ) + profile = natgateway.update_nat_gateway_profile( + None, None, origin, models=self.nat_gateway_models, nat_gateway_sku="StandardV2" + ) + self.assertEqual(profile.sku, "StandardV2") + self.assertEqual(profile.managed_outbound_ip_profile.count, 1) + + if __name__ == "__main__": unittest.main() diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py index 6f5421bc3bf..405af72fa5c 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py @@ -2046,5 +2046,53 @@ def test_no_ssh_key_still_skips(self): self.assertIsNone(namespace.ssh_key_value) +class TestNatGatewayV2Validators(unittest.TestCase): + def _ns(self, **kwargs): + defaults = { + "nat_gateway_sku": None, + "outbound_type": None, + "nat_gateway_managed_outbound_ipv6_count": None, + "nat_gateway_outbound_ip_ids": None, + "nat_gateway_outbound_ip_prefix_ids": None, + } + defaults.update(kwargs) + return SimpleNamespace(**defaults) + + def test_sku_create_requires_explicit_managed_nat_gateway(self): + validators.validate_outbound_type_sku(self._ns(nat_gateway_sku="StandardV2", outbound_type="managedNATGateway")) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_outbound_type_sku(self._ns(nat_gateway_sku="StandardV2", outbound_type=None)) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_outbound_type_sku(self._ns(nat_gateway_sku="StandardV2", outbound_type="loadBalancer")) + validators.validate_outbound_type_sku(self._ns(outbound_type="loadBalancer")) + + def test_sku_update_allows_omitted_outbound_type(self): + validators.validate_outbound_type_sku_for_update(self._ns(nat_gateway_sku="StandardV2", outbound_type=None)) + validators.validate_outbound_type_sku_for_update(self._ns(nat_gateway_sku="StandardV2", outbound_type="managedNATGateway")) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_outbound_type_sku_for_update(self._ns(nat_gateway_sku="StandardV2", outbound_type="loadBalancer")) + + def test_v2_params_create_requires_managed_nat_gateway_and_v2_sku(self): + validators.validate_nat_gateway_v2_params(self._ns(nat_gateway_managed_outbound_ipv6_count=1, outbound_type="managedNATGateway")) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_nat_gateway_v2_params(self._ns(nat_gateway_managed_outbound_ipv6_count=1, outbound_type=None)) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_nat_gateway_v2_params(self._ns(nat_gateway_outbound_ip_ids="/sub/ip", outbound_type="loadBalancer")) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_nat_gateway_v2_params(self._ns(nat_gateway_managed_outbound_ipv6_count=1, outbound_type="managedNATGateway", nat_gateway_sku="Standard")) + validators.validate_nat_gateway_v2_params(self._ns(outbound_type="loadBalancer")) + + def test_v2_params_update_allows_omitted_outbound_type(self): + validators.validate_nat_gateway_v2_params_for_update(self._ns(nat_gateway_managed_outbound_ipv6_count=1, outbound_type=None)) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_nat_gateway_v2_params_for_update(self._ns(nat_gateway_outbound_ip_ids="/sub/ip", outbound_type="loadBalancer")) + + def test_ipv6_count_range(self): + validators.validate_nat_gateway_managed_outbound_ipv6_count(self._ns(nat_gateway_managed_outbound_ipv6_count=0)) + validators.validate_nat_gateway_managed_outbound_ipv6_count(self._ns(nat_gateway_managed_outbound_ipv6_count=16)) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_nat_gateway_managed_outbound_ipv6_count(self._ns(nat_gateway_managed_outbound_ipv6_count=17)) + + if __name__ == "__main__": unittest.main() From 4d218122474194f531b8d451654d1a2cc6741e38 Mon Sep 17 00:00:00 2001 From: cdossa Date: Tue, 1 Sep 2026 19:37:21 -0700 Subject: [PATCH 2/7] {AKS} az aks: add StandardV2 managed NAT gateway create/update scenario test (record --live) --- .../acs/tests/latest/test_aks_commands.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py index b776de5be20..b4152999bc4 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py @@ -1419,6 +1419,47 @@ def test_aks_create_and_update_with_managed_nat_gateway_outbound(self, resource_ self.check('networkProfile.natGatewayProfile.managedOutboundIpProfile.count', 2), ]) + @AllowLargeResponse(8192) + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westus2') + def test_aks_create_and_update_with_managed_nat_gateway_v2(self, resource_group, resource_group_location): + # NAT Gateway V2 GA: outboundType=managedNATGateway + --outbound-type-sku StandardV2. + # Record with: azdev test test_aks_create_and_update_with_managed_nat_gateway_v2 --live + aks_name = self.create_random_name('cliakstest', 16) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + 'ssh_key_value': self.generate_ssh_keys(), + 'location': resource_group_location + }) + + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--vm-set-type VirtualMachineScaleSets -c 1 ' \ + '--outbound-type=managedNATGateway ' \ + '--outbound-type-sku=StandardV2 ' \ + '--nat-gateway-managed-outbound-ip-count=1 ' \ + '--nat-gateway-idle-timeout=4 ' \ + '--generate-ssh-keys ' \ + '--location={location}' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('networkProfile.outboundType', 'managedNATGateway'), + self.check('networkProfile.natGatewayProfile.sku', 'StandardV2'), + self.check('networkProfile.natGatewayProfile.idleTimeoutInMinutes', 4), + self.check('networkProfile.natGatewayProfile.managedOutboundIpProfile.count', 1), + ]) + + # Update keeps the StandardV2 SKU and applies the new managed outbound IP count / idle timeout. + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--nat-gateway-managed-outbound-ip-count=2 ' \ + '--nat-gateway-idle-timeout=30 ' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('networkProfile.outboundType', 'managedNATGateway'), + self.check('networkProfile.natGatewayProfile.sku', 'StandardV2'), + self.check('networkProfile.natGatewayProfile.idleTimeoutInMinutes', 30), + self.check('networkProfile.natGatewayProfile.managedOutboundIpProfile.count', 2), + ]) + @AllowLargeResponse() @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='eastus') def test_aks_update_outbound_from_slb_to_natgateway(self, resource_group, resource_group_location): From ae10203cf7db23e508a3298e097501682b007505 Mon Sep 17 00:00:00 2001 From: cdossa Date: Thu, 3 Sep 2026 09:34:25 -0700 Subject: [PATCH 3/7] {AKS} az aks: fix CI - exclude new NAT gateway params from missing_parameter_test_coverage; mark StandardV2 scenario test live-only --- .../command_modules/acs/linter_exclusions.yml | 24 +++++++++++++++++++ .../acs/tests/latest/test_aks_commands.py | 1 + 2 files changed, 25 insertions(+) diff --git a/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml b/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml index b3eb404069f..4530e07e60f 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml +++ b/src/azure-cli/azure/cli/command_modules/acs/linter_exclusions.yml @@ -10,6 +10,18 @@ aks create: enable_hosted_system: rule_exclusions: - missing_parameter_test_coverage + nat_gateway_sku: + rule_exclusions: + - missing_parameter_test_coverage + nat_gateway_managed_outbound_ipv6_count: + rule_exclusions: + - missing_parameter_test_coverage + nat_gateway_outbound_ip_ids: + rule_exclusions: + - missing_parameter_test_coverage + nat_gateway_outbound_ip_prefix_ids: + rule_exclusions: + - missing_parameter_test_coverage appgw_watch_namespace: rule_exclusions: - option_length_too_long @@ -134,6 +146,18 @@ aks enable-addons: - option_length_too_long aks update: parameters: + nat_gateway_sku: + rule_exclusions: + - missing_parameter_test_coverage + nat_gateway_managed_outbound_ipv6_count: + rule_exclusions: + - missing_parameter_test_coverage + nat_gateway_outbound_ip_ids: + rule_exclusions: + - missing_parameter_test_coverage + nat_gateway_outbound_ip_prefix_ids: + rule_exclusions: + - missing_parameter_test_coverage disable_secret_rotation: rule_exclusions: - option_length_too_long diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py index b4152999bc4..2d7be05afaf 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py @@ -1419,6 +1419,7 @@ def test_aks_create_and_update_with_managed_nat_gateway_outbound(self, resource_ self.check('networkProfile.natGatewayProfile.managedOutboundIpProfile.count', 2), ]) + @live_only() # live-only until a StandardV2 NAT gateway recording is captured @AllowLargeResponse(8192) @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='westus2') def test_aks_create_and_update_with_managed_nat_gateway_v2(self, resource_group, resource_group_location): From 0db3557ff2ea2b858f77fc40f39bcda2b5ef2b67 Mon Sep 17 00:00:00 2001 From: cdossa Date: Thu, 3 Sep 2026 10:01:35 -0700 Subject: [PATCH 4/7] {AKS} az aks: move NAT gateway V2 param help inline and add --nat-gw-ipv6-count alias (fix CLI linter) --- .../azure/cli/command_modules/acs/_help.py | 24 --------------- .../azure/cli/command_modules/acs/_params.py | 30 +++++++++++++++---- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_help.py b/src/azure-cli/azure/cli/command_modules/acs/_help.py index 7e8b00d6598..108cc6d538e 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_help.py @@ -190,18 +190,6 @@ type: string short-summary: SKU of the managed NAT Gateway (Standard or StandardV2). long-summary: Only valid with --outbound-type managedNATGateway. Omit to default to StandardV2 where the region supports it, otherwise Standard. StandardV2 adds zone resiliency, IPv6 support, and higher bandwidth. - - name: --nat-gateway-managed-outbound-ipv6-count - type: int - short-summary: NAT gateway managed outbound IPv6 count. - long-summary: Desired number of managed IPv6 outbound IPs for NAT gateway outbound connection on dual-stack clusters. Please specify a value in the range of [0, 16]. Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. - - name: --nat-gateway-outbound-ips - type: string - short-summary: Comma-separated public IP resource IDs for the cluster NAT gateway. - long-summary: Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. - - name: --nat-gateway-outbound-ip-prefixes - type: string - short-summary: Comma-separated public IP prefix resource IDs for the cluster NAT gateway. - long-summary: Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. - name: --outbound-type type: string short-summary: How outbound traffic will be configured for a cluster. @@ -887,18 +875,6 @@ type: string short-summary: SKU of the managed NAT Gateway (Standard or StandardV2). long-summary: Only valid with --outbound-type managedNATGateway. Migrate an existing Standard (V1) cluster to StandardV2 by passing StandardV2. StandardV2 adds zone resiliency, IPv6 support, and higher bandwidth. Downgrade from StandardV2 to Standard is not supported. - - name: --nat-gateway-managed-outbound-ipv6-count - type: int - short-summary: NAT gateway managed outbound IPv6 count. - long-summary: Desired number of managed IPv6 outbound IPs for NAT gateway outbound connection on dual-stack clusters. Please specify a value in the range of [0, 16]. Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. - - name: --nat-gateway-outbound-ips - type: string - short-summary: Comma-separated public IP resource IDs for the cluster NAT gateway. - long-summary: Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. - - name: --nat-gateway-outbound-ip-prefixes - type: string - short-summary: Comma-separated public IP prefix resource IDs for the cluster NAT gateway. - long-summary: Only valid with --outbound-type managedNATGateway and --outbound-type-sku StandardV2. - name: --outbound-type type: string short-summary: How outbound traffic will be configured for a cluster. diff --git a/src/azure-cli/azure/cli/command_modules/acs/_params.py b/src/azure-cli/azure/cli/command_modules/acs/_params.py index 05c508318ca..84ec10966b4 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -434,11 +434,20 @@ def load_arguments(self, _): c.argument('load_balancer_backend_pool_type', arg_type=get_enum_type(backend_pool_types)) c.argument('nrg_lockdown_restriction_level', arg_type=get_enum_type(nrg_lockdown_restriction_levels)) c.argument('nat_gateway_managed_outbound_ip_count', type=int, validator=validate_nat_gateway_managed_outbound_ip_count) - c.argument('nat_gateway_managed_outbound_ipv6_count', type=int, validator=validate_nat_gateway_managed_outbound_ipv6_count) + c.argument('nat_gateway_managed_outbound_ipv6_count', + options_list=['--nat-gateway-managed-outbound-ipv6-count', '--nat-gw-ipv6-count'], + type=int, validator=validate_nat_gateway_managed_outbound_ipv6_count, + help='NAT gateway managed outbound IPv6 count. Only valid with --outbound-type ' + 'managedNATGateway and --outbound-type-sku StandardV2.') c.argument('nat_gateway_idle_timeout', type=int, validator=validate_nat_gateway_idle_timeout) c.argument('nat_gateway_sku', options_list=['--outbound-type-sku'], arg_type=get_enum_type(nat_gateway_skus), validator=validate_outbound_type_sku) - c.argument('nat_gateway_outbound_ip_ids', options_list=['--nat-gateway-outbound-ips', '--nat-gw-ips']) - c.argument('nat_gateway_outbound_ip_prefix_ids', options_list=['--nat-gateway-outbound-ip-prefixes', '--nat-gw-prefixes']) + c.argument('nat_gateway_outbound_ip_ids', options_list=['--nat-gateway-outbound-ips', '--nat-gw-ips'], + help='Comma-separated public IP resource IDs for the cluster NAT gateway. ' + 'Only valid with --outbound-type-sku StandardV2.') + c.argument('nat_gateway_outbound_ip_prefix_ids', + options_list=['--nat-gateway-outbound-ip-prefixes', '--nat-gw-prefixes'], + help='Comma-separated public IP prefix resource IDs for the cluster NAT gateway. ' + 'Only valid with --outbound-type-sku StandardV2.') c.argument('outbound_type', arg_type=get_enum_type(outbound_types), validator=validate_nat_gateway_v2_params) c.argument('network_plugin', arg_type=get_enum_type(network_plugins)) c.argument('network_plugin_mode', arg_type=get_enum_type(network_plugin_modes)) @@ -705,11 +714,20 @@ def load_arguments(self, _): c.argument("load_balancer_sku", arg_type=get_enum_type([CONST_LOAD_BALANCER_SKU_STANDARD]), validator=validate_load_balancer_sku) c.argument('nrg_lockdown_restriction_level', arg_type=get_enum_type(nrg_lockdown_restriction_levels)) c.argument('nat_gateway_managed_outbound_ip_count', type=int, validator=validate_nat_gateway_managed_outbound_ip_count) - c.argument('nat_gateway_managed_outbound_ipv6_count', type=int, validator=validate_nat_gateway_managed_outbound_ipv6_count) + c.argument('nat_gateway_managed_outbound_ipv6_count', + options_list=['--nat-gateway-managed-outbound-ipv6-count', '--nat-gw-ipv6-count'], + type=int, validator=validate_nat_gateway_managed_outbound_ipv6_count, + help='NAT gateway managed outbound IPv6 count. Only valid with --outbound-type ' + 'managedNATGateway and --outbound-type-sku StandardV2.') c.argument('nat_gateway_idle_timeout', type=int, validator=validate_nat_gateway_idle_timeout) c.argument('nat_gateway_sku', options_list=['--outbound-type-sku'], arg_type=get_enum_type(nat_gateway_skus), validator=validate_outbound_type_sku_for_update) - c.argument('nat_gateway_outbound_ip_ids', options_list=['--nat-gateway-outbound-ips', '--nat-gw-ips']) - c.argument('nat_gateway_outbound_ip_prefix_ids', options_list=['--nat-gateway-outbound-ip-prefixes', '--nat-gw-prefixes']) + c.argument('nat_gateway_outbound_ip_ids', options_list=['--nat-gateway-outbound-ips', '--nat-gw-ips'], + help='Comma-separated public IP resource IDs for the cluster NAT gateway. ' + 'Only valid with --outbound-type-sku StandardV2.') + c.argument('nat_gateway_outbound_ip_prefix_ids', + options_list=['--nat-gateway-outbound-ip-prefixes', '--nat-gw-prefixes'], + help='Comma-separated public IP prefix resource IDs for the cluster NAT gateway. ' + 'Only valid with --outbound-type-sku StandardV2.') c.argument('network_dataplane', arg_type=get_enum_type(network_dataplanes)) c.argument('network_plugin', arg_type=get_enum_type(network_plugins)) c.argument('network_policy', arg_type=get_enum_type(network_policies)) From df09593d39d8d42b5363c87b3b36fd8bc3c6bc4e Mon Sep 17 00:00:00 2001 From: Christine DOSSA <119143674+christine33-creator@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:13:50 -0700 Subject: [PATCH 5/7] Refactor NAT gateway outbound IP assignment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/azure-cli/azure/cli/command_modules/acs/_natgateway.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py b/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py index c4b68598cf1..1b3158e793f 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py @@ -100,10 +100,10 @@ def configure_nat_gateway_profile( if outbound_ip_ids is not None: ManagedClusterNATGatewayProfileOutboundIPs = models.ManagedClusterNATGatewayProfileOutboundIPs + if ManagedClusterNATGatewayProfileOutboundIPs is None: + raise ValueError("The selected AKS API profile/SDK does not support setting NAT gateway outbound IPs.") ip_id_list = [x.strip() for x in outbound_ip_ids.split(',') if x.strip()] - profile.outbound_i_ps = ManagedClusterNATGatewayProfileOutboundIPs( - public_i_ps=ip_id_list - ) + profile.outbound_i_ps = ManagedClusterNATGatewayProfileOutboundIPs(public_i_ps=ip_id_list) if outbound_ip_prefix_ids is not None: ManagedClusterNATGatewayProfileOutboundIpPrefixes = models.ManagedClusterNATGatewayProfileOutboundIpPrefixes From 03fd7cce5d240b367fad60f9032203f8c0077ea6 Mon Sep 17 00:00:00 2001 From: cdossa Date: Fri, 4 Sep 2026 10:31:58 -0700 Subject: [PATCH 6/7] {AKS} az aks: clarify NAT gateway V2 validator docstrings (sku-less defaults to StandardV2 per GA matrix) --- .../azure/cli/command_modules/acs/_validators.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_validators.py b/src/azure-cli/azure/cli/command_modules/acs/_validators.py index c2b7f06ebcf..4796f4ab628 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_validators.py @@ -398,10 +398,12 @@ def validate_outbound_type_sku_for_update(namespace): def validate_nat_gateway_v2_params(namespace): - """Validate StandardV2-only NAT gateway params on create. + """Validate the V2-only NAT gateway params on create. The V2-only params (managed IPv6 count, BYO outbound IPs / IP prefixes) require the managed NAT - gateway outbound type with the StandardV2 SKU; the Standard (V1) SKU cannot carry them. On create + gateway outbound type at the StandardV2 tier; the Standard (V1) SKU cannot carry them. Omitting + --outbound-type-sku is tolerated and defaults to StandardV2 where the region supports it, so only + an explicit Standard SKU is rejected here (region availability is enforced by the RP). On create --outbound-type must be set explicitly to managedNATGateway. """ v2_params = [ @@ -411,6 +413,7 @@ def validate_nat_gateway_v2_params(namespace): ] if not any(p is not None for p in v2_params): return + # Omitting the sku defaults to StandardV2 where supported, so only an explicit Standard is rejected. if namespace.outbound_type != 'managedNATGateway' or getattr(namespace, 'nat_gateway_sku', None) == 'Standard': raise InvalidArgumentValueError( "--nat-gateway-managed-outbound-ipv6-count, --nat-gateway-outbound-ips and " @@ -420,10 +423,12 @@ def validate_nat_gateway_v2_params(namespace): def validate_nat_gateway_v2_params_for_update(namespace): - """Validate StandardV2-only NAT gateway params on update. + """Validate the V2-only NAT gateway params on update. - Unlike create, --outbound-type may be omitted when the cluster is already managed NAT gateway; - only an explicit non-managed-NAT-gateway outbound type or the Standard SKU is rejected here. + As on create these require the StandardV2 tier; omitting --outbound-type-sku is tolerated + (defaults to StandardV2 where the region supports it) and only an explicit Standard SKU is + rejected. Unlike create, --outbound-type may be omitted when the cluster is already managed NAT + gateway; an explicit non-managed-NAT-gateway outbound type is also rejected. """ v2_params = [ getattr(namespace, 'nat_gateway_managed_outbound_ipv6_count', None), @@ -432,6 +437,7 @@ def validate_nat_gateway_v2_params_for_update(namespace): ] if not any(p is not None for p in v2_params): return + # Omitting the sku defaults to StandardV2 where supported, so only an explicit Standard is rejected. if (namespace.outbound_type is not None and namespace.outbound_type != 'managedNATGateway') or \ getattr(namespace, 'nat_gateway_sku', None) == 'Standard': raise InvalidArgumentValueError( From 636d6b01e9668b63cc55de101fbf622aeb9931b7 Mon Sep 17 00:00:00 2001 From: cdossa Date: Fri, 4 Sep 2026 11:17:51 -0700 Subject: [PATCH 7/7] {AKS} az aks: require StandardV2 for V2 params on update, fix ipv6 count range [1,16], and preserve IPv4 default of 1 --- .../cli/command_modules/acs/_natgateway.py | 3 +++ .../cli/command_modules/acs/_validators.py | 18 +++++++++------- .../acs/tests/latest/test_natgateway.py | 21 +++++++++++++++++++ .../acs/tests/latest/test_validators.py | 17 +++++++++++---- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py b/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py index 1b3158e793f..465de7c0191 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_natgateway.py @@ -92,6 +92,9 @@ def configure_nat_gateway_profile( profile.managed_outbound_ip_profile = ManagedClusterManagedOutboundIPProfile() if managed_outbound_ip_count is not None: profile.managed_outbound_ip_profile.count = managed_outbound_ip_count + elif profile.managed_outbound_ip_profile.count is None: + # SDK 41.6 no longer materializes the IPv4 default; keep the documented default of 1. + profile.managed_outbound_ip_profile.count = 1 if managed_outbound_ipv6_count is not None: profile.managed_outbound_ip_profile.count_ipv6 = managed_outbound_ipv6_count diff --git a/src/azure-cli/azure/cli/command_modules/acs/_validators.py b/src/azure-cli/azure/cli/command_modules/acs/_validators.py index 4796f4ab628..5d2cfad1d67 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_validators.py @@ -360,9 +360,9 @@ def validate_nat_gateway_managed_outbound_ipv6_count(namespace): """validate NAT gateway profile managed outbound IPv6 count (StandardV2, dual-stack only)""" ipv6_count = namespace.nat_gateway_managed_outbound_ipv6_count if ipv6_count is not None: - if ipv6_count < 0 or ipv6_count > 16: + if ipv6_count < 1 or ipv6_count > 16: raise InvalidArgumentValueError( - "--nat-gateway-managed-outbound-ipv6-count must be in the range [0,16]" + "--nat-gateway-managed-outbound-ipv6-count must be in the range [1,16]" ) @@ -425,10 +425,12 @@ def validate_nat_gateway_v2_params(namespace): def validate_nat_gateway_v2_params_for_update(namespace): """Validate the V2-only NAT gateway params on update. - As on create these require the StandardV2 tier; omitting --outbound-type-sku is tolerated - (defaults to StandardV2 where the region supports it) and only an explicit Standard SKU is - rejected. Unlike create, --outbound-type may be omitted when the cluster is already managed NAT - gateway; an explicit non-managed-NAT-gateway outbound type is also rejected. + The V2-only params (managed IPv6 count, BYO outbound IPs / IP prefixes) require the StandardV2 + tier. Unlike create, an omitted --outbound-type-sku is not a "default to StandardV2": on update + the RP preserves the cluster's existing SKU (which may be Standard), so the V2-only params + require an explicit --outbound-type-sku StandardV2. --outbound-type may be omitted when the + cluster is already managed NAT gateway; an explicit non-managed-NAT-gateway outbound type is + also rejected. """ v2_params = [ getattr(namespace, 'nat_gateway_managed_outbound_ipv6_count', None), @@ -437,9 +439,9 @@ def validate_nat_gateway_v2_params_for_update(namespace): ] if not any(p is not None for p in v2_params): return - # Omitting the sku defaults to StandardV2 where supported, so only an explicit Standard is rejected. + # On update the RP preserves the existing SKU (possibly Standard), so require an explicit StandardV2. if (namespace.outbound_type is not None and namespace.outbound_type != 'managedNATGateway') or \ - getattr(namespace, 'nat_gateway_sku', None) == 'Standard': + getattr(namespace, 'nat_gateway_sku', None) != 'StandardV2': raise InvalidArgumentValueError( "--nat-gateway-managed-outbound-ipv6-count, --nat-gateway-outbound-ips and " "--nat-gateway-outbound-ip-prefixes are only valid with --outbound-type managedNATGateway " diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_natgateway.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_natgateway.py index f7cb67d9001..81f06d81fad 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_natgateway.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_natgateway.py @@ -121,6 +121,27 @@ def test_create_with_sku_and_v2_params(self): self.assertEqual(profile.outbound_ip_prefixes.public_ip_prefixes, ["/sub/pfx1"]) self.assertEqual(profile.idle_timeout_in_minutes, 10) + def test_create_ipv6_only_defaults_ipv4_count(self): + # Only an IPv6 count is provided; the IPv4 count must fall back to the documented default of 1. + profile = natgateway.create_nat_gateway_profile( + None, None, models=self.nat_gateway_models, + managed_outbound_ipv6_count=3, + nat_gateway_sku="StandardV2", + ) + self.assertEqual(profile.managed_outbound_ip_profile.count, 1) + self.assertEqual(profile.managed_outbound_ip_profile.count_ipv6, 3) + + def test_update_ipv6_only_preserves_existing_ipv4_count(self): + origin = self.nat_gateway_models.ManagedClusterNATGatewayProfile( + managed_outbound_ip_profile=self.nat_gateway_models.ManagedClusterManagedOutboundIPProfile(count=5), + ) + profile = natgateway.update_nat_gateway_profile( + None, None, origin, models=self.nat_gateway_models, + managed_outbound_ipv6_count=2, + ) + self.assertEqual(profile.managed_outbound_ip_profile.count, 5) + self.assertEqual(profile.managed_outbound_ip_profile.count_ipv6, 2) + def test_create_sku_only(self): profile = natgateway.create_nat_gateway_profile( None, None, models=self.nat_gateway_models, nat_gateway_sku="StandardV2" diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py index 405af72fa5c..db7d20f569d 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py @@ -2082,14 +2082,23 @@ def test_v2_params_create_requires_managed_nat_gateway_and_v2_sku(self): validators.validate_nat_gateway_v2_params(self._ns(nat_gateway_managed_outbound_ipv6_count=1, outbound_type="managedNATGateway", nat_gateway_sku="Standard")) validators.validate_nat_gateway_v2_params(self._ns(outbound_type="loadBalancer")) - def test_v2_params_update_allows_omitted_outbound_type(self): - validators.validate_nat_gateway_v2_params_for_update(self._ns(nat_gateway_managed_outbound_ipv6_count=1, outbound_type=None)) + def test_v2_params_update_requires_v2_sku(self): + # On update the RP preserves the existing SKU (possibly Standard), so V2 params require an + # explicit StandardV2; a sku-less update is rejected. + validators.validate_nat_gateway_v2_params_for_update(self._ns(nat_gateway_managed_outbound_ipv6_count=1, outbound_type=None, nat_gateway_sku="StandardV2")) + validators.validate_nat_gateway_v2_params_for_update(self._ns(nat_gateway_managed_outbound_ipv6_count=1, outbound_type="managedNATGateway", nat_gateway_sku="StandardV2")) with self.assertRaises(InvalidArgumentValueError): - validators.validate_nat_gateway_v2_params_for_update(self._ns(nat_gateway_outbound_ip_ids="/sub/ip", outbound_type="loadBalancer")) + validators.validate_nat_gateway_v2_params_for_update(self._ns(nat_gateway_managed_outbound_ipv6_count=1, outbound_type=None)) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_nat_gateway_v2_params_for_update(self._ns(nat_gateway_managed_outbound_ipv6_count=1, outbound_type=None, nat_gateway_sku="Standard")) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_nat_gateway_v2_params_for_update(self._ns(nat_gateway_outbound_ip_ids="/sub/ip", outbound_type="loadBalancer", nat_gateway_sku="StandardV2")) def test_ipv6_count_range(self): - validators.validate_nat_gateway_managed_outbound_ipv6_count(self._ns(nat_gateway_managed_outbound_ipv6_count=0)) + validators.validate_nat_gateway_managed_outbound_ipv6_count(self._ns(nat_gateway_managed_outbound_ipv6_count=1)) validators.validate_nat_gateway_managed_outbound_ipv6_count(self._ns(nat_gateway_managed_outbound_ipv6_count=16)) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_nat_gateway_managed_outbound_ipv6_count(self._ns(nat_gateway_managed_outbound_ipv6_count=0)) with self.assertRaises(InvalidArgumentValueError): validators.validate_nat_gateway_managed_outbound_ipv6_count(self._ns(nat_gateway_managed_outbound_ipv6_count=17))