Skip to content

Commit f4205de

Browse files
authored
Merge pull request #5124 from tssurya/bgp-e2e-tests-phase2-udn
Add BGP Tests: Phase2
2 parents eb91e43 + a637c80 commit f4205de

File tree

4 files changed

+264
-13
lines changed

4 files changed

+264
-13
lines changed

contrib/kind-common

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ install_ffr_k8s() {
792792
sed -i "${LINE_NUM}a\\${IPv6_LINE}" receive_filtered.yaml
793793
done
794794
fi
795-
kubectl apply -f receive_filtered.yaml
795+
kubectl apply -n frr-k8s-system -f receive_filtered.yaml
796796
popd || exit 1
797797

798798
rm -rf "${FRR_TMP_DIR}"

test/e2e/e2e.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,9 +585,13 @@ func getApiAddress() string {
585585
func IsGatewayModeLocal() bool {
586586
anno, err := e2ekubectl.RunKubectl("default", "get", "node", "ovn-control-plane", "-o", "template", "--template={{.metadata.annotations}}")
587587
if err != nil {
588+
framework.Logf("Error getting annotations: %v", err)
588589
return false
589590
}
590-
return strings.Contains(anno, "local")
591+
framework.Logf("Annotations received: %s", anno)
592+
isLocal := strings.Contains(anno, "local")
593+
framework.Logf("IsGatewayModeLocal returning: %v", isLocal)
594+
return isLocal
591595
}
592596

593597
// runCommand runs the cmd and returns the combined stdout and stderr

test/e2e/network_segmentation.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2117,7 +2117,7 @@ spec:
21172117
topology: Layer3
21182118
layer3:
21192119
role: Primary
2120-
subnets: ` + generateCIDRforClusterUDN()
2120+
subnets: ` + generateCIDRforClusterUDN("10.20.100.0/16", "2014:100:200::0/60")
21212121
}
21222122

21232123
func newL2SecondaryUDNManifest(name string) string {
@@ -2144,32 +2144,32 @@ spec:
21442144
topology: Layer3
21452145
layer3:
21462146
role: Primary
2147-
subnets: ` + generateCIDRforUDN()
2147+
subnets: ` + generateCIDRforUDN("10.20.100.0/16", "2014:100:200::0/60")
21482148
}
21492149

2150-
func generateCIDRforUDN() string {
2150+
func generateCIDRforUDN(v4, v6 string) string {
21512151
cidr := `
2152-
- cidr: 10.20.100.0/16
2152+
- cidr: ` + v4 + `
21532153
`
21542154
if isIPv6Supported() && isIPv4Supported() {
21552155
cidr = `
2156-
- cidr: 10.20.100.0/16
2157-
- cidr: 2014:100:200::0/60
2156+
- cidr: ` + v4 + `
2157+
- cidr: ` + v6 + `
21582158
`
21592159
} else if isIPv6Supported() {
21602160
cidr = `
2161-
- cidr: 2014:100:200::0/60
2161+
- cidr: ` + v6 + `
21622162
`
21632163
}
21642164
return cidr
21652165
}
21662166

2167-
func generateCIDRforClusterUDN() string {
2168-
cidr := `[{cidr: "10.100.0.0/16"}]`
2167+
func generateCIDRforClusterUDN(v4, v6 string) string {
2168+
cidr := `[{cidr: ` + v4 + `}]`
21692169
if isIPv6Supported() && isIPv4Supported() {
2170-
cidr = `[{cidr: "10.100.0.0/16"},{cidr: "2014:100:200::0/60"}]`
2170+
cidr = `[{cidr: ` + v4 + `},{cidr: ` + v6 + `}]`
21712171
} else if isIPv6Supported() {
2172-
cidr = `[{cidr: "2014:100:200::0/60"}]`
2172+
cidr = `[{cidr: ` + v6 + `}]`
21732173
}
21742174
return cidr
21752175
}

test/e2e/route_advertisements.go

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@ package e2e
22

33
import (
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

Comments
 (0)