Skip to content

Commit

Permalink
test: add an e2e test for session persistence
Browse files Browse the repository at this point in the history
Signed-off-by: Kensei Nakada <handbomusic@gmail.com>
  • Loading branch information
sanposhiho committed Jul 26, 2024
1 parent a30c794 commit d3527e8
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 0 deletions.
30 changes: 30 additions & 0 deletions test/e2e/testdata/cookie-based-session-persistence.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: cookie-based-session-persistence
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: same-namespace
rules:
- matches:
- path:
type: PathPrefix
value: /v1
backendRefs:
- name: infra-backend-v1
port: 8080
- matches:
- path:
type: PathPrefix
value: /v2
backendRefs:
- name: infra-backend-v1
port: 8080
sessionPersistence:
sessionName: Session-A
type: Cookie
# Actually, absoluteTimeout is not necessary for Header based session persistence.
# But, we have to add it, otherwise the gateway-api validation (mistakenly) rejects it.
# https://github.com/kubernetes-sigs/gateway-api/issues/3214
absoluteTimeout: 10s
30 changes: 30 additions & 0 deletions test/e2e/testdata/header-based-session-persistence.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: header-based-session-persistence
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: same-namespace
rules:
- matches:
- path:
type: PathPrefix
value: /v1
backendRefs:
- name: infra-backend-v1
port: 8080
- matches:
- path:
type: PathPrefix
value: /v2
backendRefs:
- name: infra-backend-v1
port: 8080
sessionPersistence:
sessionName: Session-A
type: Header
# Actually, absoluteTimeout is not necessary for Header based session persistence.
# But, we have to add it, otherwise the gateway-api validation (mistakenly) rejects it.
# https://github.com/kubernetes-sigs/gateway-api/issues/3214
absoluteTimeout: 100s
189 changes: 189 additions & 0 deletions test/e2e/tests/session_persistence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// Copyright Envoy Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

//go:build e2e
// +build e2e

package tests

import (
"fmt"
"net/http"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"k8s.io/apimachinery/pkg/types"
httputils "sigs.k8s.io/gateway-api/conformance/utils/http"
"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
"sigs.k8s.io/gateway-api/conformance/utils/suite"
)

func init() {
ConformanceTests = append(ConformanceTests, HeaderBasedSessionPersistenceTest)
ConformanceTests = append(ConformanceTests, CookieBasedSessionPersistenceTest)
}

var HeaderBasedSessionPersistenceTest = suite.ConformanceTest{
ShortName: "HeaderBasedSessionPersistenceTest",
Description: "Test that the session persistence filter is correctly configured with header based session persistence",
Manifests: []string{"testdata/header-based-session-persistence.yaml"},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
t.Run("traffic is routed based on header based session persistence", func(t *testing.T) {
ns := "gateway-conformance-infra"
routeNN := types.NamespacedName{Name: "header-based-session-persistence", Namespace: ns}
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
req := httputils.MakeRequest(t, &httputils.ExpectedResponse{
Request: httputils.Request{
Path: "/v2",
},
}, gwAddr, "HTTP", "http")

pod := ""
// We make 10 requests to the gateway and expect them to be routed to the same pod.
for i := 0; i < 10; i++ {
captReq, res, err := suite.RoundTripper.CaptureRoundTrip(req)
if err != nil {
t.Fatalf("failed to make request: %v", err)
}

if i == 0 {
// First request, capture the pod name and header.
sessionHeader, ok := res.Headers["Session-A"]
if !ok {
t.Fatalf("expected header Session-A to be set: %v", res.Headers)
}

if captReq.Pod == "" {
t.Fatalf("expected pod to be set")
}
pod = captReq.Pod
req.Headers["Session-A"] = sessionHeader
continue
}

t.Logf("request is received from pod %s", captReq.Pod)

if captReq.Pod != pod {
t.Fatalf("expected pod to be the same as previous requests")
}
}
})
t.Run("session persistence is configured per route", func(t *testing.T) {
ns := "gateway-conformance-infra"
routeNN := types.NamespacedName{Name: "header-based-session-persistence", Namespace: ns}
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
req := httputils.MakeRequest(t, &httputils.ExpectedResponse{
Request: httputils.Request{
// /v1 path does not have the session persistence.
Path: "/v1",
},
}, gwAddr, "HTTP", "http")

_, res, err := suite.RoundTripper.CaptureRoundTrip(req)
if err != nil {
t.Fatalf("failed to make request: %v", err)
}

if h, ok := res.Headers["Session-A"]; ok {
t.Fatalf("expected header Session-A to not be set: %v", h)
}
})
},
}

var CookieBasedSessionPersistenceTest = suite.ConformanceTest{
ShortName: "CookieBasedSessionPersistenceTest",
Description: "Test that the session persistence filter is correctly configured with cookie based session persistence",
Manifests: []string{"testdata/cookie-based-session-persistence.yaml"},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
t.Run("traffic is routed based on cookie based session persistence", func(t *testing.T) {
ns := "gateway-conformance-infra"
routeNN := types.NamespacedName{Name: "cookie-based-session-persistence", Namespace: ns}
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
req := httputils.MakeRequest(t, &httputils.ExpectedResponse{
Request: httputils.Request{
Path: "/v2",
},
}, gwAddr, "HTTP", "http")

pod := ""
// We make 10 requests to the gateway and expect them to be routed to the same pod.
for i := 0; i < 10; i++ {
captReq, res, err := suite.RoundTripper.CaptureRoundTrip(req)
if err != nil {
t.Fatalf("failed to make request: %v", err)
}

if i == 0 {
// First request, capture the pod name and cookie.
if captReq.Pod == "" {
t.Fatalf("expected pod to be set")
}

cookie, err := parseCookie(res.Headers, "Session-A")
if err != nil {
t.Fatalf("failed to parse cookie: %v", err)
}

// Check the cookie is set correctly.
if diff := cmp.Diff(cookie, &http.Cookie{
Name: "Session-A",
MaxAge: 10,
Path: "/v2",
HttpOnly: true,
}, cmpopts.IgnoreFields(http.Cookie{}, "Value", "Raw"), // Ignore the value as it is random.
); diff != "" {
t.Fatalf("unexpected cookie: %v", diff)
}

pod = captReq.Pod
req.Headers["Cookie"] = []string{fmt.Sprintf("Session-A=%s", cookie.Value)}
continue
}

t.Logf("request is received from pod %s", captReq.Pod)

if captReq.Pod != pod {
t.Fatalf("expected pod to be the same as previous requests")
}
}
})
t.Run("session persistence is configured per route", func(t *testing.T) {
ns := "gateway-conformance-infra"
routeNN := types.NamespacedName{Name: "cookie-based-session-persistence", Namespace: ns}
gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns}
gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
req := httputils.MakeRequest(t, &httputils.ExpectedResponse{
Request: httputils.Request{
// /v1 path does not have the session persistence.
Path: "/v1",
},
}, gwAddr, "HTTP", "http")

_, res, err := suite.RoundTripper.CaptureRoundTrip(req)
if err != nil {
t.Fatalf("failed to make request: %v", err)
}

if _, ok := res.Headers["Set-Cookie"]; ok {
t.Fatal("expected the envoy not to response set-cookie back")
}
})
},
}

func parseCookie(headers map[string][]string, cookieName string) (*http.Cookie, error) {
parser := &http.Response{Header: headers}
for _, c := range parser.Cookies() {
if c.Name == cookieName {
return c, nil
}
}
return nil, fmt.Errorf("cookie %s not found: headers: %v", cookieName, headers)
}

0 comments on commit d3527e8

Please sign in to comment.