Skip to content

Commit a5dfbbb

Browse files
committed
Support IPv6 for Subnet/SubnetSet
Signed-off-by: Yanjun Zhou <yanjun.zhou@broadcom.com>
1 parent f21b666 commit a5dfbbb

24 files changed

Lines changed: 1532 additions & 157 deletions

build/yaml/crd/vpc/crd.nsx.vmware.com_subnets.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ spec:
167167
- DHCPServer
168168
- DHCPRelay
169169
- DHCPDeactivated
170+
- DHCPServerStateless
170171
type: string
171172
type: object
172173
x-kubernetes-validations:

build/yaml/crd/vpc/crd.nsx.vmware.com_subnetsets.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ spec:
116116
- DHCPServer
117117
- DHCPRelay
118118
- DHCPDeactivated
119+
- DHCPServerStateless
119120
type: string
120121
type: object
121122
x-kubernetes-validations:

docs/ref/apis/vpc.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ _Appears in:_
207207
| `DHCPDeactivated` | |
208208
| `DHCPServer` | |
209209
| `DHCPRelay` | |
210+
| `DHCPServerStateless` | |
210211

211212

212213
#### DHCPv6ServerAdditionalConfig
@@ -908,7 +909,7 @@ _Appears in:_
908909

909910
| Field | Description | Default | Validation |
910911
| --- | --- | --- | --- |
911-
| `mode` _[DHCPConfigMode](#dhcpconfigmode)_ | DHCP Mode. DHCPDeactivated will be used if it is not defined. | | Enum: [DHCPServer DHCPRelay DHCPDeactivated] <br /> |
912+
| `mode` _[DHCPConfigMode](#dhcpconfigmode)_ | DHCP Mode. DHCPDeactivated will be used if it is not defined. | | Enum: [DHCPServer DHCPRelay DHCPDeactivated DHCPServerStateless] <br /> |
912913
| `dhcpServerAdditionalConfig` _[DHCPServerAdditionalConfig](#dhcpserveradditionalconfig)_ | Additional DHCP server config for a VPC Subnet. | | |
913914

914915

@@ -1236,7 +1237,7 @@ _Appears in:_
12361237

12371238
| Field | Description | Default | Validation |
12381239
| --- | --- | --- | --- |
1239-
| `vpc` _string_ | NSX path of the VPC the Namespace is associated with.<br />If vpc is set, only defaultSubnetSize takes effect, other fields are ignored. | | |
1240+
| `vpc` _string_ | NSX path of the VPC the Namespace is associated with.<br />If vpc is set, only defaultSubnetSize and defaultIPv6PrefixLength take effect, other fields are ignored. | | |
12401241
| `subnets` _[SharedSubnet](#sharedsubnet) array_ | Shared Subnets the Namespace is associated with. | | |
12411242
| `nsxProject` _string_ | NSX Project the Namespace is associated with. | | |
12421243
| `vpcConnectivityProfile` _string_ | VPCConnectivityProfile Path. This profile has configuration related to creating VPC transit gateway attachment. | | |

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ require (
3434
github.com/vmware/govmomi v0.53.1
3535
github.com/vmware/vsphere-automation-sdk-go/lib v0.8.0
3636
github.com/vmware/vsphere-automation-sdk-go/runtime v0.8.0
37-
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260310075027-d32fca6a7b22
37+
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260506074423-13747423203f
3838
github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260310075027-d32fca6a7b22
3939
go.uber.org/automaxprocs v1.6.0
4040
go.uber.org/zap v1.27.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ github.com/vmware/vsphere-automation-sdk-go/lib v0.8.0 h1:u1SXOTM6D4Ygb3jeidj2Rd
175175
github.com/vmware/vsphere-automation-sdk-go/lib v0.8.0/go.mod h1:8d5JTwjpM/Z03n/IZb0fwmXkJNWvWwuLXBqoakqYio4=
176176
github.com/vmware/vsphere-automation-sdk-go/runtime v0.8.0 h1:KnDIX9LY0nru7iMQTg0sy9vChhyorPo5OdASM2MaAcI=
177177
github.com/vmware/vsphere-automation-sdk-go/runtime v0.8.0/go.mod h1:DzLetYAmw1+vj7bqElRWEpuy40WYE/woL3alsymYa/c=
178-
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260310075027-d32fca6a7b22 h1:yDMJj+UG0u9aDdC0Q1byw8QEjfPd8gm7QKB2mo2oU1I=
179-
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260310075027-d32fca6a7b22/go.mod h1:C3JVOHRVLrGBQ8kTWAiGYlRz5UQC5qAcTdt3tvA+5P0=
178+
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260506074423-13747423203f h1:HvbZGTOUm9rJDG7ngNQSd5UC5ikiZI/M3cUai8u5+Jg=
179+
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260506074423-13747423203f/go.mod h1:C3JVOHRVLrGBQ8kTWAiGYlRz5UQC5qAcTdt3tvA+5P0=
180180
github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260310075027-d32fca6a7b22 h1:SKbUc9p+LFUwtPvjk9WCwrjstN6NpewgPx4eWSIZq+k=
181181
github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260310075027-d32fca6a7b22/go.mod h1:ugk9I4YM62SSAox57l5NAVBCRIkPQ1RNLb3URxyTADc=
182182
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=

pkg/apis/vpc/v1alpha1/subnet_types.go

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,22 @@ type ConnectivityState string
1414
type IPAddressType string
1515

1616
const (
17-
AccessModePublic string = "Public"
18-
AccessModePrivate string = "Private"
19-
AccessModeProject string = "PrivateTGW"
20-
AccessModeL2Only string = "L2Only"
21-
DHCPConfigModeDeactivated string = "DHCPDeactivated"
22-
DHCPConfigModeServer string = "DHCPServer"
23-
DHCPConfigModeRelay string = "DHCPRelay"
24-
DHCPv6ConfigModeDeactivated DHCPv6ConfigMode = "DHCPDeactivated"
25-
DHCPv6ConfigModeServer DHCPv6ConfigMode = "DHCPServer"
26-
DHCPv6ConfigModeRelay DHCPv6ConfigMode = "DHCPRelay"
27-
ConnectivityStateConnected ConnectivityState = "Connected"
28-
ConnectivityStateDisconnected ConnectivityState = "Disconnected"
29-
IPAddressTypeIPv4 IPAddressType = "IPv4"
30-
IPAddressTypeIPv6 IPAddressType = "IPv6"
31-
IPAddressTypeIPv4IPv6 IPAddressType = "IPv4IPv6"
17+
AccessModePublic string = "Public"
18+
AccessModePrivate string = "Private"
19+
AccessModeProject string = "PrivateTGW"
20+
AccessModeL2Only string = "L2Only"
21+
DHCPConfigModeDeactivated string = "DHCPDeactivated"
22+
DHCPConfigModeServer string = "DHCPServer"
23+
DHCPConfigModeRelay string = "DHCPRelay"
24+
DHCPv6ConfigModeDeactivated DHCPv6ConfigMode = "DHCPDeactivated"
25+
DHCPv6ConfigModeServer DHCPv6ConfigMode = "DHCPServer"
26+
DHCPv6ConfigModeRelay DHCPv6ConfigMode = "DHCPRelay"
27+
DHCPv6ConfigModeServerStateless DHCPv6ConfigMode = "DHCPServerStateless"
28+
ConnectivityStateConnected ConnectivityState = "Connected"
29+
ConnectivityStateDisconnected ConnectivityState = "Disconnected"
30+
IPAddressTypeIPv4 IPAddressType = "IPv4"
31+
IPAddressTypeIPv6 IPAddressType = "IPv6"
32+
IPAddressTypeIPv4IPv6 IPAddressType = "IPv4IPv6"
3233
)
3334

3435
// SubnetSpec defines the desired state of Subnet.
@@ -162,7 +163,7 @@ type DHCPServerAdditionalConfig struct {
162163
// +kubebuilder:validation:XValidation:rule="(!has(self.mode)|| self.mode=='DHCPDeactivated' || self.mode=='DHCPRelay' ) && (!has(self.dhcpServerAdditionalConfig) || !has(self.dhcpServerAdditionalConfig.reservedIPRanges) || size(self.dhcpServerAdditionalConfig.reservedIPRanges)==0) || has(self.mode) && self.mode=='DHCPServer'", message="DHCPServerAdditionalConfig must be cleared when Subnet has DHCP relay enabled or DHCP is deactivated."
163164
type SubnetDHCPConfig struct {
164165
// DHCP Mode. DHCPDeactivated will be used if it is not defined.
165-
// +kubebuilder:validation:Enum=DHCPServer;DHCPRelay;DHCPDeactivated
166+
// +kubebuilder:validation:Enum=DHCPServer;DHCPRelay;DHCPDeactivated;DHCPServerStateless
166167
Mode DHCPConfigMode `json:"mode,omitempty"`
167168
// Additional DHCP server config for a VPC Subnet.
168169
DHCPServerAdditionalConfig DHCPServerAdditionalConfig `json:"dhcpServerAdditionalConfig,omitempty"`

pkg/controllers/common/utils.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,65 @@ func PodIsDeleted(pod *v1.Pod) bool {
616616
return pod.Status.Phase == v1.PodSucceeded ||
617617
pod.Status.Phase == v1.PodFailed
618618
}
619+
620+
// ConvertCRIPAddressTypeToNSX converts CR IPAddressType to NSX API format
621+
// v1alpha1 format: IPV4, IPV6, IPV4IPV6
622+
// NSX format: IPV4, IPV6, IPV4_IPV6
623+
func ConvertCRIPAddressTypeToNSX(crType v1alpha1.IPAddressType) string {
624+
switch crType {
625+
case v1alpha1.IPAddressTypeIPv4:
626+
return "IPV4"
627+
case v1alpha1.IPAddressTypeIPv6:
628+
return "IPV6"
629+
case v1alpha1.IPAddressTypeIPv4IPv6:
630+
return "IPV4_IPV6"
631+
default:
632+
return "IPV4" // Default to IPv4
633+
}
634+
}
635+
636+
// ConvertNSXIPAddressTypeToCR converts NSX API IPAddressType to CR format
637+
// NSX format: IPV4, IPV6, IPV4_IPV6
638+
// v1alpha1 format: IPV4, IPV6, IPV4IPV6
639+
func ConvertNSXIPAddressTypeToCR(nsxType string) v1alpha1.IPAddressType {
640+
switch nsxType {
641+
case "IPV4":
642+
return v1alpha1.IPAddressTypeIPv4
643+
case "IPV6":
644+
return v1alpha1.IPAddressTypeIPv6
645+
case "IPV4_IPV6":
646+
return v1alpha1.IPAddressTypeIPv4IPv6
647+
default:
648+
return v1alpha1.IPAddressTypeIPv4 // Default to IPv4
649+
}
650+
}
651+
652+
// IntersectIPAddressTypes computes the intersection of multiple IP address types
653+
// Returns the most restrictive common type, or error if no intersection
654+
// Example:
655+
// intersection of [IPv4IPv6, IPv6] should equal IPv6
656+
// intersection of [IPv4IPv6, IPv4] should equal IPv4
657+
func IntersectIPAddressTypes(types []v1alpha1.IPAddressType) (v1alpha1.IPAddressType, error) {
658+
if len(types) == 0 {
659+
return "", fmt.Errorf("no IP address types provided")
660+
}
661+
662+
result := types[0]
663+
664+
for _, t := range types[1:] {
665+
if result == t {
666+
continue
667+
}
668+
669+
if result == v1alpha1.IPAddressTypeIPv4IPv6 {
670+
result = t // Dual-stack intersects to the more specific type
671+
} else if t == v1alpha1.IPAddressTypeIPv4IPv6 {
672+
// Dual-stack with single-stack: keep single-stack (more restrictive)
673+
continue
674+
} else if result != t {
675+
return "", fmt.Errorf("no intersection between IP address types %s and %s", result, t)
676+
}
677+
}
678+
679+
return result, nil
680+
}

pkg/controllers/common/utils_test.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,3 +1174,191 @@ func TestPodIsDeleted(t *testing.T) {
11741174
})
11751175
}
11761176
}
1177+
1178+
func TestConvertCRIPAddressTypeToNSX(t *testing.T) {
1179+
tests := []struct {
1180+
name string
1181+
crType v1alpha1.IPAddressType
1182+
expected string
1183+
}{
1184+
{
1185+
name: "IPv4 conversion",
1186+
crType: v1alpha1.IPAddressTypeIPv4,
1187+
expected: "IPV4",
1188+
},
1189+
{
1190+
name: "IPv6 conversion",
1191+
crType: v1alpha1.IPAddressTypeIPv6,
1192+
expected: "IPV6",
1193+
},
1194+
{
1195+
name: "IPv4IPv6 conversion",
1196+
crType: v1alpha1.IPAddressTypeIPv4IPv6,
1197+
expected: "IPV4_IPV6",
1198+
},
1199+
{
1200+
name: "empty type defaults to IPv4",
1201+
crType: v1alpha1.IPAddressType(""),
1202+
expected: "IPV4",
1203+
},
1204+
{
1205+
name: "unknown type defaults to IPv4",
1206+
crType: v1alpha1.IPAddressType("Unknown"),
1207+
expected: "IPV4",
1208+
},
1209+
}
1210+
1211+
for _, tt := range tests {
1212+
t.Run(tt.name, func(t *testing.T) {
1213+
result := ConvertCRIPAddressTypeToNSX(tt.crType)
1214+
assert.Equal(t, tt.expected, result)
1215+
})
1216+
}
1217+
}
1218+
1219+
func TestConvertNSXIPAddressTypeToCR(t *testing.T) {
1220+
tests := []struct {
1221+
name string
1222+
nsxType string
1223+
expected v1alpha1.IPAddressType
1224+
}{
1225+
{
1226+
name: "IPV4 conversion",
1227+
nsxType: "IPV4",
1228+
expected: v1alpha1.IPAddressTypeIPv4,
1229+
},
1230+
{
1231+
name: "IPV6 conversion",
1232+
nsxType: "IPV6",
1233+
expected: v1alpha1.IPAddressTypeIPv6,
1234+
},
1235+
{
1236+
name: "IPV4_IPV6 conversion",
1237+
nsxType: "IPV4_IPV6",
1238+
expected: v1alpha1.IPAddressTypeIPv4IPv6,
1239+
},
1240+
{
1241+
name: "empty type defaults to IPv4",
1242+
nsxType: "",
1243+
expected: v1alpha1.IPAddressTypeIPv4,
1244+
},
1245+
{
1246+
name: "unknown type defaults to IPv4",
1247+
nsxType: "UNKNOWN",
1248+
expected: v1alpha1.IPAddressTypeIPv4,
1249+
},
1250+
}
1251+
1252+
for _, tt := range tests {
1253+
t.Run(tt.name, func(t *testing.T) {
1254+
result := ConvertNSXIPAddressTypeToCR(tt.nsxType)
1255+
assert.Equal(t, tt.expected, result)
1256+
})
1257+
}
1258+
}
1259+
1260+
func TestIntersectIPAddressTypes(t *testing.T) {
1261+
tests := []struct {
1262+
name string
1263+
types []v1alpha1.IPAddressType
1264+
expected v1alpha1.IPAddressType
1265+
expectedErr bool
1266+
errMsg string
1267+
}{
1268+
{
1269+
name: "single IPv4 type",
1270+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv4},
1271+
expected: v1alpha1.IPAddressTypeIPv4,
1272+
expectedErr: false,
1273+
},
1274+
{
1275+
name: "single IPv6 type",
1276+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv6},
1277+
expected: v1alpha1.IPAddressTypeIPv6,
1278+
expectedErr: false,
1279+
},
1280+
{
1281+
name: "dual stack IPv4IPv6",
1282+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv4IPv6},
1283+
expected: v1alpha1.IPAddressTypeIPv4IPv6,
1284+
expectedErr: false,
1285+
},
1286+
{
1287+
name: "IPv4IPv6 with IPv4 returns IPv4",
1288+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv4IPv6, v1alpha1.IPAddressTypeIPv4},
1289+
expected: v1alpha1.IPAddressTypeIPv4,
1290+
expectedErr: false,
1291+
},
1292+
{
1293+
name: "IPv4IPv6 with IPv6 returns IPv6",
1294+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv4IPv6, v1alpha1.IPAddressTypeIPv6},
1295+
expected: v1alpha1.IPAddressTypeIPv6,
1296+
expectedErr: false,
1297+
},
1298+
{
1299+
name: "IPv4 with IPv4 same type",
1300+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv4, v1alpha1.IPAddressTypeIPv4},
1301+
expected: v1alpha1.IPAddressTypeIPv4,
1302+
expectedErr: false,
1303+
},
1304+
{
1305+
name: "IPv6 with IPv6 same type",
1306+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv6, v1alpha1.IPAddressTypeIPv6},
1307+
expected: v1alpha1.IPAddressTypeIPv6,
1308+
expectedErr: false,
1309+
},
1310+
{
1311+
name: "IPv4 with IPv6 no intersection",
1312+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv4, v1alpha1.IPAddressTypeIPv6},
1313+
expected: "",
1314+
expectedErr: true,
1315+
errMsg: "no intersection",
1316+
},
1317+
{
1318+
name: "IPv4 with IPv6 with IPv4IPv6 returns IPv4",
1319+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv4, v1alpha1.IPAddressTypeIPv6, v1alpha1.IPAddressTypeIPv4IPv6},
1320+
expected: "",
1321+
expectedErr: true,
1322+
errMsg: "no intersection",
1323+
},
1324+
{
1325+
name: "empty slice",
1326+
types: []v1alpha1.IPAddressType{},
1327+
expected: "",
1328+
expectedErr: true,
1329+
errMsg: "no IP address types provided",
1330+
},
1331+
{
1332+
name: "IPv4 with IPv4IPv6 returns IPv4",
1333+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv4, v1alpha1.IPAddressTypeIPv4IPv6},
1334+
expected: v1alpha1.IPAddressTypeIPv4,
1335+
expectedErr: false,
1336+
},
1337+
{
1338+
name: "IPv6 with IPv4IPv6 returns IPv6",
1339+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv6, v1alpha1.IPAddressTypeIPv4IPv6},
1340+
expected: v1alpha1.IPAddressTypeIPv6,
1341+
expectedErr: false,
1342+
},
1343+
{
1344+
name: "IPv4IPv6 with IPv4 with IPv6 no intersection",
1345+
types: []v1alpha1.IPAddressType{v1alpha1.IPAddressTypeIPv4IPv6, v1alpha1.IPAddressTypeIPv4, v1alpha1.IPAddressTypeIPv6},
1346+
expected: "",
1347+
expectedErr: true,
1348+
errMsg: "no intersection",
1349+
},
1350+
}
1351+
1352+
for _, tt := range tests {
1353+
t.Run(tt.name, func(t *testing.T) {
1354+
result, err := IntersectIPAddressTypes(tt.types)
1355+
if tt.expectedErr {
1356+
assert.Error(t, err)
1357+
assert.Contains(t, err.Error(), tt.errMsg)
1358+
} else {
1359+
assert.NoError(t, err)
1360+
assert.Equal(t, tt.expected, result)
1361+
}
1362+
})
1363+
}
1364+
}

0 commit comments

Comments
 (0)