@@ -2,16 +2,20 @@ package e2e
22
33import (
44 "context"
5+ "encoding/json"
56 "fmt"
67 "net"
78 "strings"
89
910 "time"
1011
12+ nadapi "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1"
1113 "github.com/onsi/ginkgo/v2"
1214 "github.com/onsi/gomega"
1315
1416 corev1 "k8s.io/api/core/v1"
17+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+ clientset "k8s.io/client-go/kubernetes"
1519 utilnet "k8s.io/utils/net"
1620
1721 "k8s.io/kubernetes/test/e2e/framework"
@@ -222,3 +226,246 @@ var _ = ginkgo.Describe("BGP: Pod to external server when default podNetwork is
222226 })
223227 })
224228})
229+
230+ var _ = ginkgo .Describe ("BGP: Pod to external server when CUDN Layer3 Network is advertised" , func () {
231+ const (
232+ serverContainerName = "bgpserver"
233+ routerContainerName = "frr"
234+ echoClientPodName = "echo-client-pod"
235+ echoServerPodPortMin = 9800
236+ echoServerPodPortMax = 9899
237+ primaryNetworkName = "kind"
238+ bgpExternalNetworkName = "bgpnet"
239+ testCudnName = "bgp-udn-layer3-network"
240+ testRAName = "udn-layer3-ra"
241+ )
242+ var serverContainerIPs []string
243+ var frrContainerIPv4 , frrContainerIPv6 string
244+ var nodes * corev1.NodeList
245+ var cs clientset.Interface
246+ f := wrappedTestFramework ("pod2external-route-advertisements" )
247+ // disable automatic namespace creation, we need to add the required UDN label
248+ f .SkipNamespaceCreation = true
249+
250+ ginkgo .BeforeEach (func () {
251+ cs = f .ClientSet
252+
253+ var err error
254+ namespace , err := f .CreateNamespace (context .TODO (), f .BaseName , map [string ]string {
255+ "e2e-framework" : f .BaseName ,
256+ RequiredUDNNamespaceLabel : "" ,
257+ })
258+ f .Namespace = namespace
259+ gomega .Expect (err ).NotTo (gomega .HaveOccurred ())
260+
261+ serverContainerIPs = []string {}
262+
263+ bgpServerIPv4 , bgpServerIPv6 := getContainerAddressesForNetwork (serverContainerName , bgpExternalNetworkName )
264+ if isIPv4Supported () {
265+ serverContainerIPs = append (serverContainerIPs , bgpServerIPv4 )
266+ }
267+
268+ if isIPv6Supported () {
269+ serverContainerIPs = append (serverContainerIPs , bgpServerIPv6 )
270+ }
271+ framework .Logf ("The external server IPs are: %+v" , serverContainerIPs )
272+
273+ frrContainerIPv4 , frrContainerIPv6 = getContainerAddressesForNetwork (routerContainerName , primaryNetworkName )
274+ framework .Logf ("The frr router container IPs are: %s/%s" , frrContainerIPv4 , frrContainerIPv6 )
275+ })
276+
277+ ginkgo .When ("a client ovnk pod targeting an external server is created" , func () {
278+
279+ var clientPod * corev1.Pod
280+ var err error
281+
282+ ginkgo .BeforeEach (func () {
283+ if ! IsGatewayModeLocal () {
284+ const upstreamIssue = "https://github.com/ovn-kubernetes/ovn-kubernetes/issues/5134"
285+ e2eskipper .Skipf (
286+ "The import of routes into UDNs is broken on shared gateway mode. Upstream issue: %s" , upstreamIssue ,
287+ )
288+ }
289+ ginkgo .By ("Selecting 3 schedulable nodes" )
290+ nodes , err = e2enode .GetBoundedReadySchedulableNodes (context .TODO (), f .ClientSet , 3 )
291+ gomega .Expect (err ).NotTo (gomega .HaveOccurred ())
292+ gomega .Expect (len (nodes .Items )).To (gomega .BeNumerically (">" , 2 ))
293+
294+ ginkgo .By ("create layer3 ClusterUserDefinedNetwork" )
295+ cudnManifest := generateBGPCUDNManifest (testCudnName , f .Namespace .Name )
296+ cleanup , err := createManifest ("" , cudnManifest )
297+ gomega .Expect (err ).NotTo (gomega .HaveOccurred ())
298+ gomega .Eventually (clusterUserDefinedNetworkReadyFunc (f .DynamicClient , testCudnName ), 5 * time .Second , time .Second ).Should (gomega .Succeed ())
299+
300+ conditionsJSON , err := e2ekubectl .RunKubectl ("" , "get" , "clusteruserdefinednetwork" , testCudnName , "-o" , "jsonpath={.status.conditions}" )
301+ gomega .Expect (err ).NotTo (gomega .HaveOccurred ())
302+ var actualConditions []metav1.Condition
303+ gomega .Expect (json .Unmarshal ([]byte (conditionsJSON ), & actualConditions )).To (gomega .Succeed ())
304+ ginkgo .DeferCleanup (func () {
305+ cleanup ()
306+ ginkgo .By (fmt .Sprintf ("delete pods in %s namespace to unblock CUDN CR & associate NAD deletion" , f .Namespace .Name ))
307+ gomega .Expect (cs .CoreV1 ().Pods (f .Namespace .Name ).DeleteCollection (context .Background (), metav1.DeleteOptions {}, metav1.ListOptions {})).To (gomega .Succeed ())
308+ _ , err := e2ekubectl .RunKubectl ("" , "delete" , "clusteruserdefinednetwork" , testCudnName , "--wait" , fmt .Sprintf ("--timeout=%ds" , 120 ))
309+ gomega .Expect (err ).NotTo (gomega .HaveOccurred ())
310+ })
311+
312+ ginkgo .By ("Creating client pod on the udn namespace" )
313+ podConfig := * podConfig (echoClientPodName )
314+ podConfig .namespace = f .Namespace .Name
315+ clientPod = runUDNPod (cs , f .Namespace .Name , podConfig , nil )
316+
317+ ginkgo .By ("asserting the pod UDN interface on the network-status annotation" )
318+ udnNetStat , err := podNetworkStatus (clientPod , func (status nadapi.NetworkStatus ) bool {
319+ return status .Default
320+ })
321+ gomega .Expect (err ).NotTo (gomega .HaveOccurred ())
322+ const (
323+ expectedDefaultNetStatusLen = 1
324+ ovnUDNInterface = "ovn-udn1"
325+ )
326+ gomega .Expect (udnNetStat ).To (gomega .HaveLen (expectedDefaultNetStatusLen ))
327+ gomega .Expect (udnNetStat [0 ].Interface ).To (gomega .Equal (ovnUDNInterface ))
328+
329+ gomega .Expect (len (serverContainerIPs )).To (gomega .BeNumerically (">" , 0 ))
330+ })
331+ ginkgo .AfterEach (func () {
332+ e2ekubectl .RunKubectlOrDie ("" , "delete" , "ra" , testRAName , "--ignore-not-found=true" )
333+ })
334+ // ----------------- ------------------ ---------------------
335+ // | | 172.26.0.0/16 | | 172.18.0.0/16 | ovn-control-plane |
336+ // | external |<------------- | FRR router |<------ KIND cluster -- ---------------------
337+ // | server | | | | ovn-worker | (client UDN pod advertised
338+ // ----------------- ------------------ --------------------- using RouteAdvertisements
339+ // | ovn-worker2 | from default pod network)
340+ // ---------------------
341+ // The client pod inside the KIND cluster on the default network exposed using default network Router
342+ // Advertisement will curl the external server container sitting outside the cluster via a FRR router
343+ // This test ensures the north-south connectivity is happening through podIP
344+ ginkgo .It ("tests are run towards the external agnhost echo server" , func () {
345+ ginkgo .By ("routes from external bgp server are imported by nodes in the cluster" )
346+ externalServerV4CIDR , externalServerV6CIDR := getContainerNetworkCIDRs (bgpExternalNetworkName )
347+ framework .Logf ("the network cidrs to be imported are v4=%s and v6=%s" , externalServerV4CIDR , externalServerV6CIDR )
348+ for _ , node := range nodes .Items {
349+ ipVer := ""
350+ cmd := []string {containerRuntime , "exec" , node .Name }
351+ bgpRouteCommand := strings .Split (fmt .Sprintf ("ip%s route show %s" , ipVer , externalServerV4CIDR ), " " )
352+ cmd = append (cmd , bgpRouteCommand ... )
353+ framework .Logf ("Checking for server's route in node %s" , node .Name )
354+ gomega .Eventually (func () bool {
355+ routes , err := runCommand (cmd ... )
356+ framework .ExpectNoError (err , "failed to get BGP routes from node" )
357+ framework .Logf ("Routes in node %s" , routes )
358+ return strings .Contains (routes , frrContainerIPv4 )
359+ }, 30 * time .Second ).Should (gomega .BeTrue ())
360+ if isDualStackCluster (nodes ) {
361+ ipVer = " -6"
362+ nodeIPv6LLA , err := GetNodeIPv6LinkLocalAddressForEth0 (routerContainerName )
363+ gomega .Expect (err ).NotTo (gomega .HaveOccurred ())
364+ cmd := []string {containerRuntime , "exec" , node .Name }
365+ bgpRouteCommand := strings .Split (fmt .Sprintf ("ip%s route show %s" , ipVer , externalServerV6CIDR ), " " )
366+ cmd = append (cmd , bgpRouteCommand ... )
367+ framework .Logf ("Checking for server's route in node %s" , node .Name )
368+ gomega .Eventually (func () bool {
369+ routes , err := runCommand (cmd ... )
370+ framework .ExpectNoError (err , "failed to get BGP routes from node" )
371+ framework .Logf ("Routes in node %s" , routes )
372+ return strings .Contains (routes , nodeIPv6LLA )
373+ }, 30 * time .Second ).Should (gomega .BeTrue ())
374+ }
375+ }
376+
377+ ginkgo .By ("routes to the CUDN network are advertised to external frr router" )
378+ // Get the first element in the advertisements array (assuming you want to check the first one)
379+ ginkgo .By ("create route advertisement matching CUDN Network" )
380+ raManifest := generateRAManifest (testRAName )
381+ cleanup , err := createManifest (f .Namespace .Name , raManifest )
382+ ginkgo .DeferCleanup (cleanup )
383+ gomega .Expect (err ).NotTo (gomega .HaveOccurred ())
384+
385+ ginkgo .By ("ensure route advertisement matching CUDN was created successfully" )
386+ gomega .Eventually (func () string {
387+ podNetworkValue , err := e2ekubectl .RunKubectl ("" , "get" , "ra" , testRAName , "--template={{index .spec.advertisements 0}}" )
388+ if err != nil {
389+ return ""
390+ }
391+ return podNetworkValue
392+ }, 5 * time .Second , time .Second ).Should (gomega .Equal ("PodNetwork" ))
393+
394+ gomega .Eventually (func () string {
395+ reason , err := e2ekubectl .RunKubectl ("" , "get" , "ra" , testRAName , "-o" , "jsonpath={.status.conditions[?(@.type=='Accepted')].reason}" )
396+ if err != nil {
397+ return ""
398+ }
399+ return reason
400+ }, 30 * time .Second , time .Second ).Should (gomega .Equal ("Accepted" ))
401+
402+ ginkgo .By ("queries to the external server are not SNATed (uses UDN podIP)" )
403+ podIP , err := podIPsForUserDefinedPrimaryNetwork (cs , f .Namespace .Name , clientPod .Name , namespacedName (f .Namespace .Name , testCudnName ), 0 )
404+ framework .ExpectNoError (err , fmt .Sprintf ("Getting podIPs for pod %s failed: %v" , clientPod .Name , err ))
405+ framework .Logf ("Client pod IP address=%s" , podIP )
406+ for _ , serverContainerIP := range serverContainerIPs {
407+ ginkgo .By (fmt .Sprintf ("Sending request to node IP %s " +
408+ "and expecting to receive the same payload" , serverContainerIP ))
409+ cmd := fmt .Sprintf ("curl --max-time 10 -g -q -s http://%s/clientip" ,
410+ net .JoinHostPort (serverContainerIP , "8080" ),
411+ )
412+ framework .Logf ("Testing pod to external traffic with command %q" , cmd )
413+ stdout , err := e2epodoutput .RunHostCmdWithRetries (
414+ clientPod .Namespace ,
415+ clientPod .Name ,
416+ cmd ,
417+ framework .Poll ,
418+ 60 * time .Second )
419+ framework .ExpectNoError (err , fmt .Sprintf ("Testing pod to external traffic failed: %v" , err ))
420+ if isIPv6Supported () && utilnet .IsIPv6String (serverContainerIP ) {
421+ podIP , err := podIPsForUserDefinedPrimaryNetwork (cs , f .Namespace .Name , clientPod .Name , namespacedName (f .Namespace .Name , testCudnName ), 1 )
422+ framework .ExpectNoError (err , fmt .Sprintf ("Getting podIPs for pod %s failed: %v" , clientPod .Name , err ))
423+ // For IPv6 addresses, need to handle the brackets in the output
424+ outputIP := strings .TrimPrefix (strings .Split (stdout , "]:" )[0 ], "[" )
425+ gomega .Expect (outputIP ).To (gomega .Equal (podIP ),
426+ fmt .Sprintf ("Testing pod %s to external traffic failed while analysing output %v" , echoClientPodName , stdout ))
427+ } else {
428+ // Original IPv4 handling
429+ gomega .Expect (strings .Split (stdout , ":" )[0 ]).To (gomega .Equal (podIP ),
430+ fmt .Sprintf ("Testing pod %s to external traffic failed while analysing output %v" , echoClientPodName , stdout ))
431+ }
432+ }
433+ })
434+ })
435+ })
436+
437+ func generateBGPCUDNManifest (testCudnName string , targetNamespaces ... string ) string {
438+ targetNs := strings .Join (targetNamespaces , "," )
439+ return `
440+ apiVersion: k8s.ovn.org/v1
441+ kind: ClusterUserDefinedNetwork
442+ metadata:
443+ name: ` + testCudnName + `
444+ labels:
445+ k8s.ovn.org/bgp-network: ""
446+ spec:
447+ namespaceSelector:
448+ matchExpressions:
449+ - key: kubernetes.io/metadata.name
450+ operator: In
451+ values: [ ` + targetNs + ` ]
452+ network:
453+ topology: Layer3
454+ layer3:
455+ role: Primary
456+ subnets: ` + generateCIDRforClusterUDN ("103.103.0.0/16" , "2014:100:200::0/60" )
457+ }
458+
459+ func generateRAManifest (name string ) string {
460+ return `
461+ apiVersion: k8s.ovn.org/v1
462+ kind: RouteAdvertisements
463+ metadata:
464+ name: ` + name + `
465+ spec:
466+ networkSelector:
467+ matchLabels:
468+ k8s.ovn.org/bgp-network: ""
469+ advertisements:
470+ - "PodNetwork"`
471+ }
0 commit comments