Skip to content

Commit a0b54cd

Browse files
committed
Merge remote-tracking branch 'origin/master' into feat/webhook-adc-validation
2 parents 70b6a7c + 23ee25f commit a0b54cd

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
@@ -38,23 +38,30 @@ import (
3838
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3939
)
4040

41+
const defaultDashboardVersion = "dev"
42+
4143
var (
4244
API7EELicense string
4345

4446
dashboardVersion string
4547
)
4648

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

5457
dashboardVersion = os.Getenv("DASHBOARD_VERSION")
5558
if dashboardVersion == "" {
56-
dashboardVersion = "dev"
59+
dashboardVersion = defaultDashboardVersion
5760
}
61+
}
62+
63+
func (f *Framework) BeforeSuite() {
64+
initSuiteEnv()
5865

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

@@ -87,6 +94,61 @@ func (f *Framework) AfterSuite() {
8794
f.shutdownDashboardTunnel()
8895
}
8996

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

244+
// dashboardLocalPorts returns the local port pair to use for the dashboard HTTP
245+
// and HTTPS tunnels. Each ginkgo parallel process gets a unique, non-overlapping
246+
// range based on its 1-indexed process number, eliminating port conflicts without
247+
// any TOCTOU race.
248+
//
249+
// Formula: base = 18000 + node*100
250+
//
251+
// node=1 → 18100 (HTTP) / 18101 (HTTPS)
252+
// node=2 → 18200 (HTTP) / 18201 (HTTPS)
253+
func dashboardLocalPorts() (httpLocal, httpsLocal int) {
254+
node := GinkgoParallelProcess() // 1-indexed
255+
base := 18000 + node*100
256+
return base, base + 1
257+
}
258+
182259
func (f *Framework) newDashboardTunnel() error {
183260
var (
184-
httpNodePort int
185-
httpsNodePort int
186-
httpPort int
187-
httpsPort int
261+
httpPort int
262+
httpsPort int
188263
)
189264

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

192267
for _, port := range service.Spec.Ports {
193268
switch port.Name {
194269
case "http":
195-
httpNodePort = int(port.NodePort)
196270
httpPort = int(port.Port)
197271
case "https":
198-
httpsNodePort = int(port.NodePort)
199272
httpsPort = int(port.Port)
200273
}
201274
}
202275

276+
httpLocal, httpsLocal := dashboardLocalPorts()
203277
_dashboardHTTPTunnel = k8s.NewTunnel(f.kubectlOpts, k8s.ResourceTypeService, "api7ee3-dashboard",
204-
httpNodePort, httpPort)
278+
httpLocal, httpPort)
205279
_dashboardHTTPSTunnel = k8s.NewTunnel(f.kubectlOpts, k8s.ResourceTypeService, "api7ee3-dashboard",
206-
httpsNodePort, httpsPort)
280+
httpsLocal, httpsPort)
207281

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

0 commit comments

Comments
 (0)