@@ -37,23 +37,30 @@ import (
3737 k8serrors "k8s.io/apimachinery/pkg/api/errors"
3838)
3939
40+ const defaultDashboardVersion = "dev"
41+
4042var (
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
87149func (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+
170247func (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