diff --git a/test/gatewayapi/install.sh b/test/gatewayapi/install.sh index 00d82ea65..c7ab93372 100755 --- a/test/gatewayapi/install.sh +++ b/test/gatewayapi/install.sh @@ -2,7 +2,7 @@ set -o errexit -CONTOUR_VER="v1.23.0" +CONTOUR_VER="v1.26.0" GATEWAY_API_VER="v1beta1" REPO_ROOT=$(git rev-parse --show-toplevel) KUSTOMIZE_VERSION=4.5.2 diff --git a/test/gatewayapi/test-session-affinity.sh b/test/gatewayapi/test-session-affinity.sh new file mode 100755 index 000000000..e244f1737 --- /dev/null +++ b/test/gatewayapi/test-session-affinity.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash + +# This script runs e2e tests for progressive traffic shifting with session affinity, Canary analysis and promotion +# Prerequisites: Kubernetes Kind and Contour with GatewayAPI + +set -o errexit + +REPO_ROOT=$(git rev-parse --show-toplevel) + +source ${REPO_ROOT}/test/gatewayapi/test-utils.sh + +create_latency_metric_template +create_error_rate_metric_template + +echo '>>> Deploy podinfo in sa-test namespace' +kubectl create ns sa-test +kubectl apply -f ${REPO_ROOT}/test/workloads/secret.yaml -n sa-test +kubectl apply -f ${REPO_ROOT}/test/workloads/deployment.yaml -n sa-test + +echo '>>> Installing Canary' +cat <>> Port forwarding load balancer' +kubectl port-forward -n projectcontour svc/envoy-contour 8888:80 2>&1 > /dev/null & +pf_pid=$! + +cleanup() { + echo ">> Killing port forward process ${pf_pid}" + kill -9 $pf_pid +} +trap "cleanup" EXIT SIGINT + +echo '>>> Triggering canary deployment' +kubectl -n sa-test set image deployment/podinfo podinfod=stefanprodan/podinfo:6.1.0 + +echo '>>> Waiting for initial traffic shift' +retries=50 +count=0 +ok=false +until ${ok}; do + kubectl -n sa-test get canary podinfo -o=jsonpath='{.status.canaryWeight}' | grep '10' && ok=true || ok=false + sleep 5 + kubectl -n flagger-system logs deployment/flagger --tail 1 + count=$(($count + 1)) + if [[ ${count} -eq ${retries} ]]; then + kubectl -n flagger-system logs deployment/flagger + echo "No more retries left" + exit 1 + fi +done + +echo '>>> Verifying session affinity' +if ! URL=http://localhost:8888 HOST=localproject.contour.io VERSION=6.1.0 COOKIE_NAME=flagger-cookie \ + go run ${REPO_ROOT}/test/gatewayapi/verify_session_affinity.go; then + echo "failed to verify session affinity" + exit $? +fi + +echo '>>> Waiting for canary promotion' +retries=50 +count=0 +ok=false +until ${ok}; do + kubectl -n sa-test describe deployment/podinfo-primary | grep '6.1.0' && ok=true || ok=false + sleep 10 + kubectl -n flagger-system logs deployment/flagger --tail 1 + count=$(($count + 1)) + if [[ ${count} -eq ${retries} ]]; then + kubectl -n flagger-system logs deployment/flagger + echo "No more retries left" + exit 1 + fi +done + +display_httproute "sa-test" + +echo '>>> Waiting for canary finalization' +retries=50 +count=0 +ok=false +until ${ok}; do + kubectl -n sa-test get canary/podinfo | grep 'Succeeded' && ok=true || ok=false + sleep 5 + count=$(($count + 1)) + if [[ ${count} -eq ${retries} ]]; then + kubectl -n flagger-system logs deployment/flagger + echo "No more retries left" + exit 1 + fi +done + +echo '>>> Verifying cookie cleanup' +canary_cookie=$(kubectl -n sa-test get canary podinfo -o=jsonpath='{.status.previousSessionAffinityCookie}' | xargs) +echo $canary_cookie +response=$(curl -H "Host: localproject.contour.io" -H "Cookie: $canary_cookie" -D - http://localhost:8888) +echo $response + +if [[ $response == *"$canary_cookie"* ]]; then + echo "✔ Found previous cookie in response" +else + echo "⨯ Previous cookie ${canary_cookie} not found in response" + exit 1 +fi + +if [[ $response == *"Max-Age=-1"* ]]; then + echo "✔ Found Max-Age attribute in cookie" +else + echo "⨯ Max-Age attribute not present in cookie" + exit 1 +fi + +echo '✔ Canary release with session affinity promotion test passed' + +kubectl delete -n sa-test canary podinfo diff --git a/test/gatewayapi/verify_session_affinity.go b/test/gatewayapi/verify_session_affinity.go new file mode 100644 index 000000000..0a1d12eb7 --- /dev/null +++ b/test/gatewayapi/verify_session_affinity.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + "sync" + "time" +) + +var c = make(chan string, 1) +var mu sync.Mutex +var try = true +var timeout = time.Second * 10 + +func main() { + url := os.Getenv("URL") + host := os.Getenv("HOST") + version := os.Getenv("VERSION") + cookieName := os.Getenv("COOKIE_NAME") + + for i := 0; i < 10; i++ { + go tryUntilCanaryIsHit(url, host, version, cookieName) + } + + select { + // If we receive a cookie, then try to verify that we are always routed to the + // Canary deployment based on the cookie. + case cookie := <-c: + mu.Lock() + try = false + mu.Unlock() + + for i := 0; i < 5; i++ { + headers := map[string]string{ + "Cookie": cookie, + } + body, _, err := sendRequest(url, host, headers) + if err != nil { + log.Fatalf("failed to send request to verify cookie based routing: %v", err) + } + if !strings.Contains(body, version) { + log.Fatalf("received response from primary deployment instead of canary deployment") + } + } + + log.Println("✔ successfully verified session affinity") + case <-time.After(timeout): + log.Fatal("timed out waiting for canary hit") + } +} + +// sendRequest sends a request to the URL with the provided host and headers. +// It returns the response body and cookies or an error. +func sendRequest(url, host string, headers map[string]string) (string, []*http.Cookie, error) { + client := http.DefaultClient + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", nil, err + } + + for key, value := range headers { + req.Header.Add(key, value) + } + req.Host = host + + resp, err := client.Do(req) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", nil, err + } + + return string(body), resp.Cookies(), nil +} + +// tryUntilCanaryIsHit is a recursive function that tries to send request and +// either sends the cookie back to the main thread (if received) or re-sends +// the request. +func tryUntilCanaryIsHit(url, host, version, cookieName string) { + mu.Lock() + if !try { + mu.Unlock() + return + } + mu.Unlock() + + body, cookies, err := sendRequest(url, host, nil) + if err != nil { + log.Printf("warning: failed to send request: %s", err) + return + } + if strings.Contains(body, version) { + if cookies[0].Name == cookieName { + c <- fmt.Sprintf("%s=%s", cookies[0].Name, cookies[0].Value) + return + } + } + + tryUntilCanaryIsHit(url, host, version, cookieName) + return +}