Skip to content

Commit 93618ac

Browse files
committed
test: ensure LoadBalancer Service IP is reachable
1 parent ab8b39e commit 93618ac

File tree

3 files changed

+197
-6
lines changed

3 files changed

+197
-6
lines changed

test/e2e/config/caren.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ intervals:
226226
default/wait-deployment: ["10m", "10s"]
227227
default/wait-daemonset: [ "5m", "10s" ]
228228
default/wait-statefulset: [ "10m", "10s" ]
229+
default/wait-service: [ "10m", "10s" ]
229230
default/wait-clusterresourceset: [ "5m", "10s" ]
230231
default/wait-helmrelease: [ "5m", "10s" ]
231232
default/wait-resource: [ "5m", "10s" ]

test/e2e/quick_start_test.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,19 @@ var _ = Describe("Quick start", func() {
9696
)
9797
}
9898

99-
// For Nutanix provider, reserve an IP address for the workload cluster control plane endpoint -
100-
// remember to unreserve it!
99+
// For Nutanix provider, reserve an IP address for the workload cluster:
100+
// 1. control plane endpoint
101+
// 2. service load balancer
102+
// Remember to unreserve it after the test!
101103
if provider == "Nutanix" {
102-
By(
103-
"Reserving an IP address for the workload cluster control plane endpoint",
104-
)
105104
nutanixClient, err := nutanix.NewV4Client(
106105
nutanix.CredentialsFromCAPIE2EConfig(testE2EConfig),
107106
)
108107
Expect(err).ToNot(HaveOccurred())
109108

109+
By(
110+
"Reserving an IP address for the workload cluster control plane endpoint",
111+
)
110112
controlPlaneEndpointIP, unreserveControlPlaneEndpointIP, err := nutanix.ReserveIP(
111113
testE2EConfig.MustGetVariable("NUTANIX_SUBNET_NAME"),
112114
testE2EConfig.MustGetVariable(
@@ -117,6 +119,20 @@ var _ = Describe("Quick start", func() {
117119
Expect(err).ToNot(HaveOccurred())
118120
DeferCleanup(unreserveControlPlaneEndpointIP)
119121
testE2EConfig.Variables["CONTROL_PLANE_ENDPOINT_IP"] = controlPlaneEndpointIP
122+
123+
By(
124+
"Reserving an IP address for the workload cluster kubernetes Service load balancer",
125+
)
126+
kubernetesServiceLoadBalancerIP, unreservekubernetesServiceLoadBalancerIP, err := nutanix.ReserveIP(
127+
testE2EConfig.MustGetVariable("NUTANIX_SUBNET_NAME"),
128+
testE2EConfig.MustGetVariable(
129+
"NUTANIX_PRISM_ELEMENT_CLUSTER_NAME",
130+
),
131+
nutanixClient,
132+
)
133+
Expect(err).ToNot(HaveOccurred())
134+
DeferCleanup(unreservekubernetesServiceLoadBalancerIP)
135+
testE2EConfig.Variables["KUBERNETES_SERVICE_LOAD_BALANCER_IP"] = kubernetesServiceLoadBalancerIP
120136
}
121137

122138
clusterLocalTempDir, err := os.MkdirTemp("", "clusterctl-")
@@ -326,7 +342,6 @@ var _ = Describe("Quick start", func() {
326342
ClusterProxy: proxy,
327343
},
328344
)
329-
330345
EnsureAntiAffnityForRegistryAddon(
331346
ctx,
332347
EnsureAntiAffnityForRegistryAddonInput{
@@ -335,6 +350,21 @@ var _ = Describe("Quick start", func() {
335350
ClusterProxy: proxy,
336351
},
337352
)
353+
354+
// TODO: Test for other providers.
355+
if provider == "Nutanix" {
356+
EnsureLoadBalancerService(
357+
ctx,
358+
EnsureLoadBalancerServiceInput{
359+
WorkloadCluster: workloadCluster,
360+
ClusterProxy: proxy,
361+
ServciceIntervals: testE2EConfig.GetIntervals(
362+
flavor,
363+
"wait-service",
364+
),
365+
},
366+
)
367+
}
338368
},
339369
}
340370
})

test/e2e/serviceloadbalancer_helpers.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,20 @@ package e2e
88
import (
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

Comments
 (0)