Skip to content

Commit 23ee25f

Browse files
authored
feat: enable ginkgo parallel execution for API7EE E2E tests (#404)
1 parent 39e1c91 commit 23ee25f

4 files changed

Lines changed: 109 additions & 15 deletions

File tree

.github/workflows/e2e-test.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ jobs:
6161
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
6262
chmod 700 get_helm.sh
6363
./get_helm.sh
64+
65+
- name: Install ginkgo
66+
run: make install-ginkgo
67+
6468
- name: Login to Registry
6569
uses: docker/login-action@v3
6670
with:
@@ -121,4 +125,8 @@ jobs:
121125
TEST_LABEL: ${{ matrix.cases_subset }}
122126
TEST_ENV: CI
123127
run: |
124-
make e2e-test
128+
if [[ "${{ matrix.cases_subset }}" == "webhook" ]]; then
129+
E2E_NODES=1 make ginkgo-api7ee-e2e-test
130+
else
131+
E2E_NODES=4 make ginkgo-api7ee-e2e-test
132+
fi

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ ADC_VERSION ?= 0.22.1
3434

3535
DIR := $(shell pwd)
3636

37-
GINKGO_VERSION ?= 2.20.0
37+
GINKGO_VERSION ?= 2.22.0
3838
TEST_TIMEOUT ?= 80m
3939
TEST_DIR ?= ./test/e2e/
4040
E2E_NODES ?= 4
@@ -154,6 +154,13 @@ download-api7ee3-chart:
154154
ginkgo-e2e-test: adc
155155
@ginkgo -cover -coverprofile=coverage.txt -r --randomize-all --randomize-suites --trace --focus=$(E2E_FOCUS) --nodes=$(E2E_NODES) --label-filter="$(TEST_LABEL)" $(TEST_DIR)
156156

157+
.PHONY: ginkgo-api7ee-e2e-test
158+
ginkgo-api7ee-e2e-test: adc
159+
@DASHBOARD_VERSION=$(DASHBOARD_VERSION) ginkgo -cover -coverprofile=coverage.txt \
160+
--randomize-all --randomize-suites --trace \
161+
--timeout=$(TEST_TIMEOUT) --nodes=$(E2E_NODES) \
162+
--label-filter="$(TEST_LABEL)" ./test/e2e/
163+
157164
.PHONY: install-ginkgo
158165
install-ginkgo:
159166
@go install github.com/onsi/ginkgo/v2/ginkgo@v$(GINKGO_VERSION)

test/e2e/e2e_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,13 @@ func TestE2E(t *testing.T) {
4242
// init newDeployer function
4343
scaffold.NewDeployer = scaffold.NewAPI7Deployer
4444

45-
BeforeSuite(f.BeforeSuite)
46-
AfterSuite(f.AfterSuite)
45+
// DeployAPI7EE runs only on ginkgo node 1 and deploys the shared API7EE control plane.
46+
// InitNodeConnections runs on every node to set up per-node dashboard connections.
47+
SynchronizedBeforeSuite(f.DeployAPI7EE, f.InitNodeConnections)
48+
49+
// CloseNodeConnections runs on every node to close per-node dashboard tunnels.
50+
// TeardownInfrastructure runs only on node 1 for any suite-level cleanup.
51+
SynchronizedAfterSuite(f.CloseNodeConnections, f.TeardownInfrastructure)
4752

4853
_, _ = fmt.Fprintf(GinkgoWriter, "Starting apisix-ingress suite\n")
4954
RunSpecs(t, "e2e suite")

test/e2e/framework/api7_framework.go

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,30 @@ import (
3737
k8serrors "k8s.io/apimachinery/pkg/api/errors"
3838
)
3939

40+
const defaultDashboardVersion = "dev"
41+
4042
var (
4143
API7EELicense string
4244

4345
dashboardVersion string
4446
)
4547

46-
func (f *Framework) BeforeSuite() {
47-
// init license and dashboard version
48+
// initSuiteEnv reads required environment variables and panics early with a clear
49+
// message if any mandatory variable is missing.
50+
func initSuiteEnv() {
4851
API7EELicense = os.Getenv("API7_EE_LICENSE")
4952
if API7EELicense == "" {
5053
panic("env {API7_EE_LICENSE} is required")
5154
}
5255

5356
dashboardVersion = os.Getenv("DASHBOARD_VERSION")
5457
if dashboardVersion == "" {
55-
dashboardVersion = "dev"
58+
dashboardVersion = defaultDashboardVersion
5659
}
60+
}
61+
62+
func (f *Framework) BeforeSuite() {
63+
initSuiteEnv()
5764

5865
_ = k8s.DeleteNamespaceE(GinkgoT(), f.kubectlOpts, _namespace)
5966

@@ -83,6 +90,61 @@ func (f *Framework) AfterSuite() {
8390
f.shutdownDashboardTunnel()
8491
}
8592

93+
// DeployAPI7EE deploys the API7EE control plane once (runs on ginkgo node 1 only).
94+
// It returns a ready signal consumed by InitNodeConnections on all nodes.
95+
func (f *Framework) DeployAPI7EE() []byte {
96+
initSuiteEnv()
97+
98+
_ = k8s.DeleteNamespaceE(GinkgoT(), f.kubectlOpts, _namespace)
99+
100+
Eventually(func() error {
101+
_, err := k8s.GetNamespaceE(GinkgoT(), f.kubectlOpts, _namespace)
102+
if k8serrors.IsNotFound(err) {
103+
return nil
104+
}
105+
return fmt.Errorf("namespace %s still exists", _namespace)
106+
}, "1m", "2s").Should(Succeed())
107+
108+
k8s.CreateNamespace(GinkgoT(), f.kubectlOpts, _namespace)
109+
110+
f.DeployComponents()
111+
112+
time.Sleep(1 * time.Minute)
113+
114+
// Create a temporary tunnel for one-time setup operations.
115+
// Each node will create its own persistent tunnel in InitNodeConnections.
116+
err := f.newDashboardTunnel()
117+
Expect(err).ShouldNot(HaveOccurred(), "creating temporary dashboard tunnel")
118+
f.Logf("Temporary dashboard tunnel: %s", _dashboardHTTPTunnel.Endpoint())
119+
120+
f.UploadLicense()
121+
f.setDpManagerEndpoints()
122+
123+
// Close the temporary tunnel; each node creates its own in InitNodeConnections.
124+
f.shutdownDashboardTunnel()
125+
126+
return []byte("ready")
127+
}
128+
129+
// InitNodeConnections initializes per-node connections to the shared API7EE control plane.
130+
// It runs on every ginkgo parallel node after DeployAPI7EE completes.
131+
func (f *Framework) InitNodeConnections(_ []byte) {
132+
initSuiteEnv()
133+
134+
err := f.newDashboardTunnel()
135+
Expect(err).ShouldNot(HaveOccurred(), "creating dashboard tunnel for node")
136+
f.Logf("Dashboard HTTP Tunnel: %s", _dashboardHTTPTunnel.Endpoint())
137+
}
138+
139+
// CloseNodeConnections closes per-node connections. Runs on every ginkgo parallel node.
140+
func (f *Framework) CloseNodeConnections() {
141+
f.shutdownDashboardTunnel()
142+
}
143+
144+
// TeardownInfrastructure cleans up suite-level resources. Runs on ginkgo node 1 only.
145+
// The Kind cluster is deleted by CI after the job, so this is a no-op.
146+
func (f *Framework) TeardownInfrastructure() {}
147+
86148
// DeployComponents deploy necessary components
87149
func (f *Framework) DeployComponents() {
88150
f.deploy()
@@ -167,31 +229,43 @@ var (
167229
_dashboardHTTPSTunnel *k8s.Tunnel
168230
)
169231

232+
// dashboardLocalPorts returns the local port pair to use for the dashboard HTTP
233+
// and HTTPS tunnels. Each ginkgo parallel process gets a unique, non-overlapping
234+
// range based on its 1-indexed process number, eliminating port conflicts without
235+
// any TOCTOU race.
236+
//
237+
// Formula: base = 18000 + node*100
238+
//
239+
// node=1 → 18100 (HTTP) / 18101 (HTTPS)
240+
// node=2 → 18200 (HTTP) / 18201 (HTTPS)
241+
func dashboardLocalPorts() (httpLocal, httpsLocal int) {
242+
node := GinkgoParallelProcess() // 1-indexed
243+
base := 18000 + node*100
244+
return base, base + 1
245+
}
246+
170247
func (f *Framework) newDashboardTunnel() error {
171248
var (
172-
httpNodePort int
173-
httpsNodePort int
174-
httpPort int
175-
httpsPort int
249+
httpPort int
250+
httpsPort int
176251
)
177252

178253
service := k8s.GetService(f.GinkgoT, f.kubectlOpts, "api7ee3-dashboard")
179254

180255
for _, port := range service.Spec.Ports {
181256
switch port.Name {
182257
case "http":
183-
httpNodePort = int(port.NodePort)
184258
httpPort = int(port.Port)
185259
case "https":
186-
httpsNodePort = int(port.NodePort)
187260
httpsPort = int(port.Port)
188261
}
189262
}
190263

264+
httpLocal, httpsLocal := dashboardLocalPorts()
191265
_dashboardHTTPTunnel = k8s.NewTunnel(f.kubectlOpts, k8s.ResourceTypeService, "api7ee3-dashboard",
192-
httpNodePort, httpPort)
266+
httpLocal, httpPort)
193267
_dashboardHTTPSTunnel = k8s.NewTunnel(f.kubectlOpts, k8s.ResourceTypeService, "api7ee3-dashboard",
194-
httpsNodePort, httpsPort)
268+
httpsLocal, httpsPort)
195269

196270
if err := _dashboardHTTPTunnel.ForwardPortE(f.GinkgoT); err != nil {
197271
return err

0 commit comments

Comments
 (0)