@@ -17,12 +17,74 @@ import (
1717
1818 apiv1 "github.com/kubeovn/kube-ovn/pkg/apis/kubeovn/v1"
1919 "github.com/kubeovn/kube-ovn/pkg/ipam"
20+ "github.com/kubeovn/kube-ovn/pkg/ovs"
2021 "github.com/kubeovn/kube-ovn/pkg/util"
2122 "github.com/kubeovn/kube-ovn/test/e2e/framework"
23+ "github.com/kubeovn/kube-ovn/test/e2e/framework/iproute"
2224)
2325
2426const ippoolUpdateTimeout = 2 * time .Minute
2527
28+ func expectPodIPFamily (pod * corev1.Pod , family string ) {
29+ ginkgo .GinkgoHelper ()
30+
31+ ipv4 , ipv6 := util .SplitStringIP (pod .Annotations [util .IPAddressAnnotation ])
32+ switch family {
33+ case apiv1 .ProtocolIPv4 :
34+ framework .ExpectNotEmpty (ipv4 )
35+ framework .ExpectEmpty (ipv6 )
36+ framework .ExpectConsistOf (util .PodIPs (* pod ), []string {ipv4 })
37+ case apiv1 .ProtocolIPv6 :
38+ framework .ExpectEmpty (ipv4 )
39+ framework .ExpectNotEmpty (ipv6 )
40+ framework .ExpectConsistOf (util .PodIPs (* pod ), []string {ipv6 })
41+ case apiv1 .ProtocolDual :
42+ framework .ExpectNotEmpty (ipv4 )
43+ framework .ExpectNotEmpty (ipv6 )
44+ framework .ExpectConsistOf (util .PodIPs (* pod ), []string {ipv4 , ipv6 })
45+ default :
46+ framework .Failf ("unexpected IP family %q" , family )
47+ }
48+ }
49+
50+ func expectDefaultIPCRFamily (f * framework.Framework , pod * corev1.Pod , subnetName , family string ) {
51+ ginkgo .GinkgoHelper ()
52+
53+ ipName := ovs .PodNameToPortName (pod .Name , pod .Namespace , util .OvnProvider )
54+ ipCR := f .IPClient ().Get (ipName )
55+ framework .ExpectEqual (ipCR .Spec .Subnet , subnetName )
56+ framework .ExpectEqual (ipCR .Spec .PodName , pod .Name )
57+ framework .ExpectEqual (ipCR .Spec .Namespace , pod .Namespace )
58+ framework .ExpectEqual (ipCR .Spec .NodeName , pod .Spec .NodeName )
59+ framework .ExpectEqual (ipCR .Spec .IPAddress , pod .Annotations [util .IPAddressAnnotation ])
60+ ipv4 , ipv6 := util .SplitStringIP (pod .Annotations [util .IPAddressAnnotation ])
61+ framework .ExpectEqual (ipCR .Spec .V4IPAddress , ipv4 )
62+ framework .ExpectEqual (ipCR .Spec .V6IPAddress , ipv6 )
63+
64+ switch family {
65+ case apiv1 .ProtocolIPv4 :
66+ framework .ExpectNotEmpty (ipCR .Spec .V4IPAddress )
67+ framework .ExpectEmpty (ipCR .Spec .V6IPAddress )
68+ case apiv1 .ProtocolIPv6 :
69+ framework .ExpectEmpty (ipCR .Spec .V4IPAddress )
70+ framework .ExpectNotEmpty (ipCR .Spec .V6IPAddress )
71+ case apiv1 .ProtocolDual :
72+ framework .ExpectNotEmpty (ipCR .Spec .V4IPAddress )
73+ framework .ExpectNotEmpty (ipCR .Spec .V6IPAddress )
74+ }
75+ }
76+
77+ func expectEth0IPs (pod * corev1.Pod , expectedIPs []string ) {
78+ ginkgo .GinkgoHelper ()
79+
80+ links , err := iproute .AddressShow ("eth0" , func (cmd ... string ) ([]byte , []byte , error ) {
81+ return framework .KubectlExec (pod .Namespace , pod .Name , cmd ... )
82+ })
83+ framework .ExpectNoError (err )
84+ framework .ExpectHaveLen (links , 1 )
85+ framework .ExpectConsistOf (links [0 ].NonLinkLocalIPs (), expectedIPs )
86+ }
87+
2688var _ = framework .Describe ("[group:ipam]" , func () {
2789 f := framework .NewDefaultFramework ("ipam" )
2890
@@ -114,6 +176,120 @@ var _ = framework.Describe("[group:ipam]", func() {
114176 framework .ExpectConsistOf (util .PodIPs (* pod ), strings .Split (ip , "," ))
115177 })
116178
179+ framework .ConformanceIt ("should allocate requested IP family for pod on dual-stack subnet" , func () {
180+ f .SkipVersionPriorTo (1 , 17 , "Per-pod IP family selection was introduced in v1.17" )
181+ if ! f .IsDual () {
182+ ginkgo .Skip ("This test requires a dual-stack cluster" )
183+ }
184+
185+ cmd := []string {"sleep" , "infinity" }
186+ cases := []struct {
187+ name string
188+ annotations map [string ]string
189+ family string
190+ }{
191+ {
192+ name : "default dual-stack allocation" ,
193+ family : apiv1 .ProtocolDual ,
194+ },
195+ {
196+ name : "IPv4-only allocation" ,
197+ annotations : map [string ]string {util .IPFamilyAnnotation : strings .ToLower (apiv1 .ProtocolIPv4 )},
198+ family : apiv1 .ProtocolIPv4 ,
199+ },
200+ {
201+ name : "IPv6-only allocation" ,
202+ annotations : map [string ]string {util .IPFamilyAnnotation : strings .ToLower (apiv1 .ProtocolIPv6 )},
203+ family : apiv1 .ProtocolIPv6 ,
204+ },
205+ }
206+
207+ for _ , tt := range cases {
208+ ginkgo .By ("Creating pod " + podName + " with " + tt .name )
209+ pod := framework .MakePrivilegedPod (namespaceName , podName , nil , tt .annotations , f .KubeOVNImage , cmd , nil )
210+ pod = podClient .CreateSync (pod )
211+
212+ ginkgo .By ("Validating pod annotations" )
213+ framework .ExpectHaveKeyWithValue (pod .Annotations , util .AllocatedAnnotation , "true" )
214+ framework .ExpectHaveKeyWithValue (pod .Annotations , util .CidrAnnotation , subnet .Spec .CIDRBlock )
215+ framework .ExpectHaveKeyWithValue (pod .Annotations , util .GatewayAnnotation , subnet .Spec .Gateway )
216+ framework .ExpectHaveKeyWithValue (pod .Annotations , util .LogicalSwitchAnnotation , subnet .Name )
217+ framework .ExpectHaveKeyWithValue (pod .Annotations , util .RoutedAnnotation , "true" )
218+ framework .ExpectMAC (pod .Annotations [util .MacAddressAnnotation ])
219+ expectPodIPFamily (pod , tt .family )
220+ expectDefaultIPCRFamily (f , pod , subnetName , tt .family )
221+ expectEth0IPs (pod , util .PodIPs (* pod ))
222+
223+ ginkgo .By ("Deleting pod " + podName )
224+ podClient .DeleteSync (podName )
225+ f .IPClient ().DeleteSync (ovs .PodNameToPortName (podName , namespaceName , util .OvnProvider ))
226+ }
227+ })
228+
229+ framework .ConformanceIt ("should allocate requested IP family from named dual-stack IPPool" , func () {
230+ f .SkipVersionPriorTo (1 , 17 , "Per-pod IP family selection was introduced in v1.17" )
231+ if ! f .IsDual () {
232+ ginkgo .Skip ("This test requires a dual-stack cluster" )
233+ }
234+
235+ ginkgo .By ("Creating IPPool " + ippoolName )
236+ poolIPs := strings .Split (framework .RandomIPs (cidr , "," , 2 ), "," )
237+ ippool := framework .MakeIPPool (ippoolName , subnetName , poolIPs , []string {namespaceName })
238+ ippool = ippoolClient .CreateSync (ippool )
239+ framework .ExpectNotEmpty (ippool .Status .V4AvailableIPRange )
240+ framework .ExpectNotEmpty (ippool .Status .V6AvailableIPRange )
241+
242+ cmd := []string {"sleep" , "infinity" }
243+ for _ , family := range []string {apiv1 .ProtocolIPv6 , apiv1 .ProtocolIPv4 } {
244+ ginkgo .By ("Creating pod " + podName + " with " + family + " IP family from IPPool " + ippoolName )
245+ annotations := map [string ]string {
246+ util .IPFamilyAnnotation : strings .ToLower (family ),
247+ util .IPPoolAnnotation : ippoolName ,
248+ }
249+ pod := framework .MakePrivilegedPod (namespaceName , podName , nil , annotations , f .KubeOVNImage , cmd , nil )
250+ pod = podClient .CreateSync (pod )
251+
252+ ginkgo .By ("Validating pod allocation" )
253+ framework .ExpectHaveKeyWithValue (pod .Annotations , util .AllocatedAnnotation , "true" )
254+ framework .ExpectHaveKeyWithValue (pod .Annotations , util .IPPoolAnnotation , ippoolName )
255+ framework .ExpectHaveKeyWithValue (pod .Annotations , util .CidrAnnotation , subnet .Spec .CIDRBlock )
256+ framework .ExpectHaveKeyWithValue (pod .Annotations , util .LogicalSwitchAnnotation , subnet .Name )
257+ expectPodIPFamily (pod , family )
258+ expectDefaultIPCRFamily (f , pod , subnetName , family )
259+ framework .ExpectContainElement (poolIPs , pod .Annotations [util .IPAddressAnnotation ])
260+ expectEth0IPs (pod , util .PodIPs (* pod ))
261+
262+ ginkgo .By ("Deleting pod " + podName )
263+ podClient .DeleteSync (podName )
264+ f .IPClient ().DeleteSync (ovs .PodNameToPortName (podName , namespaceName , util .OvnProvider ))
265+ }
266+ })
267+
268+ framework .ConformanceIt ("should reject requested IP family that does not match single-stack subnet" , func () {
269+ f .SkipVersionPriorTo (1 , 17 , "Per-pod IP family selection was introduced in v1.17" )
270+ if f .IsDual () {
271+ ginkgo .Skip ("This test requires a single-stack cluster" )
272+ }
273+
274+ requestedFamily := apiv1 .ProtocolIPv4
275+ if f .HasIPv4 () {
276+ requestedFamily = apiv1 .ProtocolIPv6
277+ }
278+
279+ ginkgo .By ("Creating pod " + podName + " with mismatched " + requestedFamily + " IP family on " + subnet .Spec .Protocol + " subnet" )
280+ annotations := map [string ]string {util .IPFamilyAnnotation : strings .ToLower (requestedFamily )}
281+ pod := framework .MakePod (namespaceName , podName , nil , annotations , "" , nil , nil )
282+ _ = podClient .Create (pod )
283+
284+ ginkgo .By ("Waiting for pod " + podName + " to have event indicating IP family mismatch" )
285+ events := f .EventClient ().WaitToHaveEvent (util .KindPod , podName , corev1 .EventTypeWarning , "AcquireAddressFailed" , "kube-ovn-controller" , "" )
286+ framework .ExpectContainSubstring (events [0 ].Message , fmt .Sprintf ("requested ip family %s does not match subnet %s protocol %s" , requestedFamily , subnetName , subnet .Spec .Protocol ))
287+
288+ ginkgo .By ("Validating pod " + podName + " is not allocated" )
289+ pod = podClient .GetPod (podName )
290+ framework .ExpectNotHaveKey (pod .Annotations , util .AllocatedAnnotation )
291+ })
292+
117293 framework .ConformanceIt ("should allocate static ip for pod with comma separated ippool" , func () {
118294 if f .IsDual () {
119295 ginkgo .Skip ("Comma separated ippool is not supported for dual stack" )
0 commit comments