Skip to content

Commit e4f0a5a

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

File tree

3 files changed

+194
-10
lines changed

3 files changed

+194
-10
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: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,27 +96,37 @@ 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())
108+
subnetName := testE2EConfig.MustGetVariable("NUTANIX_SUBNET_NAME")
109+
prismElementClusterName := testE2EConfig.MustGetVariable("NUTANIX_PRISM_ELEMENT_CLUSTER_NAME")
109110

111+
By("Reserving an IP address for the workload cluster control plane endpoint")
110112
controlPlaneEndpointIP, unreserveControlPlaneEndpointIP, err := nutanix.ReserveIP(
111-
testE2EConfig.MustGetVariable("NUTANIX_SUBNET_NAME"),
112-
testE2EConfig.MustGetVariable(
113-
"NUTANIX_PRISM_ELEMENT_CLUSTER_NAME",
114-
),
113+
subnetName,
114+
prismElementClusterName,
115115
nutanixClient,
116116
)
117117
Expect(err).ToNot(HaveOccurred())
118118
DeferCleanup(unreserveControlPlaneEndpointIP)
119119
testE2EConfig.Variables["CONTROL_PLANE_ENDPOINT_IP"] = controlPlaneEndpointIP
120+
121+
By("Reserving an IP address for the workload cluster kubernetes Service load balancer")
122+
kubernetesServiceLoadBalancerIP, unreservekubernetesServiceLoadBalancerIP, err := nutanix.ReserveIP(
123+
subnetName,
124+
prismElementClusterName,
125+
nutanixClient,
126+
)
127+
Expect(err).ToNot(HaveOccurred())
128+
DeferCleanup(unreservekubernetesServiceLoadBalancerIP)
129+
testE2EConfig.Variables["KUBERNETES_SERVICE_LOAD_BALANCER_IP"] = kubernetesServiceLoadBalancerIP
120130
}
121131

122132
clusterLocalTempDir, err := os.MkdirTemp("", "clusterctl-")
@@ -326,7 +336,6 @@ var _ = Describe("Quick start", func() {
326336
ClusterProxy: proxy,
327337
},
328338
)
329-
330339
EnsureAntiAffnityForRegistryAddon(
331340
ctx,
332341
EnsureAntiAffnityForRegistryAddonInput{
@@ -335,6 +344,21 @@ var _ = Describe("Quick start", func() {
335344
ClusterProxy: proxy,
336345
},
337346
)
347+
348+
// TODO: Test for other providers.
349+
if provider == "Nutanix" {
350+
EnsureLoadBalancerService(
351+
ctx,
352+
EnsureLoadBalancerServiceInput{
353+
WorkloadCluster: workloadCluster,
354+
ClusterProxy: proxy,
355+
ServciceIntervals: testE2EConfig.GetIntervals(
356+
flavor,
357+
"wait-service",
358+
),
359+
},
360+
)
361+
}
338362
},
339363
}
340364
})

test/e2e/serviceloadbalancer_helpers.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,19 @@ 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"
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

Comments
 (0)