From b2bc2ed50b78fd5b5f6cd5885c7238572b26cb62 Mon Sep 17 00:00:00 2001 From: Wenqi Qiu Date: Fri, 15 May 2026 03:02:59 +0800 Subject: [PATCH 1/4] Add IPv6 support for IPBlocksInfo 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 --- .gitignore | 1 + go.mod | 4 +- go.sum | 8 +- pkg/nsx/services/ipblocksinfo/ipblocksinfo.go | 14 +- .../ipblocksinfo/ipblocksinfo_test.go | 174 +++++++++++++++++- pkg/nsx/services/nsxserviceaccount/cluster.go | 2 +- .../nsxserviceaccount/cluster_test.go | 4 +- 7 files changed, 193 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 80b6e9900..ed8a90625 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ go.work.sum .scannerwork/ .coverage/ .golangci-bin/ +vendor diff --git a/go.mod b/go.mod index 74b2cda00..605f04f50 100644 --- a/go.mod +++ b/go.mod @@ -34,8 +34,8 @@ require ( github.com/vmware/govmomi v0.53.1 github.com/vmware/vsphere-automation-sdk-go/lib v0.8.0 github.com/vmware/vsphere-automation-sdk-go/runtime v0.8.0 - github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260310075027-d32fca6a7b22 - github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260310075027-d32fca6a7b22 + github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260506074423-13747423203f + github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260506074423-13747423203f go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.1 golang.org/x/crypto v0.50.0 diff --git a/go.sum b/go.sum index 4395f9d5f..0e3ddb245 100644 --- a/go.sum +++ b/go.sum @@ -175,10 +175,10 @@ github.com/vmware/vsphere-automation-sdk-go/lib v0.8.0 h1:u1SXOTM6D4Ygb3jeidj2Rd github.com/vmware/vsphere-automation-sdk-go/lib v0.8.0/go.mod h1:8d5JTwjpM/Z03n/IZb0fwmXkJNWvWwuLXBqoakqYio4= github.com/vmware/vsphere-automation-sdk-go/runtime v0.8.0 h1:KnDIX9LY0nru7iMQTg0sy9vChhyorPo5OdASM2MaAcI= github.com/vmware/vsphere-automation-sdk-go/runtime v0.8.0/go.mod h1:DzLetYAmw1+vj7bqElRWEpuy40WYE/woL3alsymYa/c= -github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260310075027-d32fca6a7b22 h1:yDMJj+UG0u9aDdC0Q1byw8QEjfPd8gm7QKB2mo2oU1I= -github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260310075027-d32fca6a7b22/go.mod h1:C3JVOHRVLrGBQ8kTWAiGYlRz5UQC5qAcTdt3tvA+5P0= -github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260310075027-d32fca6a7b22 h1:SKbUc9p+LFUwtPvjk9WCwrjstN6NpewgPx4eWSIZq+k= -github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260310075027-d32fca6a7b22/go.mod h1:ugk9I4YM62SSAox57l5NAVBCRIkPQ1RNLb3URxyTADc= +github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260506074423-13747423203f h1:HvbZGTOUm9rJDG7ngNQSd5UC5ikiZI/M3cUai8u5+Jg= +github.com/vmware/vsphere-automation-sdk-go/services/nsxt v0.0.0-20260506074423-13747423203f/go.mod h1:C3JVOHRVLrGBQ8kTWAiGYlRz5UQC5qAcTdt3tvA+5P0= +github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260506074423-13747423203f h1:dzC9XLdl0fdqZB/K97m/NMY9o/voA2Qa4shHRnK900Q= +github.com/vmware/vsphere-automation-sdk-go/services/nsxt-mp v0.0.0-20260506074423-13747423203f/go.mod h1:fDH7JI080OD5t6TGwjJx3mMX/g6W7t6Radlome6hze8= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go b/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go index d2cdd71ee..5a276c88b 100644 --- a/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go +++ b/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go @@ -94,10 +94,11 @@ func (s *IPBlocksInfoService) ResetPeriodicSync() { } // mergeIPCidrs merges target CIDRs into source CIDRs if not already covered by source. -// Only considers IPv4, assumes no overlaps and all CIDRs are valid. -// Assume there were no duplicate cidr in target, -// None of the elements in target will be a subset of another element -// consider using radix tree or sort + binary search for large scale +// Supports both IPv4 and IPv6 CIDRs. IPv4 and IPv6 CIDRs are treated as disjoint address +// spaces and will never be considered to cover each other. +// Assumes no overlaps within source and all CIDRs are valid. +// Assumes there are no duplicate CIDRs in target, and no element in target is a subset of another. +// Consider using a radix tree or sort + binary search for large-scale inputs. func (s *IPBlocksInfoService) mergeIPCidrs(source []string, target []string) []string { if len(source) == 0 { return target @@ -370,6 +371,11 @@ func (s *IPBlocksInfoService) getIPBlockCIDRsByVPCConfig(vpcConfigList []v1alpha for _, externalIPBlock := range vpcConnectivityProfile.ExternalIpBlocks { externalIPBlockPaths.Insert(externalIPBlock) } + // Ipv6Blocks are external-visibility IPv6 blocks; merge them into the external set so their + // CIDRs/ranges appear in ExternalIPCIDRs/ExternalIPRanges without any CRD schema change. + for _, ipv6Block := range vpcConnectivityProfile.Ipv6Blocks { + externalIPBlockPaths.Insert(ipv6Block) + } // save private_tgw_ip_blocks path in set for profile associated with default project if isDefault { for _, privateTgwIpBlocks := range vpcConnectivityProfile.PrivateTgwIpBlocks { diff --git a/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go b/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go index bb7a5aedf..3a82c2d33 100644 --- a/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go +++ b/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go @@ -30,12 +30,14 @@ var ( ipBlocksPath2 = "/infra/ip-blocks/ipblock2" ipBlocksPath3 = "/infra/ip-blocks/ipblock3" ipBlocksPath4 = "/infra/ip-blocks/ipblock4" + ipBlocksPath5 = "/infra/ip-blocks/ipblock5" ipBlocksMap = map[string]string{ ipBlocksPath1: "192.168.0.0/16", ipBlocksPath2: "10.172.0.0/16", ipBlocksPath3: "10.173.0.0/16", ipBlocksPath4: "2002::1234:abcd:ffff:c0a8:101/64", + ipBlocksPath5: "2001:db8::/32", } vpcConnectivityProfilePath1 = "/orgs/default/projects/default/vpc-connectivity-profiles/vpc-connectivity-profile-1" vpcConnectivityProfilePath2 = "/orgs/default/projects/default/vpc-connectivity-profiles/vpc-connectivity-profile-2" @@ -79,6 +81,7 @@ func fakeSearchResource(_ *common.Service, resourceTypeValue string, _ string, s Path: &vpcConnectivityProfilePath1, ExternalIpBlocks: []string{ipBlocksPath1}, PrivateTgwIpBlocks: []string{ipBlocksPath2}, + Ipv6Blocks: []string{ipBlocksPath5}, } vpcConnectivityProfile2 := &model.VpcConnectivityProfile{ Path: &vpcConnectivityProfilePath2, @@ -121,7 +124,8 @@ func TestIPBlocksInfoService_UpdateIPBlocksInfo(t *testing.T) { mockK8sClient.EXPECT().Update(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { actualUpdated, ok := obj.(*v1alpha1.IPBlocksInfo) assert.True(t, ok, "expected *v1alpha1.IPBlocksInfo when updating CR, got %T") - assert.True(t, util.CompareArraysWithoutOrder(actualUpdated.ExternalIPCIDRs, []string{ipBlocksMap[ipBlocksPath4], ipBlocksMap[ipBlocksPath1]})) + // IPv6 CIDRs from Ipv6Blocks (ipBlocksPath5) are merged into ExternalIPCIDRs alongside IPv4 externals + assert.True(t, util.CompareArraysWithoutOrder(actualUpdated.ExternalIPCIDRs, []string{ipBlocksMap[ipBlocksPath4], ipBlocksMap[ipBlocksPath1], ipBlocksMap[ipBlocksPath5]})) assert.Equal(t, actualUpdated.PrivateTGWIPCIDRs, []string{ipBlocksMap[ipBlocksPath2]}) return nil }) @@ -295,6 +299,54 @@ func TestIPBlocksInfoService_mergeIPCidrs(t *testing.T) { target: []string{"10.0.0.1", "192.168.1.0/-1"}, expected: []string{"10.0.0.0/8"}, }, + // IPv6 cases + { + name: "IPv6: target is subnet of source, should not add", + source: []string{"2001:db8::/32"}, + target: []string{"2001:db8:1::/48"}, + expected: []string{"2001:db8::/32"}, + }, + { + name: "IPv6: target is not covered by source, should add", + source: []string{"2001:db8::/32"}, + target: []string{"2001:db9::/32"}, + expected: []string{"2001:db8::/32", "2001:db9::/32"}, + }, + { + name: "IPv6: identical CIDRs in source and target, no duplicates", + source: []string{"2001:db8::/32"}, + target: []string{"2001:db8::/32"}, + expected: []string{"2001:db8::/32"}, + }, + { + name: "IPv6: empty source, all targets added", + source: []string{}, + target: []string{"2001:db8::/32", "2001:db9::/32"}, + expected: []string{"2001:db8::/32", "2001:db9::/32"}, + }, + { + name: "mixed IPv4 and IPv6: IPv6 source does not block IPv4 target", + source: []string{"2001:db8::/32"}, + target: []string{"192.168.0.0/16"}, + expected: []string{"2001:db8::/32", "192.168.0.0/16"}, + }, + { + name: "mixed IPv4 and IPv6: IPv4 source does not block IPv6 target", + source: []string{"192.168.0.0/16"}, + target: []string{"2001:db8::/32"}, + expected: []string{"192.168.0.0/16", "2001:db8::/32"}, + }, + { + name: "mixed IPv4 and IPv6: source has both families, correctly covers subnets", + source: []string{"192.168.0.0/16", "2001:db8::/32"}, + target: []string{ + "192.168.1.0/24", // subset of IPv4 source + "2001:db8:1::/48", // subset of IPv6 source + "10.0.0.0/8", // new IPv4 + "2001:db9::/32", // new IPv6 + }, + expected: []string{"192.168.0.0/16", "2001:db8::/32", "10.0.0.0/8", "2001:db9::/32"}, + }, } for _, tt := range tests { @@ -496,6 +548,76 @@ func TestIPBlocksInfoService_getCIDRsRangesFromStore(t *testing.T) { ipBlockStore.Delete("block2") ipBlockStore.Delete("block3") ipBlockStore.Delete("block4") + + // Case: IPv6 CIDR in external IPBlock + addBlock("ipv6-ext", nil, []string{"2001:db8::/32"}, nil) + pathSet = sets.New[string]() + pathSet.Insert("ipv6-ext") + extCIDRs, privCIDRs, extRanges, privRanges, err = service.getCIDRsRangesFromStore(pathSet, sets.New[string](), ipBlockStore) + assert.NoError(t, err) + assert.Equal(t, []string{"2001:db8::/32"}, extCIDRs) + assert.Empty(t, privCIDRs) + assert.Empty(t, extRanges) + assert.Empty(t, privRanges) + ipBlockStore.Delete("ipv6-ext") + + // Case: IPv6 range in external IPBlock + addBlock("ipv6-range-ext", nil, nil, []model.IpPoolRange{ + {Start: stringPtr("2001:db8::1"), End: stringPtr("2001:db8::ff")}, + }) + pathSet = sets.New[string]() + pathSet.Insert("ipv6-range-ext") + extCIDRs, privCIDRs, extRanges, privRanges, err = service.getCIDRsRangesFromStore(pathSet, sets.New[string](), ipBlockStore) + assert.NoError(t, err) + assert.Empty(t, extCIDRs) + assert.Empty(t, privCIDRs) + assert.Equal(t, []v1alpha1.IPPoolRange{{Start: "2001:db8::1", End: "2001:db8::ff"}}, extRanges) + assert.Empty(t, privRanges) + ipBlockStore.Delete("ipv6-range-ext") + + // Case: IPv6 CIDR in privateTGW IPBlock + addBlock("ipv6-priv", nil, []string{"fd00::/48"}, nil) + privSet = sets.New[string]() + privSet.Insert("ipv6-priv") + extCIDRs, privCIDRs, extRanges, privRanges, err = service.getCIDRsRangesFromStore(sets.New[string](), privSet, ipBlockStore) + assert.NoError(t, err) + assert.Empty(t, extCIDRs) + assert.Equal(t, []string{"fd00::/48"}, privCIDRs) + assert.Empty(t, extRanges) + assert.Empty(t, privRanges) + ipBlockStore.Delete("ipv6-priv") + + // Case: mixed IPv4 and IPv6 CIDRs in the same IPBlock + addBlock("mixed", nil, []string{"192.168.0.0/16", "2001:db8::/32"}, []model.IpPoolRange{ + {Start: stringPtr("10.0.0.1"), End: stringPtr("10.0.0.10")}, + {Start: stringPtr("2001:db8::1"), End: stringPtr("2001:db8::ff")}, + }) + pathSet = sets.New[string]() + pathSet.Insert("mixed") + extCIDRs, privCIDRs, extRanges, privRanges, err = service.getCIDRsRangesFromStore(pathSet, sets.New[string](), ipBlockStore) + assert.NoError(t, err) + assert.True(t, util.CompareArraysWithoutOrder([]string{"192.168.0.0/16", "2001:db8::/32"}, extCIDRs)) + assert.Empty(t, privCIDRs) + assert.True(t, util.CompareArraysWithoutOrder([]v1alpha1.IPPoolRange{ + {Start: "10.0.0.1", End: "10.0.0.10"}, + {Start: "2001:db8::1", End: "2001:db8::ff"}, + }, extRanges)) + assert.Empty(t, privRanges) + ipBlockStore.Delete("mixed") + + // Case: Ipv6Blocks paths in externalIPBlockPaths (IPv6 from VpcConnectivityProfile.Ipv6Blocks) + addBlock("ipv6-block", nil, []string{"2001:db8::/32"}, []model.IpPoolRange{ + {Start: stringPtr("2001:db8::1"), End: stringPtr("2001:db8::ff")}, + }) + ipv6AsExtSet := sets.New[string]() + ipv6AsExtSet.Insert("ipv6-block") + extCIDRs, privCIDRs, extRanges, privRanges, err = service.getCIDRsRangesFromStore(ipv6AsExtSet, sets.New[string](), ipBlockStore) + assert.NoError(t, err) + assert.Equal(t, []string{"2001:db8::/32"}, extCIDRs) + assert.Empty(t, privCIDRs) + assert.Equal(t, []v1alpha1.IPPoolRange{{Start: "2001:db8::1", End: "2001:db8::ff"}}, extRanges) + assert.Empty(t, privRanges) + ipBlockStore.Delete("ipv6-block") } func TestIPBlocksInfoService_getSharedSubnetsCIDRs(t *testing.T) { @@ -580,6 +702,56 @@ func TestIPBlocksInfoService_getSharedSubnetsCIDRs(t *testing.T) { assert.Empty(t, external) assert.Empty(t, private) + // Test: dual-stack subnet with both IPv4 and IPv6 addresses (Public access mode) + dualStackSubnetPath := "/orgs/default/projects/default/vpcs/vpc1/vpc-subnets/dualstack-subnet" + getSubnetPatch.Reset() + getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) { + public := "Public" + return &model.VpcSubnet{ + Path: &dualStackSubnetPath, + AccessMode: &public, + IpAddresses: []string{"192.168.20.0/24", "2001:db8::/48"}, + }, nil + }) + vpcConfigList = []v1alpha1.VPCNetworkConfiguration{ + { + Spec: v1alpha1.VPCNetworkConfigurationSpec{ + Subnets: []v1alpha1.SharedSubnet{{ + Path: dualStackSubnetPath, + }}, + }, + }, + } + external, private, err = service.getSharedSubnetsCIDRs(vpcConfigList) + assert.NoError(t, err) + assert.ElementsMatch(t, []string{"192.168.20.0/24", "2001:db8::/48"}, external) + assert.Empty(t, private) + + // Test: dual-stack Private_TGW subnet with both IPv4 and IPv6 addresses + dualStackPrivateTgwPath := "/orgs/default/projects/default/vpcs/vpc1/vpc-subnets/dualstack-private-tgw" + getSubnetPatch.Reset() + getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) { + privateTgw := "Private_TGW" + return &model.VpcSubnet{ + Path: &dualStackPrivateTgwPath, + AccessMode: &privateTgw, + IpAddresses: []string{"10.20.0.0/16", "fd00::/48"}, + }, nil + }) + vpcConfigList = []v1alpha1.VPCNetworkConfiguration{ + { + Spec: v1alpha1.VPCNetworkConfigurationSpec{ + Subnets: []v1alpha1.SharedSubnet{{ + Path: dualStackPrivateTgwPath, + }}, + }, + }, + } + external, private, err = service.getSharedSubnetsCIDRs(vpcConfigList) + assert.NoError(t, err) + assert.Empty(t, external) + assert.ElementsMatch(t, []string{"10.20.0.0/16", "fd00::/48"}, private) + // Test: SearchResource returns error getSubnetPatch.Reset() getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) { diff --git a/pkg/nsx/services/nsxserviceaccount/cluster.go b/pkg/nsx/services/nsxserviceaccount/cluster.go index 3f3dea638..747c57a82 100644 --- a/pkg/nsx/services/nsxserviceaccount/cluster.go +++ b/pkg/nsx/services/nsxserviceaccount/cluster.go @@ -193,7 +193,7 @@ func (s *NSXServiceAccountService) RestoreRealizedNSXServiceAccount(ctx context. detail := true if piObj != nil { pi = piObj.(*mpmodel.PrincipalIdentity) - certificate, _ = s.NSXClient.CertificatesClient.Get(*(pi.CertificateId), &detail) + certificate, _ = s.NSXClient.CertificatesClient.Get(*(pi.CertificateId), &detail, nil) } // read Secret secretName := obj.Status.Secrets[0].Name diff --git a/pkg/nsx/services/nsxserviceaccount/cluster_test.go b/pkg/nsx/services/nsxserviceaccount/cluster_test.go index 06aa79391..843a2b394 100644 --- a/pkg/nsx/services/nsxserviceaccount/cluster_test.go +++ b/pkg/nsx/services/nsxserviceaccount/cluster_test.go @@ -113,7 +113,7 @@ func (c *fakeCertificatesClient) Fetchpeercertificatechain(tlsServiceEndpointPar return mpmodel.PeerCertificateChain{}, nil } -func (c *fakeCertificatesClient) Get(certIdParam string, detailsParam *bool) (mpmodel.Certificate, error) { +func (c *fakeCertificatesClient) Get(certIdParam string, detailsParam *bool, fmtParam *string) (mpmodel.Certificate, error) { return mpmodel.Certificate{}, nil } @@ -125,7 +125,7 @@ func (c *fakeCertificatesClient) Importtrustedca(aliasParam string, trustObjectD return nil } -func (c *fakeCertificatesClient) List(cursorParam *string, detailsParam *bool, includedFieldsParam *string, nodeIdParam *string, pageSizeParam *int64, sortAscendingParam *bool, sortByParam *string, type_Param *string) (mpmodel.CertificateList, error) { +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) { return mpmodel.CertificateList{}, nil } From a590014c53021bb4a7d4e5cc9109962dee4be106 Mon Sep 17 00:00:00 2001 From: Wenqi Qiu Date: Sun, 17 May 2026 01:19:20 +0800 Subject: [PATCH 2/4] update Signed-off-by: Wenqi Qiu --- pkg/nsx/services/ipblocksinfo/ipblocksinfo.go | 28 +++++++--- .../ipblocksinfo/ipblocksinfo_test.go | 56 ++++++++++++++++++- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go b/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go index 5a276c88b..9ae4a7516 100644 --- a/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go +++ b/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go @@ -259,14 +259,26 @@ func (s *IPBlocksInfoService) getSharedSubnetsCIDRs(vpcConfigList []v1alpha1.VPC continue } - switch *subnet.AccessMode { - case model.VpcSubnet_ACCESS_MODE_PUBLIC: - externalIPCIDRs = append(externalIPCIDRs, subnet.IpAddresses...) - - case model.VpcSubnet_ACCESS_MODE_PRIVATE_TGW: - project := fmt.Sprintf("/orgs/%s/projects/%s", vpcInfo.OrgID, vpcInfo.ProjectID) - if project == s.defaultProject { - privateTGWIPCIDRs = append(privateTGWIPCIDRs, subnet.IpAddresses...) + for _, cidr := range subnet.IpAddresses { + ip, _, parseErr := net.ParseCIDR(cidr) + if parseErr != nil { + log.Warn("failed to parse subnet CIDR", "cidr", cidr, "err", parseErr) + continue + } + if ip.To4() == nil { + // IPv6 CIDRs are always public with no access mode + externalIPCIDRs = append(externalIPCIDRs, cidr) + continue + } + // IPv4: apply access mode check + switch *subnet.AccessMode { + case model.VpcSubnet_ACCESS_MODE_PUBLIC: + externalIPCIDRs = append(externalIPCIDRs, cidr) + case model.VpcSubnet_ACCESS_MODE_PRIVATE_TGW: + project := fmt.Sprintf("/orgs/%s/projects/%s", vpcInfo.OrgID, vpcInfo.ProjectID) + if project == s.defaultProject { + privateTGWIPCIDRs = append(privateTGWIPCIDRs, cidr) + } } } } diff --git a/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go b/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go index 3a82c2d33..392aa76f9 100644 --- a/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go +++ b/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go @@ -727,7 +727,7 @@ func TestIPBlocksInfoService_getSharedSubnetsCIDRs(t *testing.T) { assert.ElementsMatch(t, []string{"192.168.20.0/24", "2001:db8::/48"}, external) assert.Empty(t, private) - // Test: dual-stack Private_TGW subnet with both IPv4 and IPv6 addresses + // Test: dual-stack Private_TGW subnet – IPv6 always goes to external, only IPv4 respects access mode dualStackPrivateTgwPath := "/orgs/default/projects/default/vpcs/vpc1/vpc-subnets/dualstack-private-tgw" getSubnetPatch.Reset() getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) { @@ -749,8 +749,58 @@ func TestIPBlocksInfoService_getSharedSubnetsCIDRs(t *testing.T) { } external, private, err = service.getSharedSubnetsCIDRs(vpcConfigList) assert.NoError(t, err) - assert.Empty(t, external) - assert.ElementsMatch(t, []string{"10.20.0.0/16", "fd00::/48"}, private) + assert.ElementsMatch(t, []string{"fd00::/48"}, external) + assert.ElementsMatch(t, []string{"10.20.0.0/16"}, private) + + // Test: IPv6-only Private_TGW subnet – all CIDRs go to external (no access mode applied to IPv6) + ipv6OnlyPrivateTgwPath := "/orgs/default/projects/default/vpcs/vpc1/vpc-subnets/ipv6-only-private-tgw" + getSubnetPatch.Reset() + getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) { + privateTgw := "Private_TGW" + return &model.VpcSubnet{ + Path: &ipv6OnlyPrivateTgwPath, + AccessMode: &privateTgw, + IpAddresses: []string{"2001:db8::/32", "fd00::/48"}, + }, nil + }) + vpcConfigList = []v1alpha1.VPCNetworkConfiguration{ + { + Spec: v1alpha1.VPCNetworkConfigurationSpec{ + Subnets: []v1alpha1.SharedSubnet{{ + Path: ipv6OnlyPrivateTgwPath, + }}, + }, + }, + } + external, private, err = service.getSharedSubnetsCIDRs(vpcConfigList) + assert.NoError(t, err) + assert.ElementsMatch(t, []string{"2001:db8::/32", "fd00::/48"}, external) + assert.Empty(t, private) + + // Test: subnet with an invalid CIDR entry – invalid entries are skipped, valid ones processed + invalidCIDRSubnetPath := "/orgs/default/projects/default/vpcs/vpc1/vpc-subnets/invalid-cidr-subnet" + getSubnetPatch.Reset() + getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) { + public := "Public" + return &model.VpcSubnet{ + Path: &invalidCIDRSubnetPath, + AccessMode: &public, + IpAddresses: []string{"not-a-cidr", "10.30.0.0/24", "2001:db8:1::/48"}, + }, nil + }) + vpcConfigList = []v1alpha1.VPCNetworkConfiguration{ + { + Spec: v1alpha1.VPCNetworkConfigurationSpec{ + Subnets: []v1alpha1.SharedSubnet{{ + Path: invalidCIDRSubnetPath, + }}, + }, + }, + } + external, private, err = service.getSharedSubnetsCIDRs(vpcConfigList) + assert.NoError(t, err) + assert.ElementsMatch(t, []string{"10.30.0.0/24", "2001:db8:1::/48"}, external) + assert.Empty(t, private) // Test: SearchResource returns error getSubnetPatch.Reset() From a732e1d08f14e79605d904daf6798cb4bc02bbd3 Mon Sep 17 00:00:00 2001 From: Wenqi Qiu Date: Wed, 20 May 2026 15:16:47 +0800 Subject: [PATCH 3/4] update Signed-off-by: Wenqi Qiu --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index ed8a90625..80b6e9900 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ go.work.sum .scannerwork/ .coverage/ .golangci-bin/ -vendor From 2a4b2db8aff05845bac716ca47b2d2fc12584f4a Mon Sep 17 00:00:00 2001 From: Wenqi Qiu Date: Thu, 21 May 2026 19:26:57 +0800 Subject: [PATCH 4/4] update Signed-off-by: Wenqi Qiu --- pkg/nsx/services/ipblocksinfo/ipblocksinfo.go | 6 ++++- .../ipblocksinfo/ipblocksinfo_test.go | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go b/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go index 9ae4a7516..ccfefc517 100644 --- a/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go +++ b/pkg/nsx/services/ipblocksinfo/ipblocksinfo.go @@ -271,7 +271,11 @@ func (s *IPBlocksInfoService) getSharedSubnetsCIDRs(vpcConfigList []v1alpha1.VPC continue } // IPv4: apply access mode check - switch *subnet.AccessMode { + accessMode := model.VpcSubnet_ACCESS_MODE_PUBLIC + if subnet.AccessMode != nil { + accessMode = *subnet.AccessMode + } + switch accessMode { case model.VpcSubnet_ACCESS_MODE_PUBLIC: externalIPCIDRs = append(externalIPCIDRs, cidr) case model.VpcSubnet_ACCESS_MODE_PRIVATE_TGW: diff --git a/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go b/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go index 392aa76f9..fc34379ea 100644 --- a/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go +++ b/pkg/nsx/services/ipblocksinfo/ipblocksinfo_test.go @@ -802,6 +802,29 @@ func TestIPBlocksInfoService_getSharedSubnetsCIDRs(t *testing.T) { assert.ElementsMatch(t, []string{"10.30.0.0/24", "2001:db8:1::/48"}, external) assert.Empty(t, private) + // Test: nil AccessMode defaults to Public for IPv4 (consistent with MapNSXSubnetToSubnetCR) + nilAccessModeSubnetPath := "/orgs/default/projects/default/vpcs/vpc1/vpc-subnets/nil-access-mode" + getSubnetPatch.Reset() + getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) { + return &model.VpcSubnet{ + Path: &nilAccessModeSubnetPath, + IpAddresses: []string{"172.16.0.0/16"}, + }, nil + }) + vpcConfigList = []v1alpha1.VPCNetworkConfiguration{ + { + Spec: v1alpha1.VPCNetworkConfigurationSpec{ + Subnets: []v1alpha1.SharedSubnet{{ + Path: nilAccessModeSubnetPath, + }}, + }, + }, + } + external, private, err = service.getSharedSubnetsCIDRs(vpcConfigList) + assert.NoError(t, err) + assert.ElementsMatch(t, []string{"172.16.0.0/16"}, external) + assert.Empty(t, private) + // Test: SearchResource returns error getSubnetPatch.Reset() getSubnetPatch = gomonkey.ApplyMethod(reflect.TypeOf(service.subnetService), "GetNSXSubnetFromCacheOrAPI", func(_ *subnet.SubnetService, associate string, forceAPI bool) (*model.VpcSubnet, error) {