2023-06-09 10:59:19 -04:00
|
|
|
//go:build e2e
|
2022-12-21 04:49:21 -05:00
|
|
|
|
|
|
|
/*
|
|
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
2023-01-19 09:57:50 -05:00
|
|
|
// End-to-end tests for our cloud load balancer functionality.
|
2023-06-09 10:59:19 -04:00
|
|
|
package lb
|
2022-12-21 04:49:21 -05:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2023-02-28 09:19:12 -05:00
|
|
|
"bytes"
|
2022-12-21 04:49:21 -05:00
|
|
|
"context"
|
|
|
|
"fmt"
|
2023-02-28 09:19:12 -05:00
|
|
|
"io"
|
2022-12-21 04:49:21 -05:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/edgelesssys/constellation/v2/e2e/internal/kubectl"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
coreV1 "k8s.io/api/core/v1"
|
2023-06-09 10:59:19 -04:00
|
|
|
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
2022-12-21 04:49:21 -05:00
|
|
|
"k8s.io/client-go/kubernetes"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
namespaceName = "lb-test"
|
|
|
|
serviceName = "whoami"
|
|
|
|
initialPort = int32(8080)
|
|
|
|
newPort = int32(8044)
|
|
|
|
numRequests = 256
|
|
|
|
numPods = 3
|
2023-01-03 06:41:46 -05:00
|
|
|
timeout = time.Minute * 15
|
2022-12-21 04:49:21 -05:00
|
|
|
interval = time.Second * 5
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestLoadBalancer(t *testing.T) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
k, err := kubectl.New()
|
|
|
|
require.NoError(err)
|
|
|
|
|
2023-02-28 09:19:12 -05:00
|
|
|
t.Cleanup(func() {
|
|
|
|
gatherDebugInfo(t, k)
|
|
|
|
})
|
|
|
|
|
2023-01-05 08:12:53 -05:00
|
|
|
t.Log("Waiting for external IP to be registered")
|
2022-12-21 04:49:21 -05:00
|
|
|
svc := testEventuallyExternalIPAvailable(t, k)
|
2022-12-29 06:09:40 -05:00
|
|
|
loadBalancerIP := getIPOrHostname(t, svc)
|
2022-12-21 04:49:21 -05:00
|
|
|
loadBalancerPort := svc.Spec.Ports[0].Port
|
|
|
|
require.Equal(initialPort, loadBalancerPort)
|
|
|
|
url := buildURL(t, loadBalancerIP, loadBalancerPort)
|
2023-01-05 08:12:53 -05:00
|
|
|
|
|
|
|
t.Log("Checking service can be reached through LB")
|
2022-12-21 04:49:21 -05:00
|
|
|
testEventuallyStatusOK(t, url)
|
|
|
|
|
2023-01-05 08:12:53 -05:00
|
|
|
t.Log("Check that all pods receive traffic")
|
2022-12-21 04:49:21 -05:00
|
|
|
var allHostnames []string
|
|
|
|
for i := 0; i < numRequests; i++ {
|
2023-02-28 09:19:12 -05:00
|
|
|
allHostnames = testEndpointAvailable(t, url, allHostnames, i)
|
2022-12-21 04:49:21 -05:00
|
|
|
}
|
|
|
|
assert.True(hasNUniqueStrings(allHostnames, numPods))
|
|
|
|
allHostnames = allHostnames[:0]
|
|
|
|
|
2023-01-05 08:12:53 -05:00
|
|
|
t.Log("Change port of service to 8044")
|
2022-12-21 04:49:21 -05:00
|
|
|
svc.Spec.Ports[0].Port = newPort
|
2023-06-09 10:59:19 -04:00
|
|
|
svc, err = k.CoreV1().Services(namespaceName).Update(context.Background(), svc, metaV1.UpdateOptions{})
|
2022-12-21 04:49:21 -05:00
|
|
|
require.NoError(err)
|
|
|
|
assert.Equal(newPort, svc.Spec.Ports[0].Port)
|
|
|
|
|
2023-01-05 08:12:53 -05:00
|
|
|
t.Log("Wait for changed port to be available")
|
2022-12-21 04:49:21 -05:00
|
|
|
newURL := buildURL(t, loadBalancerIP, newPort)
|
|
|
|
testEventuallyStatusOK(t, newURL)
|
|
|
|
|
2023-01-05 08:12:53 -05:00
|
|
|
t.Log("Check again that all pods receive traffic")
|
2022-12-21 04:49:21 -05:00
|
|
|
for i := 0; i < numRequests; i++ {
|
2023-02-28 09:19:12 -05:00
|
|
|
allHostnames = testEndpointAvailable(t, newURL, allHostnames, i)
|
2022-12-21 04:49:21 -05:00
|
|
|
}
|
|
|
|
assert.True(hasNUniqueStrings(allHostnames, numPods))
|
|
|
|
}
|
|
|
|
|
2023-06-09 10:59:19 -04:00
|
|
|
func gatherDebugInfo(t *testing.T, k *kubernetes.Clientset) {
|
|
|
|
// Do not gather additional information on success
|
|
|
|
if !t.Failed() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Log("Gathering additional debug information.")
|
|
|
|
|
|
|
|
pods, err := k.CoreV1().Pods(namespaceName).List(context.Background(), metaV1.ListOptions{
|
|
|
|
LabelSelector: "app=whoami",
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
t.Logf("listing pods: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for idx := range pods.Items {
|
|
|
|
pod := pods.Items[idx]
|
|
|
|
req := k.CoreV1().Pods(namespaceName).GetLogs(pod.Name, &coreV1.PodLogOptions{
|
|
|
|
LimitBytes: func() *int64 { i := int64(1024 * 1024); return &i }(),
|
|
|
|
})
|
|
|
|
logs, err := req.Stream(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
t.Logf("fetching logs: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer logs.Close()
|
|
|
|
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
_, err = io.Copy(buf, logs)
|
|
|
|
if err != nil {
|
|
|
|
t.Logf("copying logs: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
t.Logf("Logs of pod '%s':\n%s\n\n", pod.Name, buf)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-29 06:09:40 -05:00
|
|
|
func getIPOrHostname(t *testing.T, svc *coreV1.Service) string {
|
|
|
|
t.Helper()
|
|
|
|
if ip := svc.Status.LoadBalancer.Ingress[0].IP; ip != "" {
|
|
|
|
return ip
|
|
|
|
}
|
|
|
|
return svc.Status.LoadBalancer.Ingress[0].Hostname
|
|
|
|
}
|
|
|
|
|
2022-12-21 04:49:21 -05:00
|
|
|
func hasNUniqueStrings(elements []string, n int) bool {
|
|
|
|
m := make(map[string]bool)
|
|
|
|
for i := range elements {
|
|
|
|
m[elements[i]] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
numKeys := 0
|
|
|
|
for range m {
|
|
|
|
numKeys++
|
|
|
|
}
|
|
|
|
return numKeys == n
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildURL(t *testing.T, ip string, port int32) string {
|
|
|
|
t.Helper()
|
|
|
|
return fmt.Sprintf("http://%s:%d", ip, port)
|
|
|
|
}
|
|
|
|
|
|
|
|
// testEventuallyStatusOK tests that the URL response with StatusOK within 5min.
|
|
|
|
func testEventuallyStatusOK(t *testing.T, url string) {
|
|
|
|
assert := assert.New(t)
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
assert.Eventually(func() bool {
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody)
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
2023-01-05 08:12:53 -05:00
|
|
|
t.Log("Request failed: ", err.Error())
|
2022-12-21 04:49:21 -05:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
2023-01-05 08:12:53 -05:00
|
|
|
|
|
|
|
statusOK := resp.StatusCode == http.StatusOK
|
|
|
|
if !statusOK {
|
|
|
|
t.Log("Status not OK: ", resp.StatusCode)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Log("Status OK")
|
|
|
|
return true
|
2022-12-21 04:49:21 -05:00
|
|
|
}, timeout, interval)
|
|
|
|
}
|
|
|
|
|
2023-01-03 06:41:46 -05:00
|
|
|
// testEventuallyExternalIPAvailable uses k to query if the whoami service is eventually available.
|
|
|
|
// Once the service is available the Service is returned.
|
2022-12-21 04:49:21 -05:00
|
|
|
func testEventuallyExternalIPAvailable(t *testing.T, k *kubernetes.Clientset) *coreV1.Service {
|
|
|
|
var svc *coreV1.Service
|
|
|
|
|
2023-01-03 06:41:46 -05:00
|
|
|
require.Eventually(t, func() bool {
|
2022-12-21 04:49:21 -05:00
|
|
|
var err error
|
2023-06-09 10:59:19 -04:00
|
|
|
svc, err = k.CoreV1().Services(namespaceName).Get(context.Background(), serviceName, metaV1.GetOptions{})
|
2023-01-03 06:41:46 -05:00
|
|
|
if err != nil {
|
2023-01-05 08:12:53 -05:00
|
|
|
t.Log("Getting service failed: ", err.Error())
|
2023-01-03 06:41:46 -05:00
|
|
|
return false
|
|
|
|
}
|
2023-01-05 08:12:53 -05:00
|
|
|
t.Log("Successfully fetched service: ", svc.String())
|
|
|
|
|
|
|
|
ingressAvailable := len(svc.Status.LoadBalancer.Ingress) > 0
|
|
|
|
if !ingressAvailable {
|
|
|
|
t.Log("Ingress not yet available")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Log("Ingress available")
|
|
|
|
return true
|
2022-12-21 04:49:21 -05:00
|
|
|
}, timeout, interval)
|
|
|
|
|
|
|
|
return svc
|
|
|
|
}
|
|
|
|
|
|
|
|
// testEndpointAvailable GETs the provided URL. It expects a payload from
|
|
|
|
// traefik/whoami service and checks that the first body line is of form
|
|
|
|
// Hostname: <pod-name>
|
|
|
|
// If this works the <pod-name> value is appended to allHostnames slice and
|
|
|
|
// new allHostnames is returned.
|
2023-02-28 09:19:12 -05:00
|
|
|
func testEndpointAvailable(t *testing.T, url string, allHostnames []string, reqIdx int) []string {
|
2022-12-21 04:49:21 -05:00
|
|
|
assert := assert.New(t)
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody)
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
2023-02-28 09:19:12 -05:00
|
|
|
require.NoError(err, "Request #%d failed", reqIdx)
|
2022-12-21 04:49:21 -05:00
|
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(http.StatusOK, resp.StatusCode)
|
|
|
|
// Force close of connections so that we see different backends
|
|
|
|
http.DefaultClient.CloseIdleConnections()
|
|
|
|
|
|
|
|
firstLine, err := bufio.NewReader(resp.Body).ReadString('\n')
|
|
|
|
require.NoError(err)
|
|
|
|
parts := strings.Split(firstLine, ": ")
|
|
|
|
hostnameKey := parts[0]
|
|
|
|
hostnameValue := parts[1]
|
|
|
|
|
|
|
|
assert.Equal("Hostname", hostnameKey)
|
|
|
|
require.NotEmpty(hostnameValue)
|
|
|
|
|
|
|
|
return append(allHostnames, hostnameValue)
|
|
|
|
}
|