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