From 863626b3f607d5c9299cb0dfb8e6f0d423d32ce3 Mon Sep 17 00:00:00 2001 From: justinsb Date: Tue, 7 Jul 2026 18:12:03 -0400 Subject: [PATCH] tests: add simple e2e test for guestbook This highlights a few issues (out of date images, etc.), and will allow us to fix them with test coverage. --- .gitignore | 3 + tests/e2e/e2e-kind | 47 ++++++++++ web/guestbook/e2e/README.md | 37 ++++++++ web/guestbook/e2e/test-kind | 176 ++++++++++++++++++++++++++++++++++++ 4 files changed, 263 insertions(+) create mode 100755 tests/e2e/e2e-kind create mode 100644 web/guestbook/e2e/README.md create mode 100755 web/guestbook/e2e/test-kind diff --git a/.gitignore b/.gitignore index 6ef5822c8..0083cf91d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ cscope.* /bazel-* *.pyc + +# Build artifacts +.build/ diff --git a/tests/e2e/e2e-kind b/tests/e2e/e2e-kind new file mode 100755 index 000000000..acfbbadb9 --- /dev/null +++ b/tests/e2e/e2e-kind @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# +# Entry point for running e2e tests with kind in prow. + +set -euo pipefail + +CLUSTER_NAME="${CLUSTER_NAME:-e2e}" + +REPO_ROOT=$(git rev-parse --show-toplevel) +cd "${REPO_ROOT}" + +BUILD_DIR="${REPO_ROOT}/.build" +BIN_DIR="${BUILD_DIR}/bin" +mkdir -p "${BIN_DIR}" + +# Download kind +KIND_VERSION="v0.32.0" +KIND_URL="https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64" +curl -Lo "${BIN_DIR}/kind" "${KIND_URL}" +chmod +x "${BIN_DIR}/kind" + +# Make sure kind is in the path +export PATH="${BIN_DIR}:${PATH}" + +require() { + command -v "$1" >/dev/null 2>&1 || { echo "Required command not found: $1"; exit 1; } +} +require docker +require kind +require kubectl + + +# Create a kind cluster +echo "Creating kind cluster '${CLUSTER_NAME}'" +kind create cluster --name "${CLUSTER_NAME}" --wait 120s + +# Delete the kind cluster when we're done (usually) +cleanup() { + echo "Deleting kind cluster '${CLUSTER_NAME}'" + kind delete cluster --name "${CLUSTER_NAME}" >/dev/null 2>&1 || true +} +trap cleanup EXIT + + +# Run the per-app e2e tests +echo "Running e2e tests" +web/guestbook/e2e/test-kind \ No newline at end of file diff --git a/web/guestbook/e2e/README.md b/web/guestbook/e2e/README.md new file mode 100644 index 000000000..fb7e27f1f --- /dev/null +++ b/web/guestbook/e2e/README.md @@ -0,0 +1,37 @@ +# Guestbook end-to-end smoke test + +`test` is a self-contained smoke test that verifies the guestbook example +actually works. + +It assumes a kind cluster has already been created and is called `e2e`; this can be changed with the `CLUSTER_NAME` environment variable. + +It: + +1. builds the images from source, +2. loads those images into the cluster, +3. deploys the guestbook manifests, +4. waits for everything to become ready, and +5. POSTs a message through the frontend and reads it back. + +Because the read is served by the redis *replica*, a successful round trip +also proves redis replication is working. + +## Running it locally + +You should create a kind cluster first with `kind create cluster --name e2e`, then run + +```console +$ web/guestbook/e2e/test-kind +``` + +## CI + +Prow runs this test via the main `tests/e2e/e2e-kind` entry point. + +## Known issue: the redis-master image + +The manifests reference `registry.k8s.io/redis:e2e` for the master, but that +image is no longer pullable by modern container runtimes (it ships an ancient +Docker v1 schema manifest). The test overrides it with `redis:3.2.9` — the same +version the redis-replica image is built from — so the example runs. Updating +the manifest itself is a separate follow-up. diff --git a/web/guestbook/e2e/test-kind b/web/guestbook/e2e/test-kind new file mode 100755 index 000000000..4f0613e9f --- /dev/null +++ b/web/guestbook/e2e/test-kind @@ -0,0 +1,176 @@ +#!/usr/bin/env bash +# +# End-to-end smoke test for the PHP / Redis guestbook example. + +set -euo pipefail + + +REPO_ROOT=$(git rev-parse --show-toplevel) +GUESTBOOK_DIR="${REPO_ROOT}/web/guestbook" +cd "${GUESTBOOK_DIR}" + + +CLUSTER_NAME="${CLUSTER_NAME:-e2e}" +REDIS_MASTER_IMAGE="${REDIS_MASTER_IMAGE:-redis:3.2.9}" + +# Local port used for the kubectl port-forward to the frontend service. +FRONTEND_LOCAL_PORT="${FRONTEND_LOCAL_PORT:-18080}" + +PORT_FORWARD_PID="" + +log() { echo -e "\n\033[1;34m==> $*\033[0m"; } +err() { echo -e "\033[1;31m$*\033[0m" >&2; } + +# Read the frontend / replica image names straight out of the manifests so the +# images we build and load always match what actually gets deployed, even if +# someone bumps the tags later. +image_from_manifest() { + # $1 = manifest file, $2 = substring identifying the image + awk -v pat="$2" '$1=="image:" && $2 ~ pat {print $2; exit}' "$1" +} + +FRONTEND_IMAGE="$(image_from_manifest "${GUESTBOOK_DIR}/frontend-deployment.yaml" gb-frontend)" +REDIS_REPLICA_IMAGE="$(image_from_manifest "${GUESTBOOK_DIR}/redis-replica-deployment.yaml" gb-redisslave)" + +if [[ -z "${FRONTEND_IMAGE}" || -z "${REDIS_REPLICA_IMAGE}" ]]; then + err "Could not determine image names from the manifests." + exit 1 +fi + +dump_diagnostics() { + err "Test failed - dumping cluster state for debugging:" + kubectl get pods -o wide || true + kubectl get events --sort-by=.lastTimestamp | tail -n 30 || true + for d in redis-master redis-replica frontend; do + echo "--- describe deployment/${d} ---" + kubectl describe deployment "${d}" || true + echo "--- logs deployment/${d} ---" + kubectl logs "deployment/${d}" --tail=40 || true + done +} + +cleanup() { + local ec=$? + if [[ -n "${PORT_FORWARD_PID}" ]]; then + kill "${PORT_FORWARD_PID}" 2>/dev/null || true + fi + if [[ ${ec} -ne 0 ]]; then + dump_diagnostics || true + fi + exit "${ec}" +} +trap cleanup EXIT + + +# --------------------------------------------------------------------------- +log "Guestbook e2e configuration" +echo " kind cluster name : ${CLUSTER_NAME}" +echo " frontend image : ${FRONTEND_IMAGE}" +echo " redis-replica image: ${REDIS_REPLICA_IMAGE}" +echo " redis-master image : ${REDIS_MASTER_IMAGE}" + + +# --------------------------------------------------------------------------- +log "Building images from source" +docker build -t "${FRONTEND_IMAGE}" "${GUESTBOOK_DIR}/php-redis" +docker build -t "${REDIS_REPLICA_IMAGE}" "${GUESTBOOK_DIR}/redis-slave" + +log "Pulling redis master image (${REDIS_MASTER_IMAGE})" +docker pull "${REDIS_MASTER_IMAGE}" + +# --------------------------------------------------------------------------- +log "Loading images into the kind cluster" +kind load docker-image --name "${CLUSTER_NAME}" \ + "${FRONTEND_IMAGE}" "${REDIS_REPLICA_IMAGE}" "${REDIS_MASTER_IMAGE}" + +# --------------------------------------------------------------------------- +log "Deploying the guestbook" +kubectl apply \ + -f "${GUESTBOOK_DIR}/redis-master-deployment.yaml" \ + -f "${GUESTBOOK_DIR}/redis-master-service.yaml" \ + -f "${GUESTBOOK_DIR}/redis-replica-deployment.yaml" \ + -f "${GUESTBOOK_DIR}/redis-replica-service.yaml" \ + -f "${GUESTBOOK_DIR}/frontend-deployment.yaml" \ + -f "${GUESTBOOK_DIR}/frontend-service.yaml" + +# Override the unpullable registry.k8s.io/redis:e2e master image. See the note +# at the top of this file. +log "Overriding redis-master image with ${REDIS_MASTER_IMAGE}" +kubectl set image deployment/redis-master "master=${REDIS_MASTER_IMAGE}" + +# --------------------------------------------------------------------------- +log "Waiting for deployments to become ready" +for d in redis-master redis-replica frontend; do + kubectl rollout status "deployment/${d}" --timeout=180s +done + +# --------------------------------------------------------------------------- +log "Waiting for the redis replica to sync from the master" +replication_up() { + local replica + replica="$(kubectl get pod -l role=replica -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)" + [[ -n "${replica}" ]] || return 1 + kubectl exec "${replica}" -- redis-cli info replication 2>/dev/null \ + | grep -q 'master_link_status:up' +} +for _ in $(seq 1 30); do + if replication_up; then + echo " replica is in sync with the master" + break + fi + sleep 2 +done +if ! replication_up; then + err "redis replica never reached master_link_status:up" + exit 1 +fi + +# --------------------------------------------------------------------------- +log "Port-forwarding the frontend service to localhost:${FRONTEND_LOCAL_PORT}" +kubectl port-forward svc/frontend "${FRONTEND_LOCAL_PORT}:80" >/dev/null 2>&1 & +PORT_FORWARD_PID=$! + +base_url="http://localhost:${FRONTEND_LOCAL_PORT}" + +# Wait until the port-forward is actually serving. +ready="" +for _ in $(seq 1 30); do + if curl -fsS -o /dev/null "${base_url}/guestbook.php?cmd=get&key=messages" 2>/dev/null; then + ready=1 + break + fi + sleep 1 +done +if [[ -z "${ready}" ]]; then + err "frontend did not become reachable via port-forward" + exit 1 +fi + +# --------------------------------------------------------------------------- +# The actual smoke test: write a message, then read it back. +message="smoketest-$$-${RANDOM}" + +log "POSTing a message (cmd=set) via the frontend" +set_resp="$(curl -fsS "${base_url}/guestbook.php?cmd=set&key=messages&value=${message}")" +echo " response: ${set_resp}" +if [[ "${set_resp}" != *'"message": "Updated"'* ]]; then + err "unexpected response to set: ${set_resp}" + exit 1 +fi + +log "Reading the message back (cmd=get) via the frontend" +get_resp="" +for _ in $(seq 1 15); do + get_resp="$(curl -fsS "${base_url}/guestbook.php?cmd=get&key=messages" || true)" + if [[ "${get_resp}" == *"${message}"* ]]; then + break + fi + sleep 1 +done +echo " response: ${get_resp}" +if [[ "${get_resp}" != *"${message}"* ]]; then + err "message '${message}' was not returned by the guestbook (got: ${get_resp})" + exit 1 +fi + +log "SUCCESS: the guestbook stored and returned the message."