Skip to content

Commit 00eb219

Browse files
committed
Add IPv6 support for IPBlocksInfo CRD
Support VpcConnectivityProfile.Ipv6Blocks in IPBlocksInfo sync Include paths from VpcConnectivityProfile.Ipv6Blocks into the same externalIPBlockPaths set as ExternalIpBlocks during IPBlocksInfo reconciliation. This ensures that external IPv6 IP blocks assigned to a VPC connectivity profile have their CIDRs and IP ranges surfaced in the IPBlocksInfo CR (ExternalIPCIDRs / ExternalIPRanges) without requiring any CRD schema change. Signed-off-by: Wenqi Qiu <wenqi.qiu@broadcom.com>
1 parent fc924c7 commit 00eb219

7 files changed

Lines changed: 193 additions & 14 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ go.work.sum
77
.scannerwork/
88
.coverage/
99
.golangci-bin/
10+
vendor

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ require (
3232
github.com/vmware/govmomi v0.27.4
3333
github.com/vmware/vsphere-automation-sdk-go/lib v0.7.0
3434
github.com/vmware/vsphere-automation-sdk-go/runtime v0.7.0
35-
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260310075027-d32fca6a7b22
36-
github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260310075027-d32fca6a7b22
35+
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260506074423-13747423203f
36+
github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260506074423-13747423203f
3737
go.uber.org/automaxprocs v1.5.3
3838
go.uber.org/zap v1.26.0
3939
golang.org/x/crypto v0.47.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,10 @@ github.com/vmware/vsphere-automation-sdk-go/lib v0.7.0 h1:pT+oqJ8FD5eUBQkl+e7LZw
159159
github.com/vmware/vsphere-automation-sdk-go/lib v0.7.0/go.mod h1:f3+6YVZpNcK2pYyiQ94BoHWmjMj9BnYav0vNFuTiDVM=
160160
github.com/vmware/vsphere-automation-sdk-go/runtime v0.7.0 h1:pSBxa9Agh6bgW8Hr0A1eQxuwnxGTnuAVox8iQb023hg=
161161
github.com/vmware/vsphere-automation-sdk-go/runtime v0.7.0/go.mod h1:qdzEFm2iK3dvlmm99EYYNxs70HbzuiHyENFD24Ps8fQ=
162-
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260310075027-d32fca6a7b22 h1:yDMJj+UG0u9aDdC0Q1byw8QEjfPd8gm7QKB2mo2oU1I=
163-
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260310075027-d32fca6a7b22/go.mod h1:C3JVOHRVLrGBQ8kTWAiGYlRz5UQC5qAcTdt3tvA+5P0=
164-
github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260310075027-d32fca6a7b22 h1:SKbUc9p+LFUwtPvjk9WCwrjstN6NpewgPx4eWSIZq+k=
165-
github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260310075027-d32fca6a7b22/go.mod h1:ugk9I4YM62SSAox57l5NAVBCRIkPQ1RNLb3URxyTADc=
162+
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260506074423-13747423203f h1:HvbZGTOUm9rJDG7ngNQSd5UC5ikiZI/M3cUai8u5+Jg=
163+
github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260506074423-13747423203f/go.mod h1:C3JVOHRVLrGBQ8kTWAiGYlRz5UQC5qAcTdt3tvA+5P0=
164+
github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260506074423-13747423203f h1:dzC9XLdl0fdqZB/K97m/NMY9o/voA2Qa4shHRnK900Q=
165+
github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260506074423-13747423203f/go.mod h1:fDH7JI080OD5t6TGwjJx3mMX/g6W7t6Radlome6hze8=
166166
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
167167
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
168168
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

pkg/nsx/services/ipblocksinfo/ipblocksinfo.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,11 @@ func (s *IPBlocksInfoService) ResetPeriodicSync() {
9494
}
9595

9696
// mergeIPCidrs merges target CIDRs into source CIDRs if not already covered by source.
97-
// Only considers IPv4, assumes no overlaps and all CIDRs are valid.
98-
// Assume there were no duplicate cidr in target,
99-
// None of the elements in target will be a subset of another element
100-
// consider using radix tree or sort + binary search for large scale
97+
// Supports both IPv4 and IPv6 CIDRs. IPv4 and IPv6 CIDRs are treated as disjoint address
98+
// spaces and will never be considered to cover each other.
99+
// Assumes no overlaps within source and all CIDRs are valid.
100+
// Assumes there are no duplicate CIDRs in target, and no element in target is a subset of another.
101+
// Consider using a radix tree or sort + binary search for large-scale inputs.
101102
func (s *IPBlocksInfoService) mergeIPCidrs(source []string, target []string) []string {
102103
if len(source) == 0 {
103104
return target
@@ -370,6 +371,11 @@ func (s *IPBlocksInfoService) getIPBlockCIDRsByVPCConfig(vpcConfigList []v1alpha
370371
for _, externalIPBlock := range vpcConnectivityProfile.ExternalIpBlocks {
371372
externalIPBlockPaths.Insert(externalIPBlock)
372373
}
374+
// Ipv6Blocks are external-visibility IPv6 blocks; merge them into the external set so their
375+
// CIDRs/ranges appear in ExternalIPCIDRs/ExternalIPRanges without any CRD schema change.
376+
for _, ipv6Block := range vpcConnectivityProfile.Ipv6Blocks {
377+
externalIPBlockPaths.Insert(ipv6Block)
378+
}
373379
// save private_tgw_ip_blocks path in set for profile associated with default project
374380
if isDefault {
375381
for _, privateTgwIpBlocks := range vpcConnectivityProfile.PrivateTgwIpBlocks {

pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ var (
3030
ipBlocksPath2 = "/infra/ip-blocks/ipblock2"
3131
ipBlocksPath3 = "/infra/ip-blocks/ipblock3"
3232
ipBlocksPath4 = "/infra/ip-blocks/ipblock4"
33+
ipBlocksPath5 = "/infra/ip-blocks/ipblock5"
3334

3435
ipBlocksMap = map[string]string{
3536
ipBlocksPath1: "192.168.0.0/16",
3637
ipBlocksPath2: "10.172.0.0/16",
3738
ipBlocksPath3: "10.173.0.0/16",
3839
ipBlocksPath4: "2002::1234:abcd:ffff:c0a8:101/64",
40+
ipBlocksPath5: "2001:db8::/32",
3941
}
4042
vpcConnectivityProfilePath1 = "/orgs/default/projects/default/vpc-connectivity-profiles/vpc-connectivity-profile-1"
4143
vpcConnectivityProfilePath2 = "/orgs/default/projects/default/vpc-connectivity-profiles/vpc-connectivity-profile-2"
@@ -79,6 +81,7 @@ func fakeSearchResource(_ *common.Service, resourceTypeValue string, _ string, s
7981
Path: &vpcConnectivityProfilePath1,
8082
ExternalIpBlocks: []string{ipBlocksPath1},
8183
PrivateTgwIpBlocks: []string{ipBlocksPath2},
84+
Ipv6Blocks: []string{ipBlocksPath5},
8285
}
8386
vpcConnectivityProfile2 := &model.VpcConnectivityProfile{
8487
Path: &vpcConnectivityProfilePath2,
@@ -121,7 +124,8 @@ func TestIPBlocksInfoService_UpdateIPBlocksInfo(t *testing.T) {
121124
mockK8sClient.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
122125
actualUpdated, ok := obj.(*v1alpha1.IPBlocksInfo)
123126
assert.True(t, ok, "expected *v1alpha1.IPBlocksInfo when updating CR, got %T")
124-
assert.True(t, util.CompareArraysWithoutOrder(actualUpdated.ExternalIPCIDRs, []string{ipBlocksMap[ipBlocksPath4], ipBlocksMap[ipBlocksPath1]}))
127+
// IPv6 CIDRs from Ipv6Blocks (ipBlocksPath5) are merged into ExternalIPCIDRs alongside IPv4 externals
128+
assert.True(t, util.CompareArraysWithoutOrder(actualUpdated.ExternalIPCIDRs, []string{ipBlocksMap[ipBlocksPath4], ipBlocksMap[ipBlocksPath1], ipBlocksMap[ipBlocksPath5]}))
125129
assert.Equal(t, actualUpdated.PrivateTGWIPCIDRs, []string{ipBlocksMap[ipBlocksPath2]})
126130
return nil
127131
})
@@ -295,6 +299,54 @@ func TestIPBlocksInfoService_mergeIPCidrs(t *testing.T) {
295299
target: []string{"10.0.0.1", "192.168.1.0/-1"},
296300
expected: []string{"10.0.0.0/8"},
297301
},
302+
// IPv6 cases
303+
{
304+
name: "IPv6: target is subnet of source, should not add",
305+
source: []string{"2001:db8::/32"},
306+
target: []string{"2001:db8:1::/48"},
307+
expected: []string{"2001:db8::/32"},
308+
},
309+
{
310+
name: "IPv6: target is not covered by source, should add",
311+
source: []string{"2001:db8::/32"},
312+
target: []string{"2001:db9::/32"},
313+
expected: []string{"2001:db8::/32", "2001:db9::/32"},
314+
},
315+
{
316+
name: "IPv6: identical CIDRs in source and target, no duplicates",
317+
source: []string{"2001:db8::/32"},
318+
target: []string{"2001:db8::/32"},
319+
expected: []string{"2001:db8::/32"},
320+
},
321+
{
322+
name: "IPv6: empty source, all targets added",
323+
source: []string{},
324+
target: []string{"2001:db8::/32", "2001:db9::/32"},
325+
expected: []string{"2001:db8::/32", "2001:db9::/32"},
326+
},
327+
{
328+
name: "mixed IPv4 and IPv6: IPv6 source does not block IPv4 target",
329+
source: []string{"2001:db8::/32"},
330+
target: []string{"192.168.0.0/16"},
331+
expected: []string{"2001:db8::/32", "192.168.0.0/16"},
332+
},
333+
{
334+
name: "mixed IPv4 and IPv6: IPv4 source does not block IPv6 target",
335+
source: []string{"192.168.0.0/16"},
336+
target: []string{"2001:db8::/32"},
337+
expected: []string{"192.168.0.0/16", "2001:db8::/32"},
338+
},
339+
{
340+
name: "mixed IPv4 and IPv6: source has both families, correctly covers subnets",
341+
source: []string{"192.168.0.0/16", "2001:db8::/32"},
342+
target: []string{
343+
"192.168.1.0/24", // subset of IPv4 source
344+
"2001:db8:1::/48", // subset of IPv6 source
345+
"10.0.0.0/8", // new IPv4
346+
"2001:db9::/32", // new IPv6
347+
},
348+
expected: []string{"192.168.0.0/16", "2001:db8::/32", "10.0.0.0/8", "2001:db9::/32"},
349+
},
298350
}
299351

300352
for _, tt := range tests {
@@ -496,6 +548,76 @@ func TestIPBlocksInfoService_getCIDRsRangesFromStore(t *testing.T) {
496548
ipBlockStore.Delete("block2")
497549
ipBlockStore.Delete("block3")
498550
ipBlockStore.Delete("block4")
551+
552+
// Case: IPv6 CIDR in external IPBlock
553+
addBlock("ipv6-ext", nil, []string{"2001:db8::/32"}, nil)
554+
pathSet = sets.New[string]()
555+
pathSet.Insert("ipv6-ext")
556+
extCIDRs, privCIDRs, extRanges, privRanges, err = service.getCIDRsRangesFromStore(pathSet, sets.New[string](), ipBlockStore)
557+
assert.NoError(t, err)
558+
assert.Equal(t, []string{"2001:db8::/32"}, extCIDRs)
559+
assert.Empty(t, privCIDRs)
560+
assert.Empty(t, extRanges)
561+
assert.Empty(t, privRanges)
562+
ipBlockStore.Delete("ipv6-ext")
563+
564+
// Case: IPv6 range in external IPBlock
565+
addBlock("ipv6-range-ext", nil, nil, []model.IpPoolRange{
566+
{Start: stringPtr("2001:db8::1"), End: stringPtr("2001:db8::ff")},
567+
})
568+
pathSet = sets.New[string]()
569+
pathSet.Insert("ipv6-range-ext")
570+
extCIDRs, privCIDRs, extRanges, privRanges, err = service.getCIDRsRangesFromStore(pathSet, sets.New[string](), ipBlockStore)
571+
assert.NoError(t, err)
572+
assert.Empty(t, extCIDRs)
573+
assert.Empty(t, privCIDRs)
574+
assert.Equal(t, []v1alpha1.IPPoolRange{{Start: "2001:db8::1", End: "2001:db8::ff"}}, extRanges)
575+
assert.Empty(t, privRanges)
576+
ipBlockStore.Delete("ipv6-range-ext")
577+
578+
// Case: IPv6 CIDR in privateTGW IPBlock
579+
addBlock("ipv6-priv", nil, []string{"fd00::/48"}, nil)
580+
privSet = sets.New[string]()
581+
privSet.Insert("ipv6-priv")
582+
extCIDRs, privCIDRs, extRanges, privRanges, err = service.getCIDRsRangesFromStore(sets.New[string](), privSet, ipBlockStore)
583+
assert.NoError(t, err)
584+
assert.Empty(t, extCIDRs)
585+
assert.Equal(t, []string{"fd00::/48"}, privCIDRs)
586+
assert.Empty(t, extRanges)
587+
assert.Empty(t, privRanges)
588+
ipBlockStore.Delete("ipv6-priv")
589+
590+
// Case: mixed IPv4 and IPv6 CIDRs in the same IPBlock
591+
addBlock("mixed", nil, []string{"192.168.0.0/16", "2001:db8::/32"}, []model.IpPoolRange{
592+
{Start: stringPtr("10.0.0.1"), End: stringPtr("10.0.0.10")},
593+
{Start: stringPtr("2001:db8::1"), End: stringPtr("2001:db8::ff")},
594+
})
595+
pathSet = sets.New[string]()
596+
pathSet.Insert("mixed")
597+
extCIDRs, privCIDRs, extRanges, privRanges, err = service.getCIDRsRangesFromStore(pathSet, sets.New[string](), ipBlockStore)
598+
assert.NoError(t, err)
599+
assert.True(t, util.CompareArraysWithoutOrder([]string{"192.168.0.0/16", "2001:db8::/32"}, extCIDRs))
600+
assert.Empty(t, privCIDRs)
601+
assert.True(t, util.CompareArraysWithoutOrder([]v1alpha1.IPPoolRange{
602+
{Start: "10.0.0.1", End: "10.0.0.10"},
603+
{Start: "2001:db8::1", End: "2001:db8::ff"},
604+
}, extRanges))
605+
assert.Empty(t, privRanges)
606+
ipBlockStore.Delete("mixed")
607+
608+
// Case: Ipv6Blocks paths in externalIPBlockPaths (IPv6 from VpcConnectivityProfile.Ipv6Blocks)
609+
addBlock("ipv6-block", nil, []string{"2001:db8::/32"}, []model.IpPoolRange{
610+
{Start: stringPtr("2001:db8::1"), End: stringPtr("2001:db8::ff")},
611+
})
612+
ipv6AsExtSet := sets.New[string]()
613+
ipv6AsExtSet.Insert("ipv6-block")
614+
extCIDRs, privCIDRs, extRanges, privRanges, err = service.getCIDRsRangesFromStore(ipv6AsExtSet, sets.New[string](), ipBlockStore)
615+
assert.NoError(t, err)
616+
assert.Equal(t, []string{"2001:db8::/32"}, extCIDRs)
617+
assert.Empty(t, privCIDRs)
618+
assert.Equal(t, []v1alpha1.IPPoolRange{{Start: "2001:db8::1", End: "2001:db8::ff"}}, extRanges)
619+
assert.Empty(t, privRanges)
620+
ipBlockStore.Delete("ipv6-block")
499621
}
500622

501623
func TestIPBlocksInfoService_getSharedSubnetsCIDRs(t *testing.T) {
@@ -580,6 +702,56 @@ func TestIPBlocksInfoService_getSharedSubnetsCIDRs(t *testing.T) {
580702
assert.Empty(t, external)
581703
assert.Empty(t, private)
582704

705+
// Test: dual-stack subnet with both IPv4 and IPv6 addresses (Public access mode)
706+
dualStackSubnetPath := "/orgs/default/projects/default/vpcs/vpc1/vpc-subnets/dualstack-subnet"
707+
getSubnetPatch.Reset()
708+
getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) {
709+
public := "Public"
710+
return &model.VpcSubnet{
711+
Path: &dualStackSubnetPath,
712+
AccessMode: &public,
713+
IpAddresses: []string{"192.168.20.0/24", "2001:db8::/48"},
714+
}, nil
715+
})
716+
vpcConfigList = []v1alpha1.VPCNetworkConfiguration{
717+
{
718+
Spec: v1alpha1.VPCNetworkConfigurationSpec{
719+
Subnets: []v1alpha1.SharedSubnet{{
720+
Path: dualStackSubnetPath,
721+
}},
722+
},
723+
},
724+
}
725+
external, private, err = service.getSharedSubnetsCIDRs(vpcConfigList)
726+
assert.NoError(t, err)
727+
assert.ElementsMatch(t, []string{"192.168.20.0/24", "2001:db8::/48"}, external)
728+
assert.Empty(t, private)
729+
730+
// Test: dual-stack Private_TGW subnet with both IPv4 and IPv6 addresses
731+
dualStackPrivateTgwPath := "/orgs/default/projects/default/vpcs/vpc1/vpc-subnets/dualstack-private-tgw"
732+
getSubnetPatch.Reset()
733+
getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) {
734+
privateTgw := "Private_TGW"
735+
return &model.VpcSubnet{
736+
Path: &dualStackPrivateTgwPath,
737+
AccessMode: &privateTgw,
738+
IpAddresses: []string{"10.20.0.0/16", "fd00::/48"},
739+
}, nil
740+
})
741+
vpcConfigList = []v1alpha1.VPCNetworkConfiguration{
742+
{
743+
Spec: v1alpha1.VPCNetworkConfigurationSpec{
744+
Subnets: []v1alpha1.SharedSubnet{{
745+
Path: dualStackPrivateTgwPath,
746+
}},
747+
},
748+
},
749+
}
750+
external, private, err = service.getSharedSubnetsCIDRs(vpcConfigList)
751+
assert.NoError(t, err)
752+
assert.Empty(t, external)
753+
assert.ElementsMatch(t, []string{"10.20.0.0/16", "fd00::/48"}, private)
754+
583755
// Test: SearchResource returns error
584756
getSubnetPatch.Reset()
585757
getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) {

pkg/nsx/services/nsxserviceaccount/cluster.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ func (s *NSXServiceAccountService) RestoreRealizedNSXServiceAccount(ctx context.
193193
detail := true
194194
if piObj != nil {
195195
pi = piObj.(*mpmodel.PrincipalIdentity)
196-
certificate, _ = s.NSXClient.CertificatesClient.Get(*(pi.CertificateId), &detail)
196+
certificate, _ = s.NSXClient.CertificatesClient.Get(*(pi.CertificateId), &detail, nil)
197197
}
198198
// read Secret
199199
secretName := obj.Status.Secrets[0].Name

pkg/nsx/services/nsxserviceaccount/cluster_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func (c *fakeCertificatesClient) Fetchpeercertificatechain(tlsServiceEndpointPar
113113
return mpmodel.PeerCertificateChain{}, nil
114114
}
115115

116-
func (c *fakeCertificatesClient) Get(certIdParam string, detailsParam *bool) (mpmodel.Certificate, error) {
116+
func (c *fakeCertificatesClient) Get(certIdParam string, detailsParam *bool, fmtParam *string) (mpmodel.Certificate, error) {
117117
return mpmodel.Certificate{}, nil
118118
}
119119

@@ -125,7 +125,7 @@ func (c *fakeCertificatesClient) Importtrustedca(aliasParam string, trustObjectD
125125
return nil
126126
}
127127

128-
func (c *fakeCertificatesClient) List(cursorParam *string, detailsParam *bool, includedFieldsParam *string, nodeIdParam *string, pageSizeParam *int64, sortAscendingParam *bool, sortByParam *string, type_Param *string) (mpmodel.CertificateList, error) {
128+
func (c *fakeCertificatesClient) List(cursorParam *string, detailsParam *bool, fmtParam *string, includedFieldsParam *string, nodeIdParam *string, pageSizeParam *int64, sortAscendingParam *bool, sortByParam *string, type_Param *string) (mpmodel.CertificateList, error) {
129129
return mpmodel.CertificateList{}, nil
130130
}
131131

0 commit comments

Comments
 (0)