@@ -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
501623func 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 ) {
0 commit comments