@@ -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,154 @@ 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+
165+ By ("Testing the LoadBalancer Service responds" )
166+ getClientIPURL := & url.URL {
167+ Scheme : "http" ,
168+ Host : getLoadBalancerAddress (svc ),
169+ Path : "/clientip" ,
170+ }
171+ output := testServiceLoadBalancer (ctx , getClientIPURL , input .ServciceIntervals )
172+ Expect (output ).ToNot (BeEmpty ())
173+ }
174+
175+ func createTestService (
176+ ctx context.Context ,
177+ workloadClusterClient client.Client ,
178+ intervals []interface {},
179+ ) * corev1.Service {
180+ const (
181+ name = "echo"
182+ namespace = corev1 .NamespaceDefault
183+ appKey = "app"
184+ replicas = int32 (1 )
185+ image = "registry.k8s.io/e2e-test-images/agnhost:2.57"
186+ port = 8080
187+ portName = "http"
188+ )
189+
190+ By ("Creating a test Deployment for LoadBalancer Service" )
191+ deployment := & appsv1.Deployment {
192+ ObjectMeta : metav1.ObjectMeta {
193+ Name : name ,
194+ Namespace : namespace ,
195+ },
196+ Spec : appsv1.DeploymentSpec {
197+ Replicas : ptr .To (replicas ),
198+ Selector : & metav1.LabelSelector {
199+ MatchLabels : map [string ]string {appKey : name },
200+ },
201+ Template : corev1.PodTemplateSpec {
202+ ObjectMeta : metav1.ObjectMeta {
203+ Labels : map [string ]string {appKey : name },
204+ },
205+ Spec : corev1.PodSpec {
206+ Containers : []corev1.Container {{
207+ Name : name ,
208+ Image : image ,
209+ Args : []string {"netexec" , fmt .Sprintf ("--http-port=%d" , port )},
210+ Ports : []corev1.ContainerPort {{
211+ Name : portName ,
212+ ContainerPort : int32 (port ),
213+ }},
214+ }},
215+ },
216+ },
217+ },
218+ }
219+ if err := workloadClusterClient .Create (ctx , deployment ); err != nil {
220+ Expect (err ).ToNot (HaveOccurred ())
221+ }
222+ By ("Waiting for Deployment to be ready" )
223+ Eventually (func (g Gomega ) {
224+ g .Expect (workloadClusterClient .Get (ctx , client .ObjectKeyFromObject (deployment ), deployment )).To (Succeed ())
225+ g .Expect (deployment .Status .ReadyReplicas ).To (Equal (replicas ))
226+ }, intervals ... ).Should (Succeed (), "timed out waiting for Deployment to be ready" )
227+
228+ By ("Creating a test Service for LoadBalancer Service" )
229+ service := & corev1.Service {
230+ ObjectMeta : metav1.ObjectMeta {
231+ Name : name ,
232+ Namespace : namespace ,
233+ },
234+ Spec : corev1.ServiceSpec {
235+ Type : corev1 .ServiceTypeLoadBalancer ,
236+ Selector : map [string ]string {appKey : name },
237+ Ports : []corev1.ServicePort {{
238+ Name : portName ,
239+ Port : 80 ,
240+ Protocol : corev1 .ProtocolTCP ,
241+ TargetPort : intstr .FromInt (port ),
242+ }},
243+ },
244+ }
245+ if err := workloadClusterClient .Create (ctx , service ); err != nil {
246+ Expect (err ).ToNot (HaveOccurred ())
247+ }
248+ By ("Waiting for LoadBalacer IP/Hostname to be assigned" )
249+ Eventually (func (g Gomega ) {
250+ g .Expect (workloadClusterClient .Get (ctx , client .ObjectKeyFromObject (service ), service )).To (Succeed ())
251+
252+ ingress := service .Status .LoadBalancer .Ingress
253+ g .Expect (ingress ).ToNot (BeEmpty (), "no LoadBalancer ingress yet" )
254+
255+ ip := ingress [0 ].IP
256+ hostname := ingress [0 ].Hostname
257+ g .Expect (ip == "" && hostname == "" ).To (BeFalse (), "ingress has neither IP nor Hostname yet" )
258+ }, intervals ... ).Should (Succeed (), "timed out waiting for LoadBalancer IP/hostname" )
259+
260+ return service
261+ }
262+
263+ func getLoadBalancerAddress (svc * corev1.Service ) string {
264+ ings := svc .Status .LoadBalancer .Ingress
265+ if len (ings ) == 0 {
266+ return ""
267+ }
268+ address := ings [0 ].IP
269+ if address == "" {
270+ address = ings [0 ].Hostname
271+ }
272+ return address
273+ }
274+
275+ func testServiceLoadBalancer (
276+ ctx context.Context ,
277+ requestURL * url.URL ,
278+ intervals []interface {},
279+ ) string {
280+ hc := & http.Client {Timeout : 5 * time .Second }
281+ var output string
282+ Eventually (func (g Gomega ) string {
283+ req , _ := http .NewRequestWithContext (ctx , http .MethodGet , requestURL .String (), http .NoBody )
284+ resp , err := hc .Do (req )
285+ if err != nil {
286+ return ""
287+ }
288+ defer resp .Body .Close ()
289+ if resp .StatusCode != http .StatusOK {
290+ return ""
291+ }
292+ b , _ := io .ReadAll (resp .Body )
293+ output = strings .TrimSpace (string (b ))
294+ return output
295+ }, intervals ... ).ShouldNot (BeEmpty (), "no response from service" )
296+ return output
297+ }
0 commit comments