@@ -38,23 +38,30 @@ import (
3838 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3939)
4040
41+ const defaultDashboardVersion = "dev"
42+
4143var (
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
91153func (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+
182259func (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