Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/aks-preview/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Pending
+++++++
* `az aks nodepool update`: Preserve the existing GPU management mode when `--enable-managed-gpu` is omitted, including when enabling, updating, or disabling the cluster autoscaler.
* `az aks alert-config add`: Reject an empty `--name` before looking up existing configurations instead of reporting that it already exists.
* `az aks nodepool add`: Support public IP configuration on secondary network interfaces, including mutually exclusive IP tags or a public IP prefix.

22.0.0b7
+++++++++
Expand Down
7 changes: 5 additions & 2 deletions src/aks-preview/azext_aks_preview/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -2667,11 +2667,14 @@
short-summary: Set the localDNS Profile for a nodepool with a JSON config file.
- name: --secondary-network-interfaces --secondary-nics
type: string
short-summary: Secondary network interface configurations as a JSON string or `@filename`.
short-summary: Create-only secondary network interface configurations as inline JSON or `@filename`.
long-summary: |-
Specify secondary NICs to attach to each node. Accepts inline JSON or `@filename`.
Example: '[{"type":"Standard","vnetSubnetId":"/subscriptions/.../subnets/mysubnet","enableAcceleratedNetworking":true}]'
Supported NIC types are "Standard" (requires vnetSubnetId) and "Dynamic".
A Standard NIC can include publicIPAddressConfiguration. Setting publicIPAddressVersion to "IPv4" enables public IP allocation for that NIC.
Within publicIPAddressConfiguration, optionally specify either ipTags or publicIPPrefixID. These properties are mutually exclusive.
Inline example: '[{"type":"Standard","vnetSubnetId":"/subscriptions/.../subnets/mysubnet","enableAcceleratedNetworking":true,"publicIPAddressConfiguration":{"publicIPAddressVersion":"IPv4","ipTags":[{"ipTagType":"RoutingPreference","tag":"Internet"}]}}]'
To load the same JSON array from a file, use `@filename`.
- name: --enable-managed-dranet
type: bool
short-summary: Enable Managed DRANET on the node pool.
Expand Down
8 changes: 6 additions & 2 deletions src/aks-preview/azext_aks_preview/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2550,8 +2550,12 @@ def load_arguments(self, _):
c.argument(
'secondary_network_interfaces',
options_list=['--secondary-network-interfaces', '--secondary-nics'],
help='Secondary network interface configurations as a JSON string or `@filename` to load from a file. '
'Example: \'[{"type":"Standard","vnetSubnetId":"/subscriptions/.../subnets/mysubnet"}]\'',
help='Create-only secondary network interface configurations as inline JSON or `@filename`. '
'For a Standard NIC, set publicIPAddressConfiguration.publicIPAddressVersion to "IPv4" '
'to allocate a public IP, with either ipTags or publicIPPrefixID, but not both. '
'Example: \'[{"type":"Standard","vnetSubnetId":"/subscriptions/.../subnets/mysubnet",'
'"publicIPAddressConfiguration":{"publicIPAddressVersion":"IPv4",'
'"ipTags":[{"ipTagType":"RoutingPreference","tag":"Internet"}]}}]\'',
is_preview=True,
)
c.argument(
Expand Down
22 changes: 22 additions & 0 deletions src/aks-preview/azext_aks_preview/agentpool_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1053,10 +1053,32 @@ def get_secondary_network_interfaces(self):
f"--secondary-network-interfaces: element at index {idx} "
f"must be a JSON object, got {type(item).__name__}."
)
public_ip_config = item.get("publicIPAddressConfiguration")
if public_ip_config is not None:
if not isinstance(public_ip_config, dict):
raise InvalidArgumentValueError(
"--secondary-network-interfaces: publicIPAddressConfiguration "
f"at index {idx} must be a JSON object."
)
ip_tags = public_ip_config.get("ipTags")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: could we add an explicit mutual-exclusion check for ipTags and publicIPPrefixID, along with a regression test? Both the help text and SDK contract say these properties are mutually exclusive, but the current validation only checks the JSON structure and still forwards both when supplied. Rejecting this combination locally would give users a clear CLI error instead of relying on resource-provider validation.

if ip_tags is not None:
if not isinstance(ip_tags, list):
raise InvalidArgumentValueError(
"--secondary-network-interfaces: ipTags in "
f"publicIPAddressConfiguration at index {idx} must be a JSON array."
)
Comment on lines +1063 to +1069
for tag_idx, ip_tag in enumerate(ip_tags):
if not isinstance(ip_tag, dict):
raise InvalidArgumentValueError(
"--secondary-network-interfaces: ipTags element at index "
f"{tag_idx} in publicIPAddressConfiguration at index {idx} "
"must be a JSON object."
)
result.append(self.models.AgentPoolNetworkInterface(
type=item.get("type"),
vnet_subnet_id=item.get("vnetSubnetId"),
enable_accelerated_networking=item.get("enableAcceleratedNetworking"),
public_ip_address_configuration=public_ip_config,
))
return result

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,7 @@ def common_get_secondary_network_interfaces(self):
self.assertEqual(len(result), 1)
self.assertEqual(result[0].type, "Standard")
self.assertEqual(result[0].vnet_subnet_id, "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1")
self.assertIsNone(result[0].public_ip_address_configuration)

# invalid JSON - not a list
ctx_3 = AKSPreviewAgentPoolContext(
Expand Down Expand Up @@ -1378,7 +1379,17 @@ def common_get_secondary_network_interfaces(self):
# @file input
import tempfile
import json
nics_data = [{"type": "Dynamic"}, {"type": "Standard", "vnetSubnetId": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1"}]
nics_data = [
{"type": "Dynamic"},
{
"type": "Standard",
"vnetSubnetId": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1",
"publicIPAddressConfiguration": {
"publicIPAddressVersion": "IPv4",
"publicIPPrefixID": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPPrefixes/prefix1",
},
},
]
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(nics_data, f)
tmp_path = f.name
Expand All @@ -1398,10 +1409,115 @@ def common_get_secondary_network_interfaces(self):
self.assertIsNone(result[0].vnet_subnet_id)
self.assertEqual(result[1].type, "Standard")
self.assertEqual(result[1].vnet_subnet_id, "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1")
self.assertEqual(
result[1].as_dict()["publicIPAddressConfiguration"],
{
"publicIPAddressVersion": "IPv4",
"publicIPPrefixID": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPPrefixes/prefix1",
},
)
finally:
import os
os.unlink(tmp_path)

def common_get_secondary_network_interfaces_with_public_ip_tags(self):
import json

ctx = AKSPreviewAgentPoolContext(
self.cmd,
AKSAgentPoolParamDict({
"secondary_network_interfaces": json.dumps([{
"type": "Standard",
"vnetSubnetId": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1",
"enableAcceleratedNetworking": False,
"publicIPAddressConfiguration": {
"publicIPAddressVersion": "IPv4",
"ipTags": [{
"ipTagType": "RoutingPreference",
"tag": "Internet",
}],
},
}]),
}),
self.models,
DecoratorMode.CREATE,
self.agentpool_decorator_mode,
)

self.assertEqual(
ctx.get_secondary_network_interfaces()[0].as_dict(),
{
"type": "Standard",
"vnetSubnetId": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1",
"enableAcceleratedNetworking": False,
"publicIPAddressConfiguration": {
"publicIPAddressVersion": "IPv4",
"ipTags": [{
"ipTagType": "RoutingPreference",
"tag": "Internet",
}],
},
},
)

def common_get_secondary_network_interfaces_allows_null_public_ip_fields(self):
ctx = AKSPreviewAgentPoolContext(
self.cmd,
AKSAgentPoolParamDict({
"secondary_network_interfaces": [{
"type": "Standard",
"publicIPAddressConfiguration": None,
}, {
"type": "Standard",
"publicIPAddressConfiguration": {"ipTags": None},
}, {
"type": "Standard",
"publicIPAddressConfiguration": {},
}],
}),
self.models,
DecoratorMode.CREATE,
self.agentpool_decorator_mode,
)

result = ctx.get_secondary_network_interfaces()
self.assertIsNone(result[0].public_ip_address_configuration)
self.assertEqual(
result[1].as_dict()["publicIPAddressConfiguration"],
{"ipTags": None},
)
self.assertEqual(
result[2].as_dict()["publicIPAddressConfiguration"],
{},
)

def common_get_secondary_network_interfaces_rejects_malformed_public_ip_fields(self):
malformed_values = [
({"publicIPAddressConfiguration": []}, "publicIPAddressConfiguration"),
({"publicIPAddressConfiguration": {"ipTags": {}}}, "ipTags"),
({"publicIPAddressConfiguration": {"ipTags": ["Internet"]}}, "ipTags"),
]

for nested_fields, expected_error_field in malformed_values:
with self.subTest(nested_fields=nested_fields):
ctx = AKSPreviewAgentPoolContext(
self.cmd,
AKSAgentPoolParamDict({
"secondary_network_interfaces": [{
"type": "Standard",
**nested_fields,
}],
}),
self.models,
DecoratorMode.CREATE,
self.agentpool_decorator_mode,
)
with self.assertRaisesRegex(
InvalidArgumentValueError,
expected_error_field,
):
ctx.get_secondary_network_interfaces()


class AKSPreviewAgentPoolContextStandaloneModeTestCase(
AKSPreviewAgentPoolContextCommonTestCase
Expand Down Expand Up @@ -1522,6 +1638,15 @@ def test_get_final_soak_duration(self):
def test_get_secondary_network_interfaces(self):
self.common_get_secondary_network_interfaces()

def test_get_secondary_network_interfaces_with_public_ip_tags(self):
self.common_get_secondary_network_interfaces_with_public_ip_tags()

def test_get_secondary_network_interfaces_allows_null_public_ip_fields(self):
self.common_get_secondary_network_interfaces_allows_null_public_ip_fields()

def test_get_secondary_network_interfaces_rejects_malformed_public_ip_fields(self):
self.common_get_secondary_network_interfaces_rejects_malformed_public_ip_fields()


class AKSPreviewAgentPoolContextManagedClusterModeTestCase(
AKSPreviewAgentPoolContextCommonTestCase
Expand Down Expand Up @@ -2149,6 +2274,62 @@ def common_set_up_managed_dranet(self):
dec_agentpool_2 = dec_2.set_up_agentpool_network_profile(agentpool_2)
self.assertEqual(dec_agentpool_2.network_profile.dranet.mode, "Managed")

def common_construct_agentpool_profile_serializes_secondary_nic_public_ip(self):
import inspect
import json

from azext_aks_preview.custom import aks_agentpool_add

raw_param_dict = {
name: parameter.default
for name, parameter in inspect.signature(aks_agentpool_add).parameters.items()
if parameter.default is not parameter.empty
}
raw_param_dict.update({
"resource_group_name": "test_rg_name",
"cluster_name": "test_cluster_name",
"nodepool_name": "test_nodepool_name",
"secondary_network_interfaces": json.dumps([{
"type": "Standard",
"vnetSubnetId": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1",
"publicIPAddressConfiguration": {
"publicIPAddressVersion": "IPv4",
"ipTags": [{
"ipTagType": "RoutingPreference",
"tag": "Internet",
}],
},
}]),
})
decorator = AKSPreviewAgentPoolAddDecorator(
self.cmd,
self.client,
raw_param_dict,
self.resource_type,
self.agentpool_decorator_mode,
)

with patch(
"azext_aks_preview.agentpool_decorator.cf_agent_pools",
return_value=Mock(list=Mock(return_value=[])),
):
payload = decorator.construct_agentpool_profile_preview().as_dict()

self.assertEqual(
payload["properties"]["networkProfile"]["secondaryNetworkInterfaces"],
[{
"type": "Standard",
"vnetSubnetId": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1",
"publicIPAddressConfiguration": {
"publicIPAddressVersion": "IPv4",
"ipTags": [{
"ipTagType": "RoutingPreference",
"tag": "Internet",
}],
},
}],
)

def common_set_up_virtual_machines_profile(self):
dec_1 = AKSPreviewAgentPoolAddDecorator(
self.cmd,
Expand Down Expand Up @@ -2593,6 +2774,9 @@ def test_set_up_agentpool_gateway_profile(self):
def test_set_up_managed_dranet(self):
self.common_set_up_managed_dranet()

def test_construct_agentpool_profile_serializes_secondary_nic_public_ip(self):
self.common_construct_agentpool_profile_serializes_secondary_nic_public_ip()

def test_set_up_virtual_machines_profile(self):
self.common_set_up_virtual_machines_profile()

Expand Down
Loading