@@ -8,11 +8,19 @@ package e2e
88import (
99 "context"
1010 "fmt"
11+ "io"
12+ "net/http"
13+ "net/url"
14+ "strings"
15+ "time"
1116
1217 . "github.com/onsi/ginkgo/v2"
1318 . "github.com/onsi/gomega"
1419 appsv1 "k8s.io/api/apps/v1"
20+ corev1 "k8s.io/api/core/v1"
1521 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+ "k8s.io/apimachinery/pkg/util/intstr"
23+ "k8s.io/utils/ptr"
1624 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1725 "sigs.k8s.io/cluster-api/test/framework"
1826 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -136,3 +144,158 @@ func waitForMetalLBServiceLoadBalancerToBeReadyInWorkloadCluster(
136144 Resources : resources ,
137145 }, input .resourceIntervals ... )
138146}
147+
148+ type EnsureLoadBalancerServiceInput struct {
149+ WorkloadCluster * clusterv1.Cluster
150+ ClusterProxy framework.ClusterProxy
151+ ServciceIntervals []interface {}
152+ }
153+
154+ // EnsureLoadBalancerService creates a test Service of type LoadBalancer and tests that the assigned IP responds.
155+ func EnsureLoadBalancerService (
156+ ctx context.Context ,
157+ input EnsureLoadBalancerServiceInput ,
158+ ) {
159+ workloadClusterClient := input .ClusterProxy .GetWorkloadCluster (
160+ ctx , input .WorkloadCluster .Namespace , input .WorkloadCluster .Name ,
161+ ).GetClient ()
162+
163+ svc := createTestService (ctx , workloadClusterClient , input .ServciceIntervals )
164+ url := "http://" + getLoadBalancerAddress (svc ) + "/clientip"
165+
166+ By ("Testing the LoadBalancer Service responds" )
167+ url := url.URL {
168+ Scheme : "http" ,
169+ Host : getLoadBalancerAddress (svc ),
170+ Path : "/clientip" ,
171+ }
172+ output := testServiceLoadBalancer (ctx , url , input .ServciceIntervals )
173+ Expect (output ).ToNot (BeEmpty ())
174+ }
175+
176+ func createTestService (
177+ ctx context.Context ,
178+ workloadClusterClient client.Client ,
179+ intervals []interface {},
180+ ) * corev1.Service {
181+ const (
182+ name = "echo"
183+ namespace = corev1 .NamespaceDefault
184+ appKey = "app"
185+ replicas = int32 (1 )
186+ image = "registry.k8s.io/e2e-test-images/agnhost:2.57"
187+
188+ port = 8080
189+ portName = "http"
190+ )
191+
192+ By ("Creating a test Deployment for LoadBalancer Service" )
193+ dep := & appsv1.Deployment {
194+ ObjectMeta : metav1.ObjectMeta {
195+ Name : name ,
196+ Namespace : namespace ,
197+ },
198+ Spec : appsv1.DeploymentSpec {
199+ Replicas : ptr .To (replicas ),
200+ Selector : & metav1.LabelSelector {
201+ MatchLabels : map [string ]string {appKey : name },
202+ },
203+ Template : corev1.PodTemplateSpec {
204+ ObjectMeta : metav1.ObjectMeta {
205+ Labels : map [string ]string {appKey : name },
206+ },
207+ Spec : corev1.PodSpec {
208+ Containers : []corev1.Container {{
209+ Name : name ,
210+ Image : image ,
211+ Args : []string {"netexec" , fmt .Sprintf ("--http-port=%d" , port )},
212+ Ports : []corev1.ContainerPort {{
213+ Name : portName ,
214+ ContainerPort : int32 (port ),
215+ }},
216+ }},
217+ },
218+ },
219+ },
220+ }
221+ if err := workloadClusterClient .Create (ctx , dep ); err != nil {
222+ Expect (err ).ToNot (HaveOccurred ())
223+ }
224+ By ("Waiting for Deployment to be ready" )
225+ Eventually (func (g Gomega ) {
226+ g .Expect (workloadClusterClient .Get (ctx , client .ObjectKeyFromObject (dep ), dep )).To (Succeed ())
227+ g .Expect (dep .Status .ReadyReplicas ).To (Equal (replicas ))
228+ }, intervals ... ).Should (Succeed (), "timed out waiting for Deployment to be ready" )
229+
230+ By ("Creating a test Service for LoadBalancer Service" )
231+ svc := & corev1.Service {
232+ ObjectMeta : metav1.ObjectMeta {
233+ Name : name ,
234+ Namespace : namespace ,
235+ },
236+ Spec : corev1.ServiceSpec {
237+ Type : corev1 .ServiceTypeLoadBalancer ,
238+ Selector : map [string ]string {appKey : name },
239+ Ports : []corev1.ServicePort {{
240+ Name : portName ,
241+ Port : 80 ,
242+ Protocol : corev1 .ProtocolTCP ,
243+ TargetPort : intstr .FromInt (port ),
244+ }},
245+ },
246+ }
247+ if err := workloadClusterClient .Create (ctx , svc ); err != nil {
248+ Expect (err ).ToNot (HaveOccurred ())
249+ }
250+
251+ key := client .ObjectKeyFromObject (svc )
252+ By ("Waiting for LoadBalacer IP/Hostname to be assigned" )
253+ Eventually (func (g Gomega ) {
254+ g .Expect (workloadClusterClient .Get (ctx , key , svc )).To (Succeed ())
255+
256+ ings := svc .Status .LoadBalancer .Ingress
257+ g .Expect (ings ).ToNot (BeEmpty (), "no LoadBalancer ingress yet" )
258+
259+ ip := ings [0 ].IP
260+ host := ings [0 ].Hostname
261+ g .Expect (ip == "" && host == "" ).To (BeFalse (), "ingress has neither IP nor Hostname yet" )
262+ }, intervals ... ).Should (Succeed (), "timed out waiting for LoadBalancer IP/hostname" )
263+
264+ return svc
265+ }
266+
267+ func getLoadBalancerAddress (svc * corev1.Service ) string {
268+ ings := svc .Status .LoadBalancer .Ingress
269+ if len (ings ) == 0 {
270+ return ""
271+ }
272+ address := ings [0 ].IP
273+ if address == "" {
274+ address = ings [0 ].Hostname
275+ }
276+ return address
277+ }
278+
279+ func testServiceLoadBalancer (
280+ ctx context.Context ,
281+ url url.URL ,
282+ intervals []interface {},
283+ ) string {
284+ hc := & http.Client {Timeout : 5 * time .Second }
285+ var output string
286+ Eventually (func (g Gomega ) string {
287+ req , _ := http .NewRequestWithContext (ctx , http .MethodGet , url .String (), http .NoBody )
288+ resp , err := hc .Do (req )
289+ if err != nil {
290+ return ""
291+ }
292+ defer resp .Body .Close ()
293+ if resp .StatusCode != http .StatusOK {
294+ return ""
295+ }
296+ b , _ := io .ReadAll (resp .Body )
297+ output = strings .TrimSpace (string (b ))
298+ return output
299+ }, intervals ... ).ShouldNot (BeEmpty (), "no response from service" )
300+ return output
301+ }
0 commit comments