Skip to content

Commit 2fd173c

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

22 files changed

Lines changed: 1540 additions & 154 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. | | |

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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ import (
2525
"github.com/vmware-tanzu/nsx-operator/pkg/util"
2626
)
2727

28+
const (
29+
// NSX API IP Address Types
30+
NSXIPAddressTypeIPv4 = "IPV4"
31+
NSXIPAddressTypeIPv6 = "IPV6"
32+
NSXIPAddressTypeIPv4IPv6 = "IPV4_IPV6"
33+
)
34+
2835
var (
2936
log = logger.Log
3037
SubnetSetLocks sync.Map
@@ -616,3 +623,66 @@ func PodIsDeleted(pod *v1.Pod) bool {
616623
return pod.Status.Phase == v1.PodSucceeded ||
617624
pod.Status.Phase == v1.PodFailed
618625
}
626+
627+
// ConvertCRIPAddressTypeToNSX converts CR IPAddressType to NSX API format
628+
// v1alpha1 format: IPv4, IPv6, IPv4IPv6
629+
// NSX format: IPV4, IPV6, IPV4_IPV6
630+
func ConvertCRIPAddressTypeToNSX(crType v1alpha1.IPAddressType) string {
631+
switch crType {
632+
case v1alpha1.IPAddressTypeIPv4:
633+
return NSXIPAddressTypeIPv4
634+
case v1alpha1.IPAddressTypeIPv6:
635+
return NSXIPAddressTypeIPv6
636+
case v1alpha1.IPAddressTypeIPv4IPv6:
637+
return NSXIPAddressTypeIPv4IPv6
638+
default:
639+
log.Warn("Unknown IP address type, defaulting to IPv4", "unknownType", string(crType))
640+
return NSXIPAddressTypeIPv4
641+
}
642+
}
643+
644+
// ConvertNSXIPAddressTypeToCR converts NSX API IPAddressType to CR format
645+
// NSX format: IPV4, IPV6, IPV4_IPV6
646+
// v1alpha1 format: IPV4, IPV6, IPV4IPV6
647+
func ConvertNSXIPAddressTypeToCR(nsxType string) v1alpha1.IPAddressType {
648+
switch nsxType {
649+
case NSXIPAddressTypeIPv4:
650+
return v1alpha1.IPAddressTypeIPv4
651+
case NSXIPAddressTypeIPv6:
652+
return v1alpha1.IPAddressTypeIPv6
653+
case NSXIPAddressTypeIPv4IPv6:
654+
return v1alpha1.IPAddressTypeIPv4IPv6
655+
default:
656+
return v1alpha1.IPAddressTypeIPv4
657+
}
658+
}
659+
660+
// IntersectIPAddressTypes computes the intersection of multiple IP address types
661+
// Returns the most restrictive common type, or error if no intersection
662+
// Example:
663+
// intersection of [IPv4IPv6, IPv6] should equal IPv6
664+
// intersection of [IPv4IPv6, IPv4] should equal IPv4
665+
func IntersectIPAddressTypes(types []v1alpha1.IPAddressType) (v1alpha1.IPAddressType, error) {
666+
if len(types) == 0 {
667+
return "", fmt.Errorf("no IP address types provided")
668+
}
669+
670+
result := types[0]
671+
672+
for _, t := range types[1:] {
673+
if result == t {
674+
continue
675+
}
676+
677+
if result == v1alpha1.IPAddressTypeIPv4IPv6 {
678+
result = t // Dual-stack intersects to the more specific type
679+
} else if t == v1alpha1.IPAddressTypeIPv4IPv6 {
680+
// Dual-stack with single-stack: keep single-stack (more restrictive)
681+
continue
682+
} else if result != t {
683+
return "", fmt.Errorf("no intersection between IP address types %s and %s", result, t)
684+
}
685+
}
686+
687+
return result, nil
688+
}

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)