diff --git a/hack/vpn-gateway/bird-gw.conf b/hack/vpn-gateway/bird-gw.conf index d89a5c14..72bc4cfc 100644 --- a/hack/vpn-gateway/bird-gw.conf +++ b/hack/vpn-gateway/bird-gw.conf @@ -54,7 +54,7 @@ protocol kernel { } -# separate-appnetwork gw-a1 peers on VLAN 100 (ASN 64512) +# separate-appnetwork-v4 gw-a1 peers on VLAN 100 (ASN 64512) # Also used by dual-stack gw-ds (both IPv4 and IPv6) protocol bgp GW4_A1 from LINK { local 169.254.10.150 port 10179 as 4200000000; @@ -77,7 +77,7 @@ protocol bgp GW6_DS from LINK { }; } -# separate-appnetwork gw-a2 peers on VLAN 200 (ASN 64513) +# separate-appnetwork-v4 gw-a2 peers on VLAN 200 (ASN 64513) protocol bgp GW4_A2 from LINK { local 169.254.11.150 port 10179 as 4200000000; neighbor range 0.0.0.0/0 port 10179 as 64513; @@ -275,3 +275,25 @@ protocol bgp GW4_TCPAO_B2_2 from LINK { export filter bgp_announce; }; } + +# separate-appnetwork-v6 gw-v6a1 peers on VLAN 1300 (ASN 64522, IPv6-only) +protocol bgp GW6_V6A1 from LINK { + local fd00:cafe:70::150 port 10179 as 4200000000; + neighbor range ::/0 port 10179 as 64522; + dynamic name "GW6_V6A1_"; + ipv6 { + import all; + export filter bgp_announce6; + }; +} + +# separate-appnetwork-v6 gw-v6a2 peers on VLAN 1400 (ASN 64523, IPv6-only) +protocol bgp GW6_V6A2 from LINK { + local fd00:cafe:71::150 port 10179 as 4200000000; + neighbor range ::/0 port 10179 as 64523; + dynamic name "GW6_V6A2_"; + ipv6 { + import all; + export filter bgp_announce6; + }; +} diff --git a/hack/vpn-gateway/init.sh b/hack/vpn-gateway/init.sh index 2af28e38..f206f1a5 100644 --- a/hack/vpn-gateway/init.sh +++ b/hack/vpn-gateway/init.sh @@ -4,14 +4,14 @@ sysctl -w net.ipv4.fib_multipath_hash_policy=1 sysctl -w net.ipv4.conf.all.forwarding=1 sysctl -w net.ipv6.conf.all.forwarding=1 -# VLAN 100 — separate-appnetwork gw-a1 + dual-stack gw-ds (shared) +# VLAN 100 — separate-appnetwork-v4 gw-a1 + dual-stack gw-ds (shared) ip link add link eth0 name vlan1 type vlan id 100 ip link set vlan1 up ip addr add 169.254.10.150/24 dev vlan1 ip addr add fd00:cafe:10::150/64 dev vlan1 ip addr add 200.100.0.100/32 dev vlan1 -# VLAN 200 — separate-appnetwork gw-a2 +# VLAN 200 — separate-appnetwork-v4 gw-a2 ip link add link eth0 name vlan2 type vlan id 200 ip link set vlan2 up ip addr add 169.254.11.150/24 dev vlan2 @@ -72,8 +72,18 @@ ip link add link eth0 name vlan12 type vlan id 1200 ip link set vlan12 up ip addr add 169.254.111.150/24 dev vlan12 +# VLAN 1300 — separate-appnetwork-v6 gw-v6a1 +ip link add link eth0 name vlan13 type vlan id 1300 +ip link set vlan13 up +ip addr add fd00:cafe:70::150/64 dev vlan13 + +# VLAN 1400 — separate-appnetwork-v6 gw-v6a2 +ip link add link eth0 name vlan14 type vlan id 1400 +ip link set vlan14 up +ip addr add fd00:cafe:71::150/64 dev vlan14 + ethtool -K eth0 tx off -echo "VPN Gateway ready on VLAN 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200" +echo "VPN Gateway ready on VLAN 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400" /usr/sbin/bird -d -c /etc/bird/bird-gw.conf diff --git a/test/e2e/Makefile b/test/e2e/Makefile index 67c5e609..a9167f19 100644 --- a/test/e2e/Makefile +++ b/test/e2e/Makefile @@ -28,8 +28,9 @@ MULTUS_VERSION ?= v4.1.2 WHEREABOUTS_VERSION ?= v0.8.0 # Suites -SUITES_IPV4 ?= shared-appnetwork separate-appnetwork separate-static-appnetwork sctp-multihoming ipv4-simple pod-cache-label tcp-ao +SUITES_IPV4 ?= shared-appnetwork separate-appnetwork-v4 separate-static-appnetwork sctp-multihoming ipv4-simple pod-cache-label tcp-ao SUITES_DS ?= dual-stack-simple +SUITES_IPV6 ?= separate-appnetwork-v6 # Test flags (e.g., make test GINKGO_FLAGS="--junit-report=report.xml") GINKGO_FLAGS ?= @@ -206,24 +207,45 @@ undeploy-ipv4-simple: ## Undeploy ipv4-simple topology. $(KUBECTL) delete namespace $$NS --ignore-not-found=true ################################################################################# -##@ Suite: separate-appnetwork +##@ Suite: separate-appnetwork-v4 ################################################################################ -.PHONY: separate-appnetwork -separate-appnetwork: deploy-separate-appnetwork test-separate-appnetwork ## Deploy and test separate-appnetwork suite. +.PHONY: separate-appnetwork-v4 +separate-appnetwork-v4: deploy-separate-appnetwork-v4 test-separate-appnetwork-v4 ## Deploy and test separate-appnetwork-v4 suite. -.PHONY: deploy-separate-appnetwork -deploy-separate-appnetwork: cluster ## Deploy separate-appnetwork topology. - $(call deploy-suite,e2e-separate-appnetwork,$(shell pwd)/suites/separate-appnetwork,separate-appnet,sllbr-gw-a1 sllbr-gw-a2) +.PHONY: deploy-separate-appnetwork-v4 +deploy-separate-appnetwork-v4: cluster ## Deploy separate-appnetwork-v4 topology. + $(call deploy-suite,e2e-separate-appnetwork-v4,$(shell pwd)/suites/separate-appnetwork-v4,separate-appnet-v4,sllbr-gw-a1 sllbr-gw-a2) -.PHONY: test-separate-appnetwork -test-separate-appnetwork: ## Run e2e tests for separate-appnetwork suite. - cd $(PROJECT_ROOT)/test/e2e && go run github.com/onsi/ginkgo/v2/ginkgo -v --tags=e2e --focus="Separate App Network" $(GINKGO_FLAGS) +.PHONY: test-separate-appnetwork-v4 +test-separate-appnetwork-v4: ## Run e2e tests for separate-appnetwork-v4 suite. + cd $(PROJECT_ROOT)/test/e2e && go run github.com/onsi/ginkgo/v2/ginkgo -v --tags=e2e --focus="Separate App Network IPv4" $(GINKGO_FLAGS) -.PHONY: undeploy-separate-appnetwork -undeploy-separate-appnetwork: ## Undeploy separate-appnetwork topology. - @NS=e2e-separate-appnetwork; \ - $(KUBECTL) delete validatingwebhookconfiguration meridio-2-validating-webhook-configuration-separate-appnet --ignore-not-found=true; \ +.PHONY: undeploy-separate-appnetwork-v4 +undeploy-separate-appnetwork-v4: ## Undeploy separate-appnetwork-v4 topology. + @NS=e2e-separate-appnetwork-v4; \ + $(KUBECTL) delete validatingwebhookconfiguration meridio-2-validating-webhook-configuration-separate-appnet-v4 --ignore-not-found=true; \ + $(KUBECTL) delete namespace $$NS --ignore-not-found=true + +################################################################################# +##@ Suite: separate-appnetwork-v6 +################################################################################ + +.PHONY: separate-appnetwork-v6 +separate-appnetwork-v6: deploy-separate-appnetwork-v6 test-separate-appnetwork-v6 ## Deploy and test separate-appnetwork-v6 suite. + +.PHONY: deploy-separate-appnetwork-v6 +deploy-separate-appnetwork-v6: cluster ## Deploy separate-appnetwork-v6 topology. + $(call deploy-suite,e2e-separate-appnetwork-v6,$(shell pwd)/suites/separate-appnetwork-v6,separate-appnet-v6,sllbr-gw-v6a1 sllbr-gw-v6a2) + +.PHONY: test-separate-appnetwork-v6 +test-separate-appnetwork-v6: ## Run e2e tests for separate-appnetwork-v6 suite. + cd $(PROJECT_ROOT)/test/e2e && go run github.com/onsi/ginkgo/v2/ginkgo -v --tags=e2e --focus="Separate App Network IPv6" $(GINKGO_FLAGS) + +.PHONY: undeploy-separate-appnetwork-v6 +undeploy-separate-appnetwork-v6: ## Undeploy separate-appnetwork-v6 topology. + @NS=e2e-separate-appnetwork-v6; \ + $(KUBECTL) delete validatingwebhookconfiguration meridio-2-validating-webhook-configuration-separate-appnet-v6 --ignore-not-found=true; \ $(KUBECTL) delete namespace $$NS --ignore-not-found=true ############################################################################## @@ -441,6 +463,25 @@ test-dual-stack: ## Run all dual-stack e2e tests in parallel. undeploy-dual-stack: ## Undeploy all dual-stack suites. @for suite in $(SUITES_DS); do $(MAKE) undeploy-$$suite; done +################################################################################ +##@ All IPv6 suites +################################################################################ + +.PHONY: ipv6 +ipv6: deploy-ipv6 test-ipv6 ## Deploy and test all IPv6 suites. + +.PHONY: deploy-ipv6 +deploy-ipv6: ## Deploy all IPv6 suites. + @for suite in $(SUITES_IPV6); do $(MAKE) deploy-$$suite || exit 1; done + +.PHONY: test-ipv6 +test-ipv6: ## Run all IPv6 e2e tests in parallel. + cd $(PROJECT_ROOT)/test/e2e && go run github.com/onsi/ginkgo/v2/ginkgo -v -p --tags=e2e --label-filter=ipv6 $(GINKGO_FLAGS) + +.PHONY: undeploy-ipv6 +undeploy-ipv6: ## Undeploy all IPv6 suites. + @for suite in $(SUITES_IPV6); do $(MAKE) undeploy-$$suite; done + ################################################################################ ##@ Suite: openshift-crc ################################################################################ diff --git a/test/e2e/e2e_suite_appnetwork_test.go b/test/e2e/e2e_suite_appnetwork_test.go index aad6851f..0e5ca22b 100644 --- a/test/e2e/e2e_suite_appnetwork_test.go +++ b/test/e2e/e2e_suite_appnetwork_test.go @@ -35,8 +35,8 @@ type suiteTestCase struct { var testCases = []suiteTestCase{ { - name: "Separate App Network", - namespace: "e2e-separate-appnetwork", + name: "Separate App Network IPv4", + namespace: "e2e-separate-appnetwork-v4", targetApp: "target-a", targetReplicas: 2, gateways: []gwTestCase{ @@ -66,6 +66,19 @@ var testCases = []suiteTestCase{ }, } +var ipv6TestCases = []suiteTestCase{ + { + name: "Separate App Network IPv6", + namespace: "e2e-separate-appnetwork-v6", + targetApp: "target-v6a", + targetReplicas: 2, + gateways: []gwTestCase{ + {name: "gw-v6a1", vip: "fd00:cafe:7::1", targets: 2, dgName: "dg-v6a1"}, + {name: "gw-v6a2", vip: "fd00:cafe:7::2", targets: 2, dgName: "dg-v6a2"}, + }, + }, +} + var lowMTUTestCase = suiteTestCase{ name: "Low MTU", namespace: "e2e-ipv4-simple", @@ -356,8 +369,8 @@ var _ = Describe("E2E Test Suites", Label("ipv4"), func() { switch suite.name { case "Shared App Network": suiteDir = "shared-appnetwork" - case "Separate App Network": - suiteDir = "separate-appnetwork" + case "Separate App Network IPv4": + suiteDir = "separate-appnetwork-v4" case "Separate Static App Network": suiteDir = "separate-static-appnetwork" default: @@ -569,3 +582,427 @@ var _ = Describe("E2E Test Suites", Label("ipv4"), func() { }) }) }) + +var _ = Describe("E2E IPv6 Test Suites", Label("ipv6"), func() { + SetDefaultEventuallyTimeout(5 * time.Minute) + SetDefaultEventuallyPollingInterval(2 * time.Second) + + for _, suite := range ipv6TestCases { + suite := suite + Describe(suite.name, Ordered, func() { + Context("Deployment", func() { + for _, gw := range suite.gateways { + gw := gw + It(fmt.Sprintf("should have %s Accepted", gw.name), func() { + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "gateway", gw.name, "-n", suite.namespace, + "-o", "jsonpath={.status.conditions[?(@.type=='Accepted')].status}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(Equal("True")) + }).Should(Succeed()) + }) + + It(fmt.Sprintf("should have %s Programmed", gw.name), func() { + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "gateway", gw.name, "-n", suite.namespace, + "-o", "jsonpath={.status.conditions[?(@.type=='Programmed')].status}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(Equal("True")) + }).Should(Succeed()) + }) + + It(fmt.Sprintf("should deploy LB Pod for %s", gw.name), func() { + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "pods", "-n", suite.namespace, + "-l", fmt.Sprintf("gateway.networking.k8s.io/gateway-name=%s", gw.name), + "-o", "jsonpath={.items[*].status.phase}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(ContainSubstring("Running")) + }).Should(Succeed()) + }) + + It(fmt.Sprintf("should have %s LB Pod containers ready", gw.name), func() { + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "pods", "-n", suite.namespace, + "-l", fmt.Sprintf("gateway.networking.k8s.io/gateway-name=%s", gw.name), + "-o", "jsonpath={.items[*].status.containerStatuses[*].ready}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).NotTo(ContainSubstring("false"), "all containers should be ready") + }).Should(Succeed()) + }) + } + + It("should have Gateway status.addresses populated with VIPs", func() { + for _, gw := range suite.gateways { + gw := gw + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "gateway", gw.name, "-n", suite.namespace, + "-o", "jsonpath={.status.addresses[*].value}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(ContainSubstring(gw.vip), "Gateway %s should have VIP %s in status", gw.name, gw.vip) + }).Should(Succeed()) + } + }) + + It("should have DistributionGroups Ready", func() { + for _, gw := range suite.gateways { + gw := gw + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "distg", gw.dgName, "-n", suite.namespace, + "-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(Equal("True"), "%s should be Ready", gw.dgName) + }).Should(Succeed()) + } + }) + + It(fmt.Sprintf("should create %d ENCs (one per target pod)", suite.targetReplicas), func() { + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "enc", "-n", suite.namespace, + "-o", "jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(utils.GetNonEmptyLines(out))).To(Equal(suite.targetReplicas)) + }).Should(Succeed()) + }) + + It("should create LoadBalancerEndpointSlices with Maglev IDs for each DistributionGroup", func() { + for _, gw := range suite.gateways { + gw := gw + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "lbeslice", "-n", suite.namespace, + "-l", fmt.Sprintf("meridio-2.nordix.org/distribution-group=%s", gw.dgName), + "-o", "json") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + + var result struct { + Items []struct { + Spec struct { + Endpoints []struct { + Target struct { + Name string `json:"name"` + } `json:"target"` + Identifier *int32 `json:"identifier"` + } `json:"endpoints"` + } `json:"spec"` + } `json:"items"` + } + err = utils.ParseJSON(out, &result) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(result.Items).NotTo(BeEmpty(), "no LoadBalancerEndpointSlices found for %s", gw.dgName) + + for _, slice := range result.Items { + for _, ep := range slice.Spec.Endpoints { + g.Expect(ep.Identifier).NotTo(BeNil(), "endpoint %s missing identifier", ep.Target.Name) + } + } + }).Should(Succeed()) + } + }) + + It(fmt.Sprintf("should have %d target Pods running with sidecar", suite.targetReplicas), func() { + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "pods", "-n", suite.namespace, + "-l", fmt.Sprintf("app=%s", suite.targetApp), "--field-selector=status.phase=Running", + "-o", "jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(utils.GetNonEmptyLines(out))).To(Equal(suite.targetReplicas)) + }).Should(Succeed()) + }) + + It("should have all ENCs Ready", func() { + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "enc", "-n", suite.namespace, + "-o", "jsonpath={range .items[*]}{.status.conditions[?(@.type=='Ready')].status}{\"\\n\"}{end}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + lines := utils.GetNonEmptyLines(out) + g.Expect(len(lines)).To(Equal(suite.targetReplicas), "expected %d ENCs", suite.targetReplicas) + for _, status := range lines { + g.Expect(status).To(Equal("True"), "all ENCs should be Ready") + } + }).Should(Succeed()) + }) + }) + + Context("Traffic", func() { + BeforeAll(func() { + By("waiting for BGP routes to propagate to VPN gateway") + for _, gw := range suite.gateways { + Eventually(func() error { return e2eutils.Ping(gw.vip) }).Should(Succeed()) + } + }) + + Context("ICMP reachability", func() { + for _, gw := range suite.gateways { + gw := gw + It("handles ping on "+gw.name+" VIP", func() { + Eventually(func() error { return e2eutils.Ping(gw.vip) }). + WithTimeout(30 * time.Second).Should(Succeed()) + }) + } + }) + + Context("TCP load balancing", func() { + for _, gw := range suite.gateways { + gw := gw + It("distributes "+gw.name+" TCP traffic across targets", func() { + lastingConn, lostConn, err := e2eutils.SendTraffic(gw.vip, 5000, "tcp", 100) + Expect(err).NotTo(HaveOccurred()) + Expect(lostConn).To(BeZero(), "no connections should be lost") + Expect(len(lastingConn)).To(Equal(gw.targets), + "%s: expected %d targets, got: %v", gw.name, gw.targets, lastingConn) + }) + } + }) + + Context("UDP load balancing", func() { + for _, gw := range suite.gateways { + gw := gw + It("distributes "+gw.name+" UDP traffic across targets", func() { + lastingConn, lostConn, err := e2eutils.SendTraffic(gw.vip, 5001, "udp", 100) + Expect(err).NotTo(HaveOccurred()) + Expect(lostConn).To(BeZero(), "no connections should be lost") + Expect(len(lastingConn)).To(Equal(gw.targets), + "%s: expected %d targets, got: %v", gw.name, gw.targets, lastingConn) + }) + } + }) + }) + + // Sidecar restart recovery test (for suites with 2+ gateways) + if len(suite.gateways) >= 2 { + Context("Sidecar Restart Recovery", func() { + var ( + targetPod string + gw1 gwTestCase + gw2 gwTestCase + tableID1 string + tableID2 string + ) + + BeforeAll(func() { + gw1 = suite.gateways[0] + gw2 = suite.gateways[1] + + By("selecting first target pod") + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "pods", "-n", suite.namespace, + "-l", "app="+suite.targetApp, "--field-selector=status.phase=Running", + "-o", "jsonpath={.items[0].metadata.name}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + targetPod = strings.TrimSpace(out) + g.Expect(targetPod).NotTo(BeEmpty()) + }).Should(Succeed()) + + By("waiting for ENC to be Ready") + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "enc", targetPod, "-n", suite.namespace, + "-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(Equal("True")) + }).Should(Succeed()) + + By("capturing initial table IDs") + cmd := exec.Command("kubectl", "exec", "-n", suite.namespace, targetPod, + "-c", "network-sidecar", "--", "ip", "-6", "rule", "show") + out, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + + for _, line := range strings.Split(out, "\n") { + if strings.Contains(line, gw1.vip) { + fields := strings.Fields(line) + for i, f := range fields { + if f == "lookup" && i+1 < len(fields) { + tableID1 = fields[i+1] + break + } + } + } else if strings.Contains(line, gw2.vip) { + fields := strings.Fields(line) + for i, f := range fields { + if f == "lookup" && i+1 < len(fields) { + tableID2 = fields[i+1] + break + } + } + } + } + Expect(tableID1).NotTo(BeEmpty(), "should find table ID for %s", gw1.name) + Expect(tableID2).NotTo(BeEmpty(), "should find table ID for %s", gw2.name) + + By("setting restart gate marker") + cmd = exec.Command("kubectl", "exec", "-n", suite.namespace, targetPod, + "-c", "network-sidecar", "--", "sh", "-c", + "touch /restart-gate/already-started && rm -f /restart-gate/release-restart") + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterAll(func() { + By(fmt.Sprintf("restoring %s gateway for subsequent tests", gw2.name)) + _, filename, _, ok := runtime.Caller(0) + Expect(ok).To(BeTrue(), "failed to get test file path") + testDir := filepath.Dir(filename) + + // Derive suite directory from suite name + var suiteDir string + switch suite.name { + case "Separate App Network IPv6": + suiteDir = "separate-appnetwork-v6" + default: + Skip(fmt.Sprintf("Unknown suite: %s", suite.name)) + } + + gatewayPath := filepath.Join(testDir, "suites", suiteDir, "gateway.yaml") + cmd := exec.Command("kubectl", "apply", "-f", gatewayPath, "-n", suite.namespace) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + + By(fmt.Sprintf("waiting for %s to be Accepted", gw2.name)) + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "gateway", gw2.name, "-n", suite.namespace, + "-o", "jsonpath={.status.conditions[?(@.type=='Accepted')].status}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(Equal("True")) + }).Should(Succeed()) + + By(fmt.Sprintf("waiting for %s to be Programmed", gw2.name)) + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "gateway", gw2.name, "-n", suite.namespace, + "-o", "jsonpath={.status.conditions[?(@.type=='Programmed')].status}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(Equal("True")) + }).Should(Succeed()) + + By(fmt.Sprintf("waiting for %s ENC to include %s", targetPod, gw2.name)) + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "enc", targetPod, "-n", suite.namespace, + "-o", "jsonpath={.spec.gateways[*].name}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(ContainSubstring(gw2.name)) + }).Should(Succeed()) + + By(fmt.Sprintf("waiting for sidecar to configure %s VIP", gw2.name)) + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "exec", "-n", suite.namespace, targetPod, + "-c", "network-sidecar", "--", "ip", "addr", "show") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(ContainSubstring(gw2.vip)) + }).Should(Succeed()) + }) + + It("should preserve table IDs and clean up deleted gateway state", func() { + By("killing sidecar container (will pause at restart gate)") + cmd := exec.Command("kubectl", "exec", "-n", suite.namespace, targetPod, + "-c", "network-sidecar", "--", "kill", "1") + utils.Run(cmd) // Ignore error (container dies) + + By("waiting for sidecar to reach restart gate") + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "logs", "-n", suite.namespace, targetPod, + "-c", "network-sidecar", "--tail=5") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(ContainSubstring("Restart detected")) + }).Should(Succeed()) + + By(fmt.Sprintf("deleting %s gateway while sidecar is paused", gw2.name)) + cmd = exec.Command("kubectl", "delete", "gateway", gw2.name, "-n", suite.namespace) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + + By(fmt.Sprintf("waiting for ENC controller to remove %s from ENC", gw2.name)) + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "enc", targetPod, "-n", suite.namespace, + "-o", "jsonpath={.spec.gateways[*].name}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).NotTo(ContainSubstring(gw2.name)) + g.Expect(out).To(ContainSubstring(gw1.name)) + }).Should(Succeed()) + + By("releasing restart gate") + cmd = exec.Command("kubectl", "exec", "-n", suite.namespace, targetPod, + "-c", "network-sidecar", "--", "touch", "/restart-gate/release-restart") + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + + By("waiting for sidecar to become ready") + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "pod", targetPod, "-n", suite.namespace, + "-o", "jsonpath={.status.containerStatuses[?(@.name=='network-sidecar')].ready}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(Equal("true")) + }).Should(Succeed()) + + By("waiting for ENC to be Ready after restart") + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "get", "enc", targetPod, "-n", suite.namespace, + "-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).To(Equal("True")) + }).Should(Succeed()) + + By(fmt.Sprintf("waiting for %s VIP to be removed", gw2.name)) + Eventually(func(g Gomega) { + cmd := exec.Command("kubectl", "exec", "-n", suite.namespace, targetPod, + "-c", "network-sidecar", "--", "ip", "addr", "show") + out, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out).NotTo(ContainSubstring(gw2.vip)) + }, 30*time.Second, 1*time.Second).Should(Succeed()) + + By(fmt.Sprintf("verifying %s table ID unchanged and no orphaned rules/tables", gw1.name)) + cmd = exec.Command("kubectl", "exec", "-n", suite.namespace, targetPod, + "-c", "network-sidecar", "--", "ip", "-6", "rule", "show") + out, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + + var newTableID1 string + for _, line := range strings.Split(out, "\n") { + if strings.Contains(line, gw1.vip) { + fields := strings.Fields(line) + for i, f := range fields { + if f == "lookup" && i+1 < len(fields) { + newTableID1 = fields[i+1] + break + } + } + } + } + Expect(newTableID1).To(Equal(tableID1), "%s table ID should be preserved", gw1.name) + Expect(out).To(ContainSubstring(gw1.vip), "%s rule should be present", gw1.name) + Expect(out).NotTo(ContainSubstring(gw2.vip), "%s rule should be removed (no orphan)", gw2.name) + hasOrphanedTable := strings.Contains(out, "lookup "+tableID2) + Expect(hasOrphanedTable).To(BeFalse(), "%s old table %s should be cleaned up (no orphan)", gw2.name, tableID2) + + By(fmt.Sprintf("verifying no VIP leak (%s VIP removed)", gw2.name)) + cmd = exec.Command("kubectl", "exec", "-n", suite.namespace, targetPod, + "-c", "network-sidecar", "--", "ip", "addr", "show") + out, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred()) + Expect(out).To(ContainSubstring(gw1.vip), "%s VIP should be present", gw1.name) + Expect(out).NotTo(ContainSubstring(gw2.vip), "%s VIP should be removed (no leak)", gw2.name) + }) + }) + } + }) + } +}) diff --git a/test/e2e/suites/README.md b/test/e2e/suites/README.md index b4673b5a..a6a06a0c 100644 --- a/test/e2e/suites/README.md +++ b/test/e2e/suites/README.md @@ -65,9 +65,9 @@ IPv6 achieves this with `fd00:cafe:{X}::` (external) vs `fd00:cafe:1{X}0::` (int | VLAN ID | Suite | Gateway | External Subnet (IPv4) | External Subnet (IPv6) | Internal Subnet (IPv4) | Internal Subnet (IPv6) | VIP(s) | Local ASN | Remote ASN | |---------|-------|---------|----------------------|----------------------|----------------------|----------------------|--------|-----------|------------| -| 100 | separate-appnetwork | gw-a1 | `169.254.10.0/24` | — | `169.111.10.0/24` | — | `10.0.0.1/32` | 64512 | 4200000000 | +| 100 | separate-appnetwork-v4 | gw-a1 | `169.254.10.0/24` | — | `169.111.10.0/24` | — | `10.0.0.1/32` | 64512 | 4200000000 | | 100 | dual-stack | gw-ds | `169.254.10.0/24` | `fd00:cafe:10::/64` | `169.111.10.0/24` | `fd00:cafe:110::/64` | `10.0.0.1/32`, `fd00:cafe:1::1/128` | 64512 | 4200000000 | -| 200 | separate-appnetwork | gw-a2 | `169.254.11.0/24` | — | `169.111.10.0/24` | — | `10.0.0.2/32` | 64513 | 4200000000 | +| 200 | separate-appnetwork-v4 | gw-a2 | `169.254.11.0/24` | — | `169.111.10.0/24` | — | `10.0.0.2/32` | 64513 | 4200000000 | | 300 | shared-appnetwork | gw-b1 | `169.254.20.0/24` | — | `169.111.20.0/24` | — | `20.0.0.1/32` | 64514 | 4200000000 | | 400 | shared-appnetwork | gw-b2 | `169.254.21.0/24` | — | `169.111.20.0/24` | — | `20.0.0.2/32` | 64515 | 4200000000 | | 500 | sctp-multihoming | sctp-gw1 | `169.254.30.0/24` | — | `169.111.30.0/24` | — | `30.0.0.1/32` | 64516 | 4200000000 | @@ -78,8 +78,10 @@ IPv6 achieves this with `fd00:cafe:{X}::` (external) vs `fd00:cafe:1{X}0::` (int | 1000 | tcp-ao | gw-t2 | `169.254.61.0/24` | — | `169.111.60.0/24` | — | `60.0.0.2/32` | 64521 | 4200000000 | | 1100 | separate-static-appnetwork | gw-a1 | `169.254.110.0/24` | — | `169.111.110.0/24` | — | `110.0.0.1/32` | — (static+BFD) | — | | 1200 | separate-static-appnetwork | gw-a2 | `169.254.111.0/24` | — | `169.111.110.0/24` | — | `110.0.0.2/32` | — (static+BFD) | — | +| 1300 | separate-appnetwork-v6 | gw-v6a1 | — | `fd00:cafe:70::/64` | — | `fd00:cafe:170::/64` | `fd00:cafe:7::1/128` | 64522 | 4200000000 | +| 1400 | separate-appnetwork-v6 | gw-v6a2 | — | `fd00:cafe:71::/64` | — | `fd00:cafe:170::/64` | `fd00:cafe:7::2/128` | 64523 | 4200000000 | -**Next available:** VLAN 1300, ASN 64522, external `169.254.70.0/24`, internal `169.111.70.0/24`, VIP `70.0.0.1/32` +**Next available:** VLAN 1500, ASN 64524, external `169.254.80.0/24` / `fd00:cafe:80::/64`, internal `169.111.80.0/24` / `fd00:cafe:180::/64`, VIP `80.0.0.1/32` / `fd00:cafe:8::1/128` ## Adding a New Suite @@ -182,7 +184,7 @@ ipRanges: ### 5. Namespace naming -Use `e2e-{suite-name}` (e.g., `e2e-dual-stack`, `e2e-separate-appnetwork`). +Use `e2e-{suite-name}` (e.g., `e2e-dual-stack`, `e2e-separate-appnetwork-v4`). ## Maintaining This Document @@ -197,6 +199,6 @@ Update this README whenever you add, remove, or modify a test suite. Specificall - The VPN gateway's remote ASN is always `4200000000` - All BGP sessions use port `10179` (both local and remote) - BFD is enabled on all sessions with 300ms intervals and multiplier 3 (or 5 for SCTP) -- The `separate-appnetwork` and `dual-stack` suites share VLAN 100 — they cannot run simultaneously +- The `separate-appnetwork-v4` and `dual-stack` suites share VLAN 100 — they cannot run simultaneously - Suites sharing the same VLAN are mutually exclusive (deploy only one at a time) - The `separate-static-appnetwork` suite uses static routing with BFD. LB pod IPs are limited to `.1`-`.10` per VLAN (max 10 replicas per gateway) to match the gateway's pre-configured static routes. diff --git a/test/e2e/suites/dual-stack/nad.yaml b/test/e2e/suites/dual-stack/nad.yaml index ef080551..bbe886f4 100644 --- a/test/e2e/suites/dual-stack/nad.yaml +++ b/test/e2e/suites/dual-stack/nad.yaml @@ -27,7 +27,7 @@ spec: ] }' --- -# VLAN 100 NAD for dual-stack BGP peering (reuses separate-appnetwork VLAN) +# VLAN 100 NAD for dual-stack BGP peering (reuses separate-appnetwork-v4 VLAN) apiVersion: k8s.cni.cncf.io/v1 kind: NetworkAttachmentDefinition metadata: diff --git a/test/e2e/suites/dual-stack/routing.yaml b/test/e2e/suites/dual-stack/routing.yaml index c0660ae2..a5e3b9fe 100644 --- a/test/e2e/suites/dual-stack/routing.yaml +++ b/test/e2e/suites/dual-stack/routing.yaml @@ -1,5 +1,5 @@ --- -# IPv4 router (VLAN 100, ASN 64512) - reuses separate-appnetwork gw-a1 slot +# IPv4 router (VLAN 100, ASN 64512) - reuses separate-appnetwork-v4 gw-a1 slot apiVersion: meridio-2.nordix.org/v1alpha1 kind: GatewayRouter metadata: @@ -21,7 +21,7 @@ spec: minRx: 300ms multiplier: 3 --- -# IPv6 router (VLAN 100, ASN 64512) - reuses separate-appnetwork gw-a1 slot +# IPv6 router (VLAN 100, ASN 64512) - reuses separate-appnetwork-v4 gw-a1 slot apiVersion: meridio-2.nordix.org/v1alpha1 kind: GatewayRouter metadata: diff --git a/test/e2e/suites/separate-appnetwork/dg.yaml b/test/e2e/suites/separate-appnetwork-v4/dg.yaml similarity index 100% rename from test/e2e/suites/separate-appnetwork/dg.yaml rename to test/e2e/suites/separate-appnetwork-v4/dg.yaml diff --git a/test/e2e/suites/separate-appnetwork/gateway.yaml b/test/e2e/suites/separate-appnetwork-v4/gateway.yaml similarity index 85% rename from test/e2e/suites/separate-appnetwork/gateway.yaml rename to test/e2e/suites/separate-appnetwork-v4/gateway.yaml index 858a692d..d9ddf8ad 100644 --- a/test/e2e/suites/separate-appnetwork/gateway.yaml +++ b/test/e2e/suites/separate-appnetwork-v4/gateway.yaml @@ -25,17 +25,17 @@ spec: - type: NAD nad: name: sysctl-tuning - namespace: e2e-separate-appnetwork + namespace: e2e-separate-appnetwork-v4 interface: dummy - type: NAD nad: name: vlan-100 - namespace: e2e-separate-appnetwork + namespace: e2e-separate-appnetwork-v4 interface: vlan-100 - type: NAD nad: name: app-net-a1 - namespace: e2e-separate-appnetwork + namespace: e2e-separate-appnetwork-v4 interface: net-a1 internalSubnets: - attachmentType: NAD @@ -69,17 +69,17 @@ spec: - type: NAD nad: name: sysctl-tuning - namespace: e2e-separate-appnetwork + namespace: e2e-separate-appnetwork-v4 interface: dummy - type: NAD nad: name: vlan-200 - namespace: e2e-separate-appnetwork + namespace: e2e-separate-appnetwork-v4 interface: vlan-200 - type: NAD nad: name: app-net-a2 - namespace: e2e-separate-appnetwork + namespace: e2e-separate-appnetwork-v4 interface: net-a2 internalSubnets: - attachmentType: NAD diff --git a/test/e2e/suites/separate-appnetwork/kustomization.yaml b/test/e2e/suites/separate-appnetwork-v4/kustomization.yaml similarity index 67% rename from test/e2e/suites/separate-appnetwork/kustomization.yaml rename to test/e2e/suites/separate-appnetwork-v4/kustomization.yaml index 2055673f..3b560caa 100644 --- a/test/e2e/suites/separate-appnetwork/kustomization.yaml +++ b/test/e2e/suites/separate-appnetwork-v4/kustomization.yaml @@ -1,7 +1,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: e2e-separate-appnetwork +namespace: e2e-separate-appnetwork-v4 resources: - ../../../../config/default @@ -11,11 +11,11 @@ images: newName: registry.nordix.org/cloud-native/meridio-2/controller-manager newTag: latest -nameSuffix: -separate-appnet +nameSuffix: -separate-appnet-v4 labels: - pairs: - e2e-suite: separate-appnet + e2e-suite: separate-appnet-v4 patches: - patch: |- @@ -24,27 +24,27 @@ patches: metadata: name: not-important annotations: - cert-manager.io/inject-ca-from: e2e-separate-appnetwork/meridio-2-serving-cert-separate-appnet + cert-manager.io/inject-ca-from: e2e-separate-appnetwork-v4/meridio-2-serving-cert-separate-appnet-v4 webhooks: - name: vl34route-v1alpha1.kb.io namespaceSelector: matchLabels: - e2e-suite: separate-appnet + e2e-suite: separate-appnet-v4 clientConfig: service: - name: meridio-2-webhook-service-separate-appnet - namespace: e2e-separate-appnetwork + name: meridio-2-webhook-service-separate-appnet-v4 + namespace: e2e-separate-appnetwork-v4 target: kind: ValidatingWebhookConfiguration - patch: |- - op: replace path: /spec/dnsNames value: - - meridio-2-webhook-service-separate-appnet.e2e-separate-appnetwork.svc - - meridio-2-webhook-service-separate-appnet.e2e-separate-appnetwork.svc.cluster.local + - meridio-2-webhook-service-separate-appnet-v4.e2e-separate-appnetwork-v4.svc + - meridio-2-webhook-service-separate-appnet-v4.e2e-separate-appnetwork-v4.svc.cluster.local - op: replace path: /spec/secretName - value: webhook-server-cert-separate-appnet + value: webhook-server-cert-separate-appnet-v4 target: group: cert-manager.io kind: Certificate @@ -61,7 +61,7 @@ patches: - name: manager env: - name: MERIDIO_LB_SERVICE_ACCOUNT - value: meridio-2-stateless-load-balancer-separate-appnet + value: meridio-2-stateless-load-balancer-separate-appnet-v4 - name: MERIDIO_LOG_LEVEL value: info - name: MERIDIO_LOG_LEVEL_API @@ -69,7 +69,7 @@ patches: volumes: - name: webhook-certs secret: - secretName: webhook-server-cert-separate-appnet + secretName: webhook-server-cert-separate-appnet-v4 target: kind: Deployment labelSelector: control-plane=controller-manager diff --git a/test/e2e/suites/separate-appnetwork/nad.yaml b/test/e2e/suites/separate-appnetwork-v4/nad.yaml similarity index 100% rename from test/e2e/suites/separate-appnetwork/nad.yaml rename to test/e2e/suites/separate-appnetwork-v4/nad.yaml diff --git a/test/e2e/suites/separate-appnetwork/rbac.yaml b/test/e2e/suites/separate-appnetwork-v4/rbac.yaml similarity index 81% rename from test/e2e/suites/separate-appnetwork/rbac.yaml rename to test/e2e/suites/separate-appnetwork-v4/rbac.yaml index 76a851a1..2b0ca3cf 100644 --- a/test/e2e/suites/separate-appnetwork/rbac.yaml +++ b/test/e2e/suites/separate-appnetwork-v4/rbac.yaml @@ -3,13 +3,13 @@ apiVersion: v1 kind: ServiceAccount metadata: name: meridio-2-network-sidecar - namespace: e2e-separate-appnetwork + namespace: e2e-separate-appnetwork-v4 --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: meridio-2-network-sidecar - namespace: e2e-separate-appnetwork + namespace: e2e-separate-appnetwork-v4 rules: - apiGroups: ["meridio-2.nordix.org"] resources: ["endpointnetworkconfigurations"] @@ -22,7 +22,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: meridio-2-network-sidecar - namespace: e2e-separate-appnetwork + namespace: e2e-separate-appnetwork-v4 roleRef: apiGroup: rbac.authorization.k8s.io kind: Role @@ -30,4 +30,4 @@ roleRef: subjects: - kind: ServiceAccount name: meridio-2-network-sidecar - namespace: e2e-separate-appnetwork + namespace: e2e-separate-appnetwork-v4 diff --git a/test/e2e/suites/separate-appnetwork/routing.yaml b/test/e2e/suites/separate-appnetwork-v4/routing.yaml similarity index 100% rename from test/e2e/suites/separate-appnetwork/routing.yaml rename to test/e2e/suites/separate-appnetwork-v4/routing.yaml diff --git a/test/e2e/suites/separate-appnetwork/targets.yaml b/test/e2e/suites/separate-appnetwork-v4/targets.yaml similarity index 100% rename from test/e2e/suites/separate-appnetwork/targets.yaml rename to test/e2e/suites/separate-appnetwork-v4/targets.yaml diff --git a/test/e2e/suites/separate-appnetwork-v6/dg.yaml b/test/e2e/suites/separate-appnetwork-v6/dg.yaml new file mode 100644 index 00000000..b81fa8dc --- /dev/null +++ b/test/e2e/suites/separate-appnetwork-v6/dg.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: meridio-2.nordix.org/v1alpha1 +kind: DistributionGroup +metadata: + name: dg-v6a1 +spec: + selector: + matchLabels: + app: target-v6a + maglev: + maxEndpoints: 32 + parentRefs: + - name: gw-v6a1 +--- +apiVersion: meridio-2.nordix.org/v1alpha1 +kind: DistributionGroup +metadata: + name: dg-v6a2 +spec: + selector: + matchLabels: + app: target-v6a + maglev: + maxEndpoints: 32 + parentRefs: + - name: gw-v6a2 diff --git a/test/e2e/suites/separate-appnetwork-v6/gateway.yaml b/test/e2e/suites/separate-appnetwork-v6/gateway.yaml new file mode 100644 index 00000000..6b8cadfb --- /dev/null +++ b/test/e2e/suites/separate-appnetwork-v6/gateway.yaml @@ -0,0 +1,88 @@ +--- +# Gateway V6A1 on VLAN 1300 with app-net-v6a1 (IPv6-only) +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gw-v6a1 +spec: + gatewayClassName: meridio + infrastructure: + parametersRef: + group: meridio-2.nordix.org + kind: GatewayConfiguration + name: gwconfig-v6a1 + listeners: + - name: default + protocol: TCP + port: 80 +--- +apiVersion: meridio-2.nordix.org/v1alpha1 +kind: GatewayConfiguration +metadata: + name: gwconfig-v6a1 +spec: + networkAttachments: + - type: NAD + nad: + name: sysctl-tuning + namespace: e2e-separate-appnetwork-v6 + interface: dummy + - type: NAD + nad: + name: vlan-1300 + namespace: e2e-separate-appnetwork-v6 + interface: vlan-1300 + - type: NAD + nad: + name: app-net-v6a1 + namespace: e2e-separate-appnetwork-v6 + interface: net-v6a1 + internalSubnets: + - attachmentType: NAD + cidr: "fd00:cafe:170::/64" + horizontalScaling: + replicas: 2 +--- +# Gateway V6A2 on VLAN 1400 with app-net-v6a2 (IPv6-only) +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gw-v6a2 +spec: + gatewayClassName: meridio + infrastructure: + parametersRef: + group: meridio-2.nordix.org + kind: GatewayConfiguration + name: gwconfig-v6a2 + listeners: + - name: default + protocol: TCP + port: 80 +--- +apiVersion: meridio-2.nordix.org/v1alpha1 +kind: GatewayConfiguration +metadata: + name: gwconfig-v6a2 +spec: + networkAttachments: + - type: NAD + nad: + name: sysctl-tuning + namespace: e2e-separate-appnetwork-v6 + interface: dummy + - type: NAD + nad: + name: vlan-1400 + namespace: e2e-separate-appnetwork-v6 + interface: vlan-1400 + - type: NAD + nad: + name: app-net-v6a2 + namespace: e2e-separate-appnetwork-v6 + interface: net-v6a2 + internalSubnets: + - attachmentType: NAD + cidr: "fd00:cafe:170::/64" + horizontalScaling: + replicas: 2 diff --git a/test/e2e/suites/separate-appnetwork-v6/kustomization.yaml b/test/e2e/suites/separate-appnetwork-v6/kustomization.yaml new file mode 100644 index 00000000..df06508e --- /dev/null +++ b/test/e2e/suites/separate-appnetwork-v6/kustomization.yaml @@ -0,0 +1,75 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: e2e-separate-appnetwork-v6 + +resources: +- ../../../../config/default + +images: +- name: controller-manager + newName: registry.nordix.org/cloud-native/meridio-2/controller-manager + newTag: latest + +nameSuffix: -separate-appnet-v6 + +labels: +- pairs: + e2e-suite: separate-appnet-v6 + +patches: +- patch: |- + apiVersion: admissionregistration.k8s.io/v1 + kind: ValidatingWebhookConfiguration + metadata: + name: not-important + annotations: + cert-manager.io/inject-ca-from: e2e-separate-appnetwork-v6/meridio-2-serving-cert-separate-appnet-v6 + webhooks: + - name: vl34route-v1alpha1.kb.io + namespaceSelector: + matchLabels: + e2e-suite: separate-appnet-v6 + clientConfig: + service: + name: meridio-2-webhook-service-separate-appnet-v6 + namespace: e2e-separate-appnetwork-v6 + target: + kind: ValidatingWebhookConfiguration +- patch: |- + - op: replace + path: /spec/dnsNames + value: + - meridio-2-webhook-service-separate-appnet-v6.e2e-separate-appnetwork-v6.svc + - meridio-2-webhook-service-separate-appnet-v6.e2e-separate-appnetwork-v6.svc.cluster.local + - op: replace + path: /spec/secretName + value: webhook-server-cert-separate-appnet-v6 + target: + group: cert-manager.io + kind: Certificate + name: serving-cert +- patch: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: not-important + spec: + template: + spec: + containers: + - name: manager + env: + - name: MERIDIO_LB_SERVICE_ACCOUNT + value: meridio-2-stateless-load-balancer-separate-appnet-v6 + - name: MERIDIO_LOG_LEVEL + value: info + - name: MERIDIO_LOG_LEVEL_API + value: 127.0.0.1:9901 + volumes: + - name: webhook-certs + secret: + secretName: webhook-server-cert-separate-appnet-v6 + target: + kind: Deployment + labelSelector: control-plane=controller-manager diff --git a/test/e2e/suites/separate-appnetwork-v6/nad.yaml b/test/e2e/suites/separate-appnetwork-v6/nad.yaml new file mode 100644 index 00000000..8f4e3098 --- /dev/null +++ b/test/e2e/suites/separate-appnetwork-v6/nad.yaml @@ -0,0 +1,118 @@ +--- +# Sysctl tuning NAD +apiVersion: k8s.cni.cncf.io/v1 +kind: NetworkAttachmentDefinition +metadata: + name: sysctl-tuning +spec: + config: '{ + "cniVersion": "0.4.0", + "name": "sysctl-tuning", + "plugins": [ + { + "type": "tuning", + "prevResult": {}, + "sysctl": { + "net.ipv4.conf.all.forwarding": "1", + "net.ipv6.conf.all.forwarding": "1", + "net.ipv4.fib_multipath_hash_policy": "1", + "net.ipv6.fib_multipath_hash_policy": "1", + "net.ipv4.conf.all.rp_filter": "2", + "net.ipv4.conf.default.rp_filter": "2", + "net.ipv4.fwmark_reflect": "1", + "net.ipv6.fwmark_reflect": "1", + "net.ipv4.ip_local_port_range": "49152 65535" + } + } + ] + }' +--- +# VLAN 1300 NAD for gw-v6a1 BGP peering (IPv6-only) +apiVersion: k8s.cni.cncf.io/v1 +kind: NetworkAttachmentDefinition +metadata: + name: vlan-1300 +spec: + config: | + { + "cniVersion": "0.3.1", + "type": "vlan", + "master": "eth0", + "vlanId": 1300, + "ipam": { + "type": "whereabouts", + "ipRanges": [ + { "range": "fd00:cafe:70::/64", "exclude": ["fd00:cafe:70::150/128"] } + ] + } + } +--- +# VLAN 1400 NAD for gw-v6a2 BGP peering (IPv6-only) +apiVersion: k8s.cni.cncf.io/v1 +kind: NetworkAttachmentDefinition +metadata: + name: vlan-1400 +spec: + config: | + { + "cniVersion": "0.3.1", + "type": "vlan", + "master": "eth0", + "vlanId": 1400, + "ipam": { + "type": "whereabouts", + "ipRanges": [ + { "range": "fd00:cafe:71::/64", "exclude": ["fd00:cafe:71::150/128"] } + ] + } + } +--- +# App network for gw-v6a1 (IPv6-only, SLLBR ↔ app Pods) +apiVersion: k8s.cni.cncf.io/v1 +kind: NetworkAttachmentDefinition +metadata: + name: app-net-v6a1 +spec: + config: | + { + "cniVersion": "1.0.0", + "name": "app-net-v6a1", + "plugins": [ + { + "type": "macvlan", + "master": "eth0", + "mode": "bridge", + "ipam": { + "type": "whereabouts", + "ipRanges": [ + { "range": "fd00:cafe:170::/64" } + ] + } + } + ] + } +--- +# App network for gw-v6a2 (IPv6-only, separate from gw-v6a1) +apiVersion: k8s.cni.cncf.io/v1 +kind: NetworkAttachmentDefinition +metadata: + name: app-net-v6a2 +spec: + config: | + { + "cniVersion": "1.0.0", + "name": "app-net-v6a2", + "plugins": [ + { + "type": "macvlan", + "master": "eth0", + "mode": "bridge", + "ipam": { + "type": "whereabouts", + "ipRanges": [ + { "range": "fd00:cafe:170::/64" } + ] + } + } + ] + } diff --git a/test/e2e/suites/separate-appnetwork-v6/rbac.yaml b/test/e2e/suites/separate-appnetwork-v6/rbac.yaml new file mode 100644 index 00000000..213fe3cf --- /dev/null +++ b/test/e2e/suites/separate-appnetwork-v6/rbac.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: meridio-2-network-sidecar + namespace: e2e-separate-appnetwork-v6 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: meridio-2-network-sidecar + namespace: e2e-separate-appnetwork-v6 +rules: +- apiGroups: ["meridio-2.nordix.org"] + resources: ["endpointnetworkconfigurations"] + verbs: ["get", "list", "watch"] +- apiGroups: ["meridio-2.nordix.org"] + resources: ["endpointnetworkconfigurations/status"] + verbs: ["get", "update", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: meridio-2-network-sidecar + namespace: e2e-separate-appnetwork-v6 +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: meridio-2-network-sidecar +subjects: +- kind: ServiceAccount + name: meridio-2-network-sidecar + namespace: e2e-separate-appnetwork-v6 diff --git a/test/e2e/suites/separate-appnetwork-v6/routing.yaml b/test/e2e/suites/separate-appnetwork-v6/routing.yaml new file mode 100644 index 00000000..39ee3007 --- /dev/null +++ b/test/e2e/suites/separate-appnetwork-v6/routing.yaml @@ -0,0 +1,84 @@ +--- +# gw-v6a1 router (VLAN 1300, ASN 64522, IPv6-only) +apiVersion: meridio-2.nordix.org/v1alpha1 +kind: GatewayRouter +metadata: + name: gw-v6a1-router-v6 +spec: + gatewayRef: + name: gw-v6a1 + interface: "vlan-1300" + address: "fd00:cafe:70::150" + protocol: "BGP" + bgp: + localASN: 64522 + remoteASN: 4200000000 + localPort: 10179 + remotePort: 10179 + holdTime: 24s + bfd: + minTx: 300ms + minRx: 300ms + multiplier: 3 +--- +apiVersion: meridio-2.nordix.org/v1alpha1 +kind: L34Route +metadata: + name: route-v6a1 +spec: + parentRefs: + - name: gw-v6a1 + backendRefs: + - group: meridio-2.nordix.org + kind: DistributionGroup + name: dg-v6a1 + destinationCIDRs: + - "fd00:cafe:7::1/128" + protocols: + - TCP + - UDP + destinationPorts: + - "5000-5001" + priority: 100 +--- +# gw-v6a2 router (VLAN 1400, ASN 64523, IPv6-only) +apiVersion: meridio-2.nordix.org/v1alpha1 +kind: GatewayRouter +metadata: + name: gw-v6a2-router-v6 +spec: + gatewayRef: + name: gw-v6a2 + interface: "vlan-1400" + address: "fd00:cafe:71::150" + protocol: "BGP" + bgp: + localASN: 64523 + remoteASN: 4200000000 + localPort: 10179 + remotePort: 10179 + holdTime: 24s + bfd: + minTx: 300ms + minRx: 300ms + multiplier: 3 +--- +apiVersion: meridio-2.nordix.org/v1alpha1 +kind: L34Route +metadata: + name: route-v6a2 +spec: + parentRefs: + - name: gw-v6a2 + backendRefs: + - group: meridio-2.nordix.org + kind: DistributionGroup + name: dg-v6a2 + destinationCIDRs: + - "fd00:cafe:7::2/128" + protocols: + - TCP + - UDP + destinationPorts: + - "5000-5001" + priority: 100 diff --git a/test/e2e/suites/separate-appnetwork-v6/targets.yaml b/test/e2e/suites/separate-appnetwork-v6/targets.yaml new file mode 100644 index 00000000..e33fa6aa --- /dev/null +++ b/test/e2e/suites/separate-appnetwork-v6/targets.yaml @@ -0,0 +1,79 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: target-v6a + labels: + app: target-v6a +spec: + replicas: 2 + selector: + matchLabels: + app: target-v6a + template: + metadata: + labels: + app: target-v6a + annotations: + k8s.v1.cni.cncf.io/networks: app-net-v6a1, app-net-v6a2 + spec: + serviceAccountName: meridio-2-network-sidecar + volumes: + - name: restart-gate + emptyDir: {} + - name: sidecar-state + emptyDir: {} + containers: + - name: example-target + image: registry.nordix.org/cloud-native/meridio-2/example-target:latest + imagePullPolicy: IfNotPresent + command: ["/bin/bash", "-c"] + args: + - | + screen -d -m bash -c "./ctraffic -server -address [::]:5000" ; + screen -d -m bash -c "./ctraffic -server -udp -address [::]:5001" ; + trap 'exit 0' TERM; sleep infinity & wait + securityContext: + runAsNonRoot: true + capabilities: + drop: ["ALL"] + add: ["DAC_OVERRIDE", "NET_RAW", "SYS_PTRACE"] + - name: network-sidecar + image: registry.nordix.org/cloud-native/meridio-2/network-sidecar:latest + imagePullPolicy: IfNotPresent + command: ["/bin/sh", "-c"] + args: + - | + if [ -f /restart-gate/already-started ]; then + echo "Restart detected - waiting for release signal" + while [ ! -f /restart-gate/release-restart ]; do + sleep 1 + done + echo "Release signal received - proceeding with startup" + fi + touch /restart-gate/already-started + exec ./network-sidecar run --pod-name="$POD_NAME" --pod-namespace="$POD_NAMESPACE" --pod-uid="$POD_UID" --log-level=debug + volumeMounts: + - name: restart-gate + mountPath: /restart-gate + - name: sidecar-state + mountPath: /var/run/meridio + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_UID + valueFrom: + fieldRef: + fieldPath: metadata.uid + - name: MERIDIO_LOG_LEVEL_API + value: 127.0.0.1:9903 + securityContext: + capabilities: + drop: ["ALL"] + add: ["NET_ADMIN"]