diff --git a/test/e2e/idle_connection_test.go b/test/e2e/idle_connection_test.go index 134afccff..e80d856fa 100644 --- a/test/e2e/idle_connection_test.go +++ b/test/e2e/idle_connection_test.go @@ -22,12 +22,10 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/util/retry" "k8s.io/utils/ptr" operatorv1 "github.com/openshift/api/operator/v1" routev1 "github.com/openshift/api/route/v1" - "github.com/openshift/cluster-ingress-operator/pkg/operator/controller" operatorcontroller "github.com/openshift/cluster-ingress-operator/pkg/operator/controller" ) @@ -39,9 +37,8 @@ const ( type idleConnectionTestConfig struct { deployments []*appsv1.Deployment httpClient *http.Client - namespace string pods []*corev1.Pod - route *routev1.Route + routeName types.NamespacedName services []*corev1.Service testLabels map[string]string } @@ -54,32 +51,6 @@ type haproxyBackend struct { servers []string // Server entries in this backend. } -// waitWithTimeout is a test helper that wraps wait operations -// requiring a context deadline. Instead of manually creating contexts -// with timeouts throughout test code, which can lead to easy mistakes -// with deferred cancellations, this helper encapsulates the -// boilerplate. -// -// The helper is designed for use in tests where: -// - Multiple wait operations occur in sequence -// - Each wait needs its own timeout -// - Deferred cancellations would stack up -// - Context creation/cleanup would clutter test logic -// - Local variables would be needed just to hold intermediate state -// -// Example usage: -// -// if err := waitWithTimeout(time.Minute, func(ctx context.Context) error { -// return waitForRouteAdmitted(t, ctx, "default", route) -// }); err != nil { -// t.Fatalf("route not admitted: %v", err) -// } -func waitWithTimeout(timeout time.Duration, waitFunc func(context.Context) error) error { - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - return waitFunc(ctx) -} - // getHAProxyConfigFromRouterPod retrieves the HAProxy configuration // from a pod. func getHAProxyConfigFromRouterPod(t *testing.T, pod *corev1.Pod) (string, error) { @@ -185,38 +156,35 @@ func findHAProxyBackendWithServiceServer(backends []haproxyBackend, expectedBack // given label selector, checking for consistency across all pods. The // function continues polling until the configuration is verified or // the provided context is cancelled. -func waitForHAProxyConfigUpdate(ctx context.Context, t *testing.T, ic *operatorv1.IngressController, podSelector string, expectedBackendName, expectedServerName string) error { - getPods := func(ic *operatorv1.IngressController) ([]corev1.Pod, error) { - deploymentName := controller.RouterDeploymentName(ic) +func waitForHAProxyConfigUpdate(ctx context.Context, t *testing.T, ic *operatorv1.IngressController, backendName, serverName string) error { + return wait.PollUntilContextCancel(ctx, 6*time.Second, true, func(ctx context.Context) (bool, error) { + deploymentName := operatorcontroller.RouterDeploymentName(ic) deployment, err := getDeployment(t, kclient, deploymentName, time.Minute) if err != nil { - return nil, fmt.Errorf("failed to get deployment %s/%s: %w", deploymentName.Namespace, deploymentName.Name, err) + t.Logf("Failed to get deployment %s: %v, retrying...", deploymentName, err) + return false, nil } podList, err := getPods(t, kclient, deployment) if err != nil { - return nil, fmt.Errorf("failed to get pods in deployment %s/%s: %w", deploymentName.Namespace, deploymentName.Name, err) - } - - return podList.Items, nil - } - - return wait.PollUntilContextCancel(ctx, 6*time.Second, true, func(ctx context.Context) (bool, error) { - pods, err := getPods(ic) - if err != nil { - t.Logf("Failed to get pods in namespace %s: %v, retrying...", operatorcontroller.DefaultOperandNamespace, err) + t.Logf("Failed to get pods for deployment %s: %v, retrying...", deploymentName, err) return false, nil } - if len(pods) == 0 { - return false, fmt.Errorf("no router pods in namespace %s for selector %q", operatorcontroller.DefaultOperandNamespace, podSelector) + if len(podList.Items) == 0 { + return false, fmt.Errorf("no router pods found for deployment %s", deploymentName) } allPodsMatch := true - for _, pod := range pods { + for _, pod := range podList.Items { + if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { + t.Logf("Skipping terminated pod %s/%s (phase: %v)", pod.Namespace, pod.Name, pod.Status.Phase) + continue + } + haproxyConfig, err := getHAProxyConfigFromRouterPod(t, &pod) if err != nil { - t.Logf("Failed to get HAProxy config (pod may be restarting): %v, retrying...", err) + t.Logf("Failed to get HAProxy config from pod %s/%s: %v, retrying...", pod.Namespace, pod.Name, err) allPodsMatch = false continue } @@ -228,50 +196,25 @@ func waitForHAProxyConfigUpdate(ctx context.Context, t *testing.T, ic *operatorv continue } - backend, found := findHAProxyBackendWithServiceServer(backends, expectedBackendName, expectedServerName) + backend, found := findHAProxyBackendWithServiceServer(backends, backendName, serverName) if !found { allPodsMatch = false - t.Logf("Waiting for backend %q in pod %s/%s", expectedBackendName, pod.Namespace, pod.Name) + t.Logf("Waiting for HAProxy backend %q in pod %s/%s", backendName, pod.Namespace, pod.Name) continue } - t.Logf("Found HAProxy backend in pod %s/%s:\nBackend: %s\nServers: %s", pod.Namespace, pod.Name, expectedBackendName, strings.Join(backend.servers, "\n ")) + t.Logf("Found HAProxy backend in pod %s/%s:\nBackend: %s\nServers: %s", pod.Namespace, pod.Name, backendName, strings.Join(backend.servers, "\n ")) } return allPodsMatch, nil }) } -func waitForRouteAdmitted(ctx context.Context, t *testing.T, ic *operatorv1.IngressController, route *routev1.Route) error { - return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { - if err := kclient.Get(ctx, types.NamespacedName{Name: route.Name, Namespace: route.Namespace}, route); err != nil { - return false, fmt.Errorf("failed to get route %s/%s: %w", route.Namespace, route.Name, err) - } - - for _, ingress := range route.Status.Ingress { - if ingress.RouterName == ic.Name { - for _, cond := range ingress.Conditions { - if cond.Type == routev1.RouteAdmitted && cond.Status == corev1.ConditionTrue { - t.Logf("Route %s/%s has been admitted by ingress controller %s", route.Namespace, route.Name, ic.Name) - return true, nil - } - } - - t.Logf("Route %s/%s has not yet been admitted by ingress controller %s", route.Namespace, route.Name, ic.Name) - return false, nil - } - } - - t.Logf("Route %s/%s does not list ingress controller %s", route.Namespace, route.Name, ic.Name) - return false, nil - }) -} - -func idleConnectionTestSetup(ctx context.Context, t *testing.T, namespace string, ic *operatorv1.IngressController) (*idleConnectionTestConfig, error) { - canaryImage := func(t *testing.T) (string, error) { +func idleConnectionTestSetup(ctx context.Context, t *testing.T, ns *corev1.Namespace, ic *operatorv1.IngressController) (*idleConnectionTestConfig, error) { + canaryImageReference := func(t *testing.T) (string, error) { ingressOperatorName := types.NamespacedName{ - Namespace: operatorNamespace, Name: "ingress-operator", + Namespace: operatorNamespace, } deployment, err := getDeployment(t, kclient, ingressOperatorName, 1*time.Minute) @@ -292,40 +235,23 @@ func idleConnectionTestSetup(ctx context.Context, t *testing.T, namespace string tc := &idleConnectionTestConfig{ testLabels: map[string]string{ - "test": "idle-connection", - "app": "web-server", + "ingress-controller": ic.Name, }, } - ns := createNamespace(t, namespace) - tc.namespace = ns.Name - - image, err := canaryImage(t) + image, err := canaryImageReference(t) if err != nil { return nil, fmt.Errorf("failed to get canary image: %w", err) } - if err := idleConnectionCreateBackendService(ctx, t, tc, 1, idleConnectionResponseServiceA, image); err != nil { + if err := idleConnectionCreateBackendService(ctx, t, ns, tc, 1, idleConnectionResponseServiceA, image); err != nil { return nil, fmt.Errorf("failed to create backend 1: %w", err) } - if err := idleConnectionCreateBackendService(ctx, t, tc, 2, idleConnectionResponseServiceB, image); err != nil { + if err := idleConnectionCreateBackendService(ctx, t, ns, tc, 2, idleConnectionResponseServiceB, image); err != nil { return nil, fmt.Errorf("failed to create backend 2: %w", err) } - route, err := idleConnectionCreateRoute(ctx, tc.namespace, "test", tc.services[0].Name, tc.testLabels) - if err != nil { - return nil, fmt.Errorf("failed to create test route: %w", err) - } - - tc.route = route - - if err := waitWithTimeout(time.Minute, func(ctx context.Context) error { - return waitForRouteAdmitted(ctx, t, ic, tc.route) - }); err != nil { - return nil, fmt.Errorf("error waiting for route to be admitted: %w", err) - } - for _, deployment := range tc.deployments { t.Logf("Waiting for deployment %s/%s to be ready...", deployment.Namespace, deployment.Name) @@ -347,6 +273,23 @@ func idleConnectionTestSetup(ctx context.Context, t *testing.T, namespace string } } + route, err := idleConnectionCreateRoute(ctx, ns.Name, "test", tc.services[0].Name, tc.testLabels) + if err != nil { + return nil, fmt.Errorf("failed to create test route: %w", err) + } + + routeAdmittedCondition := routev1.RouteIngressCondition{ + Type: routev1.RouteAdmitted, + Status: corev1.ConditionTrue, + } + + if err := waitForRouteIngressConditions(t, kclient, types.NamespacedName{Name: route.Name, Namespace: route.Namespace}, ic.Name, routeAdmittedCondition); err != nil { + return nil, fmt.Errorf("error waiting for route to be admitted: %w", err) + } + + t.Logf("Route %s/%s admitted by ingresscontroller %s", route.Namespace, route.Name, ic.Name) + t.Logf("%+v", route) + if len(tc.deployments) != 2 { return nil, fmt.Errorf("expected 2 deployments, but got %d", len(tc.deployments)) } @@ -359,21 +302,19 @@ func idleConnectionTestSetup(ctx context.Context, t *testing.T, namespace string return nil, fmt.Errorf("expected 2 pods, but got %d", len(tc.pods)) } - if tc.route == nil { - return nil, fmt.Errorf("expected 1 route, but got none") - } + tc.routeName = types.NamespacedName{Namespace: route.Namespace, Name: route.Name} return tc, nil } -func idleConnectionCreateBackendService(ctx context.Context, t *testing.T, tc *idleConnectionTestConfig, index int, serverResponse, image string) error { - svc, err := idleConnectionCreateService(ctx, tc.namespace, index) +func idleConnectionCreateBackendService(ctx context.Context, t *testing.T, ns *corev1.Namespace, tc *idleConnectionTestConfig, index int, serverResponse, image string) error { + svc, err := idleConnectionCreateService(ctx, ns.Name, index) if err != nil { return fmt.Errorf("failed to create service %d: %w", index, err) } tc.services = append(tc.services, svc) - deployment, err := idleConnectionCreateDeployment(ctx, tc.namespace, index, serverResponse, image) + deployment, err := idleConnectionCreateDeployment(ctx, ns.Name, index, serverResponse, image) if err != nil { return fmt.Errorf("failed to create deployment %d: %w", index, err) } @@ -393,7 +334,6 @@ func idleConnectionCreateDeployment(ctx context.Context, namespace string, servi selectorLabels := map[string]string{ "app": "web-server", "instance": fmt.Sprintf("%d", serviceNumber), - "test": namespace, } deployment := &appsv1.Deployment{ @@ -436,8 +376,8 @@ func idleConnectionCreateDeployment(ctx context.Context, namespace string, servi Scheme: corev1.URISchemeHTTP, }, }, - InitialDelaySeconds: 5, - PeriodSeconds: 10, + InitialDelaySeconds: 1, + PeriodSeconds: 1, TimeoutSeconds: 5, }, LivenessProbe: &corev1.Probe{ @@ -448,8 +388,8 @@ func idleConnectionCreateDeployment(ctx context.Context, namespace string, servi Scheme: corev1.URISchemeHTTP, }, }, - InitialDelaySeconds: 5, - PeriodSeconds: 10, + InitialDelaySeconds: 1, + PeriodSeconds: 1, TimeoutSeconds: 5, }, @@ -489,7 +429,6 @@ func idleConnectionCreateService(ctx context.Context, namespace string, serviceN selectorLabels := map[string]string{ "app": "web-server", "instance": fmt.Sprintf("%d", serviceNumber), - "test": namespace, } service := &corev1.Service{ @@ -527,6 +466,7 @@ func idleConnectionCreateRoute(ctx context.Context, namespace, name, serviceName Labels: labels, }, Spec: routev1.RouteSpec{ + Subdomain: name, To: routev1.RouteTargetReference{ Kind: "Service", Name: serviceName, @@ -545,114 +485,113 @@ func idleConnectionCreateRoute(ctx context.Context, namespace, name, serviceName return route, nil } -func idleConnectionFetchResponse(t *testing.T, route *routev1.Route, client *http.Client) (string, error) { - url := fmt.Sprintf("http://%s/custom-response", route.Spec.Host) +func idleConnectionFetchResponse(ctx context.Context, t *testing.T, client *http.Client, elbHostname, routeName string) (string, error) { + var body string - resp, err := client.Get(url) - if err != nil { - return "", fmt.Errorf("failed to GET response from service: %w", err) - } - defer resp.Body.Close() + if err := wait.PollUntilContextCancel(ctx, 6*time.Second, true, func(ctx context.Context) (bool, error) { + url := fmt.Sprintf("http://%s/custom-response", elbHostname) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return false, fmt.Errorf("failed to create request: %w", err) + } + req.Host = routeName + t.Logf("GET %s with Host %s", url, req.Host) - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } + resp, err := client.Do(req) + if err != nil { + t.Logf("Request failed: %v, retrying...", err) + return false, nil + } + defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read response body: %w", err) - } + if resp.StatusCode == http.StatusServiceUnavailable { + t.Logf("Got %v Service Unavailable, retrying...", resp.StatusCode) + return false, nil + } - t.Logf("GET %s Status=%v Body=%q", url, resp.StatusCode, body) + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } - return string(body), nil + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return false, fmt.Errorf("failed to read response body: %w", err) + } + + body = string(bodyBytes) + return true, nil + }); err != nil { + return "", err + } + + return body, nil } -func idleConnectionSwitchRouteService(ctx context.Context, t *testing.T, ic *operatorv1.IngressController, tc *idleConnectionTestConfig, serviceIndex int) (*routev1.Route, error) { +func idleConnectionSwitchRouteService(t *testing.T, ic *operatorv1.IngressController, tc *idleConnectionTestConfig, routeName types.NamespacedName, serviceIndex int) (*routev1.Route, error) { if serviceIndex >= len(tc.services) { return nil, fmt.Errorf("service index %d out of range", serviceIndex) } service := tc.services[serviceIndex] - route := tc.route - - if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - updatedRoute := &routev1.Route{} - if err := kclient.Get(ctx, types.NamespacedName{Name: route.Name, Namespace: route.Namespace}, updatedRoute); err != nil { - return fmt.Errorf("failed to get route %s/%s: %w", route.Namespace, route.Name, err) - } - - updatedRoute.Spec.To.Name = service.Name - if err := kclient.Update(ctx, updatedRoute); err != nil { - t.Logf("Failed to update route %s/%s to point to service %s/%s: %v, retrying...", route.Namespace, route.Name, service.Namespace, service.Name, err) - return err - } - return nil + var updatedRoute *routev1.Route + if err := updateRouteWithRetryOnConflict(t, routeName, time.Minute, func(route *routev1.Route) { + route.Spec.To.Name = service.Name + updatedRoute = route }); err != nil { - return nil, fmt.Errorf("failed to update route %s/%s to point to service %s/%s: %w", route.Namespace, route.Name, service.Namespace, service.Name, err) + return nil, fmt.Errorf("failed to update route %s to point to service %s/%s: %w", routeName, service.Namespace, service.Name, err) } - t.Logf("Updated route %s/%s to point to service %s/%s", route.Namespace, route.Name, service.Namespace, service.Name) + t.Logf("Switched route %s to service %s/%s", routeName, service.Namespace, service.Name) - if err := waitWithTimeout(time.Minute, func(ctx context.Context) error { - return waitForRouteAdmitted(ctx, t, ic, route) - }); err != nil { + routeAdmittedCondition := routev1.RouteIngressCondition{ + Type: routev1.RouteAdmitted, + Status: corev1.ConditionTrue, + } + if err := waitForRouteIngressConditions(t, kclient, routeName, ic.Name, routeAdmittedCondition); err != nil { return nil, fmt.Errorf("error waiting for route to be admitted: %w", err) } - expectedBackendName := fmt.Sprintf("be_http:%s:%s", route.Namespace, route.Name) - expectedServerName := fmt.Sprintf("pod:%s:%s:http:%s:%d", tc.pods[serviceIndex].Name, service.Name, tc.pods[serviceIndex].Status.PodIP, service.Spec.Ports[0].Port) + t.Logf("Route %s admitted by ingresscontroller %s", routeName, ic.Name) - podSelector := fmt.Sprintf("ingresscontroller.operator.openshift.io/deployment-ingresscontroller=%s", ic.Name) + expectedBackendName := fmt.Sprintf("be_http:%s:%s", routeName.Namespace, routeName.Name) + expectedServerName := fmt.Sprintf("pod:%s:%s:http:%s:%d", tc.pods[serviceIndex].Name, service.Name, tc.pods[serviceIndex].Status.PodIP, service.Spec.Ports[0].Port) - if err := waitWithTimeout(5*time.Minute, func(ctx context.Context) error { - return waitForHAProxyConfigUpdate(ctx, t, ic, podSelector, expectedBackendName, expectedServerName) - }); err != nil { - return nil, fmt.Errorf("error waiting for HAProxy configuration update for service %s/%s: %w", service.Namespace, service.Name, err) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + if err := waitForHAProxyConfigUpdate(ctx, t, ic, expectedBackendName, expectedServerName); err != nil { + return nil, fmt.Errorf("error waiting for HAProxy configuration update for route %s to point to service %s/%s: %w", routeName, service.Namespace, service.Name, err) } - t.Logf("HAProxy configuration updated for route %s/%s to point to service %s/%s", route.Namespace, route.Name, service.Namespace, service.Name) + t.Logf("HAProxy configuration updated for route %s to point to service %s/%s", routeName, service.Namespace, service.Name) - return route, nil + return updatedRoute, nil } -func idleConnectionSwitchTerminationPolicy(ctx context.Context, t *testing.T, icName types.NamespacedName, policy operatorv1.IngressControllerConnectionTerminationPolicy, availableConditions []operatorv1.OperatorCondition) error { - if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - ic, err := getIngressController(t, kclient, icName, time.Minute) - if err != nil { - return fmt.Errorf("failed to get IngressController: %w", err) - } - - t.Logf("Switch ingresscontroller %s IdleConnectionTerminationPolicy from %s to %s", ic.Name, - ic.Spec.IdleConnectionTerminationPolicy, policy) - +func idleConnectionSwitchIdleTerminationPolicy(t *testing.T, ic *operatorv1.IngressController, icName types.NamespacedName, policy operatorv1.IngressControllerConnectionTerminationPolicy) error { + if err := updateIngressControllerWithRetryOnConflict(t, icName, 5*time.Minute, func(ic *operatorv1.IngressController) { ic.Spec.IdleConnectionTerminationPolicy = policy - if err := kclient.Update(ctx, ic); err != nil { - t.Logf("Failed to update IdleConnectionTerminationPolicy to %s: %v, retrying...", policy, err) - return err - } - return nil }); err != nil { - return fmt.Errorf("failed to switch IdleConnectionTerminationPolicy to %s: %w", policy, err) + return fmt.Errorf("failed to update IdleConnectionTerminationPolicy to %q for ingresscontroller %s: %w", policy, icName, err) } - t.Logf("Waiting for ingresscontroller to stabilise after policy switch to %s", policy) + t.Logf("Updated IdleConnectionTerminationPolicy from %q to %q for ingresscontroller %s", ic.Spec.IdleConnectionTerminationPolicy, policy, icName) - if err := waitForIngressControllerCondition(t, kclient, 5*time.Minute, icName, availableConditions...); err != nil { - return fmt.Errorf("failed to observe expected conditions after switching policy to %s: %w", policy, err) + if err := waitForDeploymentCompleteWithOldPodTermination(t, kclient, operatorcontroller.RouterDeploymentName(ic), 3*time.Minute); err != nil { + return fmt.Errorf("failed to observe router deployment completion: %w", err) } - t.Logf("IngressController available after policy switch to %s", policy) + t.Logf("Waiting for ingresscontroller to stabilise after policy switch to %q", policy) - routerDeployment := &appsv1.Deployment{} - routerDeploymentName := types.NamespacedName{ - Namespace: operatorcontroller.DefaultOperandNamespace, - Name: "router-default", + if err := waitForIngressControllerCondition(t, kclient, 5*time.Minute, icName, availableConditionsForIngressControllerWithLoadBalancer...); err != nil { + return fmt.Errorf("failed to observe expected conditions after switching policy to %q: %w", policy, err) } - if err := kclient.Get(ctx, routerDeploymentName, routerDeployment); err != nil { - return fmt.Errorf("failed to get router deployment: %w", err) + t.Logf("IngressController available after policy switch to %q", policy) + + routerDeployment := appsv1.Deployment{} + if err := kclient.Get(context.TODO(), operatorcontroller.RouterDeploymentName(ic), &routerDeployment); err != nil { + t.Fatalf("failed to get ingresscontroller deployment: %v", err) } verifyRouterEnvVar := func(expectValue string) error { @@ -663,7 +602,7 @@ func idleConnectionSwitchTerminationPolicy(ctx context.Context, t *testing.T, ic t.Logf("Waiting for router deployment to have environment variable ROUTER_IDLE_CLOSE_ON_RESPONSE %s", state) - if err := waitForDeploymentEnvVar(t, kclient, routerDeployment, 2*time.Minute, "ROUTER_IDLE_CLOSE_ON_RESPONSE", expectValue); err != nil { + if err := waitForDeploymentEnvVar(t, kclient, &routerDeployment, 2*time.Minute, "ROUTER_IDLE_CLOSE_ON_RESPONSE", expectValue); err != nil { return fmt.Errorf("expected router deployment to have ROUTER_IDLE_CLOSE_ON_RESPONSE %s: %w", state, err) } @@ -705,74 +644,92 @@ func idleConnectionSwitchTerminationPolicy(ctx context.Context, t *testing.T, ic // behaviour and validates subsequent requests route correctly to the // new backend. func Test_IdleConnectionTerminationPolicy(t *testing.T) { - testNamespace := "idle-close-on-response-e2e-" + rand.String(5) - icAvailableConditions := defaultAvailableConditions - icName := types.NamespacedName{ - Name: "default", - Namespace: operatorcontroller.DefaultOperatorNamespace, + testName := "idle-close-on-response-" + rand.String(5) + icName := types.NamespacedName{Namespace: operatorNamespace, Name: testName} + ns := createNamespace(t, icName.Name) + ic := newLoadBalancerController(icName, icName.Name+"."+dnsConfig.Spec.BaseDomain) + ic.Spec.EndpointPublishingStrategy.LoadBalancer = &operatorv1.LoadBalancerStrategy{ + Scope: operatorv1.ExternalLoadBalancer, + DNSManagementPolicy: operatorv1.ManagedLoadBalancerDNS, + } + if err := kclient.Create(context.TODO(), ic); err != nil { + t.Fatalf("failed to create ingresscontroller: %v", err) } + defer assertIngressControllerDeleted(t, kclient, ic) - if err := waitForIngressControllerCondition(t, kclient, 5*time.Minute, defaultName, icAvailableConditions...); err != nil { + if err := waitForIngressControllerCondition(t, kclient, 5*time.Minute, icName, availableConditionsForIngressControllerWithLoadBalancer...); err != nil { t.Fatalf("failed to observe expected conditions: %v", err) } - ic, err := getIngressController(t, kclient, icName, time.Minute) + ic, err := getIngressController(t, kclient, icName, 1*time.Minute) if err != nil { - t.Fatalf("failed to retrieve IngressController: %v", err) + t.Fatalf("failed to get ingresscontroller: %v", err) } - initialPolicy := ic.Spec.IdleConnectionTerminationPolicy - t.Logf("IngressController %s initial IdleConnectionTerminationPolicy=%q", ic.Name, initialPolicy) + elbHostname := getIngressControllerLBAddress(t, ic) + externalTestPodName := types.NamespacedName{Name: icName.Name + "-external-verify", Namespace: icName.Namespace} + verifyExternalIngressController(t, externalTestPodName, "apps."+ic.Spec.Domain, elbHostname) - t.Cleanup(func() { - if err := idleConnectionSwitchTerminationPolicy(context.Background(), t, icName, initialPolicy, icAvailableConditions); err != nil { - t.Errorf("cleanup: failed to switch ingresscontroller %q back to initial policy %q: %v", icName, initialPolicy, err) - } - }) + lbService := &corev1.Service{} + if err := kclient.Get(context.TODO(), operatorcontroller.LoadBalancerServiceName(ic), lbService); err != nil { + t.Fatalf("failed to get LoadBalancer service for ingresscontroller %s: %v", icName, err) + } - tc, err := idleConnectionTestSetup(context.Background(), t, testNamespace, ic) + currentIdleTerminationPolicy := ic.Spec.IdleConnectionTerminationPolicy + t.Logf("IngressController %s initial IdleConnectionTerminationPolicy=%q", ic.Name, currentIdleTerminationPolicy) + + tc, err := idleConnectionTestSetup(context.Background(), t, ns, ic) if err != nil { - t.Fatalf("failed to set up test resources: %v", err) + t.Fatalf("test setup failed: %v", err) + } + + var route routev1.Route + if err := kclient.Get(context.TODO(), tc.routeName, &route); err != nil { + t.Fatalf("failed to get route %s: %v", tc.routeName, err) + } + + routeHost := getRouteHost(&route, ic.Name) + if routeHost == "" { + t.Fatalf("Route %s has no host assigned by ingresscontroller %s", tc.routeName, ic.Name) } expectedResponses := map[operatorv1.IngressControllerConnectionTerminationPolicy][]string{ operatorv1.IngressControllerConnectionTerminationPolicyDeferred: { - idleConnectionResponseServiceA, // Step 1: Switch to web-server-1 - idleConnectionResponseServiceA, // Step 2: Initial GET - idleConnectionResponseServiceA, // Step 3: GET after switching to web-server-2 - idleConnectionResponseServiceB, // Step 4: Final GET + idleConnectionResponseServiceA, // Step 1: Switch to web-server-1 and GET response. + idleConnectionResponseServiceA, // Step 2: GET response. + idleConnectionResponseServiceA, // Step 3: Switch to web-server-2 and GET response. + idleConnectionResponseServiceB, // Step 4: GET response. }, operatorv1.IngressControllerConnectionTerminationPolicyImmediate: { - idleConnectionResponseServiceA, // Step 1: Switch to web-server-1 - idleConnectionResponseServiceA, // Step 2: Initial GET - idleConnectionResponseServiceB, // Step 3: GET after switching to web-server-2 - idleConnectionResponseServiceB, // Step 4: Final GET + idleConnectionResponseServiceA, // Step 1: Switch to web-server-1 and GET response. + idleConnectionResponseServiceA, // Step 2: GET response. + idleConnectionResponseServiceB, // Step 3: Switch to web-server-2 and GET response. + idleConnectionResponseServiceB, // Step 4: GET response. }, } actions := []func(ctx context.Context, tc *idleConnectionTestConfig) (string, error){ func(ctx context.Context, tc *idleConnectionTestConfig) (string, error) { - // Step 1: Set the route back to web-server-1. - if _, err := idleConnectionSwitchRouteService(ctx, t, ic, tc, 0); err != nil { + // Step 1: Set the route back to service web-server-1 and fetch the response. + if _, err := idleConnectionSwitchRouteService(t, ic, tc, tc.routeName, 0); err != nil { return "", fmt.Errorf("failed to switch route back to web-server-1: %w", err) } - return idleConnectionFetchResponse(t, tc.route, tc.httpClient) + return idleConnectionFetchResponse(ctx, t, tc.httpClient, elbHostname, routeHost) }, func(ctx context.Context, tc *idleConnectionTestConfig) (string, error) { // Step 2: Verify the response from web-server-1. - return idleConnectionFetchResponse(t, tc.route, tc.httpClient) + return idleConnectionFetchResponse(ctx, t, tc.httpClient, elbHostname, routeHost) }, func(ctx context.Context, tc *idleConnectionTestConfig) (string, error) { - // Step 3: Switch the route to web-server-2 and fetch the response. - _, err := idleConnectionSwitchRouteService(ctx, t, ic, tc, 1) - if err != nil { + // Step 3: Switch the route to service web-server-2 and fetch the response. + if _, err := idleConnectionSwitchRouteService(t, ic, tc, tc.routeName, 1); err != nil { return "", fmt.Errorf("failed to switch route to web-server-2: %w", err) } - return idleConnectionFetchResponse(t, tc.route, tc.httpClient) + return idleConnectionFetchResponse(ctx, t, tc.httpClient, elbHostname, routeHost) }, func(ctx context.Context, tc *idleConnectionTestConfig) (string, error) { // Step 4: Fetch the final response (expected to be from web-server-2). - return idleConnectionFetchResponse(t, tc.route, tc.httpClient) + return idleConnectionFetchResponse(ctx, t, tc.httpClient, elbHostname, routeHost) }, } @@ -781,9 +738,12 @@ func Test_IdleConnectionTerminationPolicy(t *testing.T) { operatorv1.IngressControllerConnectionTerminationPolicyDeferred, } - // If the initial policy doesn't match our first test policy, - // reorder the tests. - if initialPolicy == operatorv1.IngressControllerConnectionTerminationPolicyDeferred { + // If the current policy is Deferred, reorder the test cases + // to start with Deferred. Later in the test, we skip updating + // the policy when it matches our test case. This way, if the + // IngressController starts with Deferred policy, we avoid an + // unnecessary rollout in the beginning of our test. + if currentIdleTerminationPolicy == operatorv1.IngressControllerConnectionTerminationPolicyDeferred { t.Log("Reordering test cases to avoid initial policy switch") policiesToTest = []operatorv1.IngressControllerConnectionTerminationPolicy{ operatorv1.IngressControllerConnectionTerminationPolicyDeferred, @@ -791,24 +751,22 @@ func Test_IdleConnectionTerminationPolicy(t *testing.T) { } } + tc.httpClient = &http.Client{ + Timeout: time.Minute, + Transport: &http.Transport{ + IdleConnTimeout: 300 * time.Second, + }, + } + for i, policy := range policiesToTest { - // Only switch policy if it's not the first - // test matching the initial policy. - if i == 0 && policy == initialPolicy { - t.Logf("Skipping policy switch as current policy %s already matches %s", initialPolicy, policy) + if i == 0 && policy == currentIdleTerminationPolicy { + t.Logf("Skipping initial policy switch as current policy %q already matches %q", currentIdleTerminationPolicy, policy) } else { - if err := idleConnectionSwitchTerminationPolicy(context.Background(), t, icName, policy, icAvailableConditions); err != nil { + if err := idleConnectionSwitchIdleTerminationPolicy(t, ic, icName, policy); err != nil { t.Fatalf("failed to switch to policy %q: %v", policy, err) } } - tc.httpClient = &http.Client{ - Timeout: 30 * time.Second, - Transport: &http.Transport{ - IdleConnTimeout: 300 * time.Second, - }, - } - for j, action := range actions { resp, err := action(context.Background(), tc) if err != nil { @@ -816,11 +774,13 @@ func Test_IdleConnectionTerminationPolicy(t *testing.T) { } if resp != expectedResponses[policy][j] { - t.Fatalf("unexpected response at step %d for policy %s: got %q, want %q", + t.Fatalf("unexpected response at step %d for policy %q: got %q, want %q", j+1, policy, resp, expectedResponses[policy][j]) } - t.Logf("Response at step %d for policy %s matches expected: %q", j+1, policy, resp) + t.Logf("Step %d response for policy %q matches expected value %q", j+1, policy, resp) } } + + t.Logf("test successful") }