← all scripts

.github/scripts/rie_smoke_test.sh

50 of 50 statements covered (100.00%).

coveredmissednever traced by Bash (not counted)A line ending in … continues the statement above it and shares its fate.

18#!/usr/bin/env bash
2# =============================================================================
3# rie_smoke_test.sh — invoke a Lambda container image through the bundled
4# Runtime Interface Emulator and assert the handler's deterministic reply.
5# =============================================================================
6#
7# What it asserts, in order:
8# 1. The image boots the Lambda runtime under the platform's filesystem
9# contract: read-only root with a writable /tmp only (the run also
10# proves the precompiled/PYTHONDONTWRITEBYTECODE images never need
11# bytecode writes at cold start).
12# 2. The RIE endpoint accepts an Invoke within --boot-timeout seconds —
13# i.e. the CMD handler string resolves and the handler module's full
14# import graph loads inside the image, not merely on the CI runner.
15# 3. POSTing --event returns the exact error envelope the handler is
16# designed to raise for that synthetic event (--expect-error-type plus
17# --expect-message-substring), proving request decode and dispatch runs
18# offline with no AWS credentials or network beyond localhost.
19#
20# The probe events are chosen so the handler raises deterministically BEFORE
21# its first AWS SDK call; a handler that suddenly reaches the network here
22# fails the assertion with a different error type, which is the point.
23#
24# Usage:
25# rie_smoke_test.sh --image lambda-img:ci --name lambda-rie --host-port 19000 \
26# --event '{"RequestType": "CiSmoke"}' \
27# --expect-error-type ValueError \
28# --expect-message-substring "Unsupported certificate manager event" \
29# [--env K=V]... [--boot-timeout 90]
30# =============================================================================
3111set -euo pipefail
32
3311IMAGE=""
3411NAME=""
3511HOST_PORT=""
3611EVENT=""
3711EXPECT_ERROR_TYPE=""
3811EXPECT_MESSAGE_SUBSTRING=""
3911BOOT_TIMEOUT="90"
4011ENVS=()
41
4270while [ $# -gt 0 ]; do
4360 case "$1" in
4420 --image) IMAGE="$2"; shift 2 ;;
4520 --name) NAME="$2"; shift 2 ;;
4618 --host-port) HOST_PORT="$2"; shift 2 ;;
4718 --event) EVENT="$2"; shift 2 ;;
4818 --expect-error-type) EXPECT_ERROR_TYPE="$2"; shift 2 ;;
4916 --expect-message-substring) EXPECT_MESSAGE_SUBSTRING="$2"; shift 2 ;;
504 --boot-timeout) BOOT_TIMEOUT="$2"; shift 2 ;;
514 --env) ENVS+=("$2"); shift 2 ;;
522 *) echo "unknown argument: $1" >&2; exit 2 ;;
53 esac
54done
55
5655for required in IMAGE NAME HOST_PORT EVENT EXPECT_ERROR_TYPE EXPECT_MESSAGE_SUBSTRING; do
5755 if [ -z "${!required}" ]; then
586 echo "missing required argument: --$(echo "$required" | tr '[:upper:]_' '[:lower:]-')" >&2
592 exit 2
60 fi
61done
62
638ENV_FLAGS=()
642for kv in ${ENVS[@]+"${ENVS[@]}"}; do
652 ENV_FLAGS+=(--env "$kv")
66done
67
68cleanup() {
6916 docker rm -f "$NAME" >/dev/null 2>&1 || true
70}
718trap cleanup EXIT
72
738cleanup
74# The AWS Lambda base images bundle the Runtime Interface Emulator and their
75# entrypoint execs it whenever AWS_LAMBDA_RUNTIME_API is unset, so a plain
76# `docker run` boots the real runtime API front door on port 8080.
77# --read-only + tmpfs /tmp mirrors the deployed filesystem contract.
788docker run -d \
79 --name "$NAME" \
80 --read-only \
81 --tmpfs /tmp \
82 -p "${HOST_PORT}:8080" \
83 ${ENV_FLAGS[@]+"${ENV_FLAGS[@]}"} \
84 "$IMAGE" >/dev/null
85
868INVOKE_URL="http://127.0.0.1:${HOST_PORT}/2015-03-31/functions/function/invocations"
878RESPONSE=""
888deadline=$((SECONDS + BOOT_TIMEOUT))
8920until RESPONSE="$(curl -sS --max-time 30 -X POST "$INVOKE_URL" -d "$EVENT" 2>/dev/null)" \
906 && [ -n "$RESPONSE" ]; do
914 if [ "$SECONDS" -ge "$deadline" ]; then
921 echo "::error::RIE endpoint for ${IMAGE} did not answer within ${BOOT_TIMEOUT}s" >&2
931 docker logs "$NAME" >&2 || true
941 exit 1
95 fi
966 if [ -z "$(docker ps -q --filter "name=^${NAME}$")" ]; then
971 echo "::error::container ${NAME} exited before serving an invocation" >&2
981 docker logs "$NAME" >&2 || true
991 exit 1
100 fi
1012 sleep 2
102done
103
1046echo "RIE response: $RESPONSE"
105
10624if ! RESPONSE="$RESPONSE" \
107 EXPECT_ERROR_TYPE="$EXPECT_ERROR_TYPE" \
108 EXPECT_MESSAGE_SUBSTRING="$EXPECT_MESSAGE_SUBSTRING" \
109 python3 - <<'PY'
110import json
111import os
112import sys
113
114response = os.environ["RESPONSE"]
115try:
116 envelope = json.loads(response)
117except ValueError:
118 sys.exit(f"RIE reply is not JSON: {response[:400]}")
119if not isinstance(envelope, dict):
120 sys.exit(f"RIE reply is not an error envelope: {response[:400]}")
121
122error_type = envelope.get("errorType", "")
123error_message = envelope.get("errorMessage", "")
124expected_type = os.environ["EXPECT_ERROR_TYPE"]
125expected_substring = os.environ["EXPECT_MESSAGE_SUBSTRING"]
126
127if error_type != expected_type:
128 sys.exit(
129 f"expected errorType {expected_type!r}, got {error_type!r} "
130 f"(message: {error_message[:200]!r})"
131 )
132if expected_substring not in error_message:
133 sys.exit(
134 f"expected errorMessage to contain {expected_substring!r}, "
135 f"got {error_message[:400]!r}"
136 )
137print(f"handler raised {error_type} as designed: {error_message[:120]}")
138PY
139then
1404 docker logs "$NAME" >&2 || true
1414 exit 1
142fi