@@ -8,11 +8,20 @@ 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"
19+ "golang.org/x/sys/windows/svc"
1420 appsv1 "k8s.io/api/apps/v1"
21+ corev1 "k8s.io/api/core/v1"
1522 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+ "k8s.io/apimachinery/pkg/util/intstr"
24+ "k8s.io/utils/ptr"
1625 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1726 "sigs.k8s.io/cluster-api/test/framework"
1827 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -136,3 +145,154 @@ func waitForMetalLBServiceLoadBalancerToBeReadyInWorkloadCluster(
136145 Resources : resources ,
137146 }, input .resourceIntervals ... )
138147}
148+
149+ type EnsureLoadBalancerServiceInput struct {
150+ WorkloadCluster * clusterv1.Cluster
151+ ClusterProxy framework.ClusterProxy
152+ ServciceIntervals []interface {}
153+ }
154+
155+ // EnsureLoadBalancerService creates a test Service of type LoadBalancer and tests that the assigned IP responds.
156+ func EnsureLoadBalancerService (
157+ ctx context.Context ,
158+ input EnsureLoadBalancerServiceInput ,
159+ ) {
160+ workloadClusterClient := input .ClusterProxy .GetWorkloadCluster (
161+ ctx , input .WorkloadCluster .Namespace , input .WorkloadCluster .Name ,
162+ ).GetClient ()
163+
164+ svc := createTestService (ctx , workloadClusterClient , input .ServciceIntervals )
165+
166+ By ("Testing the LoadBalancer Service responds" )
167+ getClientIPURL := & url.URL {
168+ Scheme : "http" ,
169+ Host : getLoadBalancerAddress (svc ),
170+ Path : "/clientip" ,
171+ }
172+ output := testServiceLoadBalancer (ctx , getClientIPURL , 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+ port = 8080
188+ portName = "http"
189+ )
190+
191+ By ("Creating a test Deployment for LoadBalancer Service" )
192+ deployment := & 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 , deployment ); 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 (deployment ), depdeployment )).To (Succeed ())
226+ g .Expect (deployment .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+ service := & 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 , service ); err != nil {
247+ Expect (err ).ToNot (HaveOccurred ())
248+ }
249+ By ("Waiting for LoadBalacer IP/Hostname to be assigned" )
250+ Eventually (func (g Gomega ) {
251+ g .Expect (workloadClusterClient .Get (ctx , client .ObjectKeyFromObject (service ), service )).To (Succeed ())
252+
253+ ings := svc .Status .LoadBalancer .Ingress
254+ g .Expect (ings ).ToNot (BeEmpty (), "no LoadBalancer ingress yet" )
255+
256+ ip := ings [0 ].IP
257+ host := ings [0 ].Hostname
258+ g .Expect (ip == "" && host == "" ).To (BeFalse (), "ingress has neither IP nor Hostname yet" )
259+ }, intervals ... ).Should (Succeed (), "timed out waiting for LoadBalancer IP/hostname" )
260+
261+ return service
262+ }
263+
264+ func getLoadBalancerAddress (svc * corev1.Service ) string {
265+ ings := svc .Status .LoadBalancer .Ingress
266+ if len (ings ) == 0 {
267+ return ""
268+ }
269+ address := ings [0 ].IP
270+ if address == "" {
271+ address = ings [0 ].Hostname
272+ }
273+ return address
274+ }
275+
276+ func testServiceLoadBalancer (
277+ ctx context.Context ,
278+ requestURL * url.URL ,
279+ intervals []interface {},
280+ ) string {
281+ hc := & http.Client {Timeout : 5 * time .Second }
282+ var output string
283+ Eventually (func (g Gomega ) string {
284+ req , _ := http .NewRequestWithContext (ctx , http .MethodGet , requestURL .String (), http .NoBody )
285+ resp , err := hc .Do (req )
286+ if err != nil {
287+ return ""
288+ }
289+ defer resp .Body .Close ()
290+ if resp .StatusCode != http .StatusOK {
291+ return ""
292+ }
293+ b , _ := io .ReadAll (resp .Body )
294+ output = strings .TrimSpace (string (b ))
295+ return output
296+ }, intervals ... ).ShouldNot (BeEmpty (), "no response from service" )
297+ return output
298+ }
0 commit comments