Skip to content

Commit 6211aec

Browse files
committed
minikube: Add integration test for metallb addon
Signed-off-by: Kartik Joshi <karikjoshi21@gmail.com>
1 parent e4c8917 commit 6211aec

File tree

1 file changed

+110
-0
lines changed

1 file changed

+110
-0
lines changed

test/integration/addons_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ func TestAddons(t *testing.T) {
153153
{"NvidiaDevicePlugin", validateNvidiaDevicePlugin},
154154
{"Yakd", validateYakdAddon},
155155
{"AmdGpuDevicePlugin", validateAmdGpuDevicePlugin},
156+
{"MetalLB", validateMetalLBAddon},
156157
}
157158

158159
for _, tc := range tests {
@@ -189,6 +190,115 @@ func TestAddons(t *testing.T) {
189190
})
190191
}
191192

193+
// validateMetalLBAddon tests MetalLB by exposing a tiny HTTP pod via a LoadBalancer
194+
// and verifying it receives an external IP and is reachable from the host.
195+
func validateMetalLBAddon(ctx context.Context, t *testing.T, profile string) {
196+
// MetalLB exercises host<->cluster L2/L3 path; skip where that’s not viable.
197+
if NoneDriver() {
198+
t.Skipf("skipping: metallb not supported on 'none' driver in this test")
199+
}
200+
if NeedsPortForward() {
201+
t.Skipf("skipping metallb test: environment requires port-forwarding")
202+
}
203+
defer disableAddon(t, "metallb", profile)
204+
defer PostMortemLogs(t, profile)
205+
206+
client, err := kapi.Client(profile)
207+
if err != nil {
208+
t.Fatalf("failed to get Kubernetes client: %v", err)
209+
}
210+
211+
// Enable MetalLB
212+
if rr, err := Run(t, exec.CommandContext(ctx, Target(), "addons", "enable", "metallb", "-p", profile, "--alsologtostderr", "-v=1")); err != nil {
213+
t.Fatalf("failed to enable metallb addon: args %q : %v", rr.Command(), err)
214+
}
215+
216+
// Wait for controller to stabilize. Name differs across versions:
217+
// try "controller" first, fall back to "metallb-controller".
218+
start := time.Now()
219+
waitController := func(name string) error {
220+
return kapi.WaitForDeploymentToStabilize(client, "metallb-system", name, Minutes(6))
221+
}
222+
if err := waitController("controller"); err != nil {
223+
t.Logf("metallb deployment 'controller' not ready (%v); trying 'metallb-controller'", err)
224+
if err2 := waitController("metallb-controller"); err2 != nil {
225+
t.Fatalf("metallb controller failed to stabilize: %v / %v", err, err2)
226+
}
227+
}
228+
t.Logf("metallb controller stabilized in %s", time.Since(start))
229+
230+
// Create a tiny HTTP server pod (busybox httpd) with a unique label to avoid collisions.
231+
// We avoid YAML here to keep the patch small and independent of other tests' resources.
232+
if rr, err := Run(t, exec.CommandContext(
233+
ctx, "kubectl", "--context", profile,
234+
"run", "mlb-http",
235+
"--image=gcr.io/k8s-minikube/busybox",
236+
"--labels", "app=mlb-http",
237+
"--restart=Never",
238+
"--command", "--", "sh", "-c", "httpd -f -p 80")); err != nil {
239+
t.Fatalf("failed to create mlb-http pod: args %q : %v", rr.Command(), err)
240+
}
241+
242+
if _, err := PodWait(ctx, t, profile, "default", "app=mlb-http", Minutes(6)); err != nil {
243+
t.Fatalf("failed waiting for mlb-http pod: %v", err)
244+
}
245+
246+
// Expose it as a LoadBalancer service.
247+
if rr, err := Run(t, exec.CommandContext(
248+
ctx, "kubectl", "--context", profile,
249+
"expose", "pod", "mlb-http",
250+
"--name", "mlb-http-lb",
251+
"--type", "LoadBalancer",
252+
"--port", "80",
253+
"--target-port", "80")); err != nil {
254+
t.Fatalf("failed to expose mlb-http-lb service: args %q : %v", rr.Command(), err)
255+
}
256+
t.Cleanup(func() {
257+
// best-effort cleanup
258+
Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "delete", "svc", "mlb-http-lb", "--now"))
259+
Run(t, exec.CommandContext(ctx, "kubectl", "--context", profile, "delete", "pod", "mlb-http", "--now"))
260+
})
261+
262+
// Wait for an external IP from MetalLB.
263+
var lbIP string
264+
waitExternalIP := func() error {
265+
rr, err := Run(t, exec.CommandContext(
266+
ctx, "kubectl", "--context", profile,
267+
"-n", "default", "get", "svc", "mlb-http-lb",
268+
"-o", "jsonpath={.status.loadBalancer.ingress[0].ip}"))
269+
if err != nil {
270+
return err
271+
}
272+
ip := strings.TrimSpace(rr.Stdout.String())
273+
if ip == "" || strings.EqualFold(ip, "<pending>") {
274+
return fmt.Errorf("external IP not allocated yet")
275+
}
276+
lbIP = ip
277+
return nil
278+
}
279+
if err := retry.Expo(waitExternalIP, 2*time.Second, Minutes(5)); err != nil {
280+
t.Fatalf("failed waiting for LoadBalancer external IP: %v", err)
281+
}
282+
t.Logf("mlb-http-lb external IP: %s", lbIP)
283+
284+
// Verify it serves HTTP 200 from the host.
285+
endpoint := fmt.Sprintf("http://%s", lbIP)
286+
checkLB := func() error {
287+
resp, err := retryablehttp.Get(endpoint)
288+
if err != nil {
289+
return err
290+
}
291+
if resp.StatusCode != http.StatusOK {
292+
return fmt.Errorf("%s = status %d, want 200", endpoint, resp.StatusCode)
293+
}
294+
return nil
295+
}
296+
if err := retry.Expo(checkLB, 500*time.Millisecond, Minutes(3)); err != nil {
297+
t.Errorf("failed reaching LoadBalancer %s: %v", endpoint, err)
298+
}
299+
}
300+
301+
192302
// validateIngressAddon tests the ingress addon by deploying a default nginx pod
193303
func validateIngressAddon(ctx context.Context, t *testing.T, profile string) {
194304
if NoneDriver() {

0 commit comments

Comments
 (0)