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