.github/scripts/rie_smoke_test.sh50 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.
| 1 | 8 | #!/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 | # ============================================================================= | |
| 31 | 11 | set -euo pipefail |
| 32 | ||
| 33 | 11 | IMAGE="" |
| 34 | 11 | NAME="" |
| 35 | 11 | HOST_PORT="" |
| 36 | 11 | EVENT="" |
| 37 | 11 | EXPECT_ERROR_TYPE="" |
| 38 | 11 | EXPECT_MESSAGE_SUBSTRING="" |
| 39 | 11 | BOOT_TIMEOUT="90" |
| 40 | 11 | ENVS=() |
| 41 | ||
| 42 | 70 | while [ $# -gt 0 ]; do |
| 43 | 60 | case "$1" in |
| 44 | 20 | --image) IMAGE="$2"; shift 2 ;; |
| 45 | 20 | --name) NAME="$2"; shift 2 ;; |
| 46 | 18 | --host-port) HOST_PORT="$2"; shift 2 ;; |
| 47 | 18 | --event) EVENT="$2"; shift 2 ;; |
| 48 | 18 | --expect-error-type) EXPECT_ERROR_TYPE="$2"; shift 2 ;; |
| 49 | 16 | --expect-message-substring) EXPECT_MESSAGE_SUBSTRING="$2"; shift 2 ;; |
| 50 | 4 | --boot-timeout) BOOT_TIMEOUT="$2"; shift 2 ;; |
| 51 | 4 | --env) ENVS+=("$2"); shift 2 ;; |
| 52 | 2 | *) echo "unknown argument: $1" >&2; exit 2 ;; |
| 53 | esac | |
| 54 | done | |
| 55 | ||
| 56 | 55 | for required in IMAGE NAME HOST_PORT EVENT EXPECT_ERROR_TYPE EXPECT_MESSAGE_SUBSTRING; do |
| 57 | 55 | if [ -z "${!required}" ]; then |
| 58 | 6 | echo "missing required argument: --$(echo "$required" | tr '[:upper:]_' '[:lower:]-')" >&2 |
| 59 | 2 | exit 2 |
| 60 | fi | |
| 61 | done | |
| 62 | ||
| 63 | 8 | ENV_FLAGS=() |
| 64 | 2 | for kv in ${ENVS[@]+"${ENVS[@]}"}; do |
| 65 | 2 | ENV_FLAGS+=(--env "$kv") |
| 66 | done | |
| 67 | ||
| 68 | cleanup() { | |
| 69 | 16 | docker rm -f "$NAME" >/dev/null 2>&1 || true |
| 70 | } | |
| 71 | 8 | trap cleanup EXIT |
| 72 | ||
| 73 | 8 | cleanup |
| 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. | |
| 78 | 8 | docker 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 | ||
| 86 | 8 | INVOKE_URL="http://127.0.0.1:${HOST_PORT}/2015-03-31/functions/function/invocations" |
| 87 | 8 | RESPONSE="" |
| 88 | 8 | deadline=$((SECONDS + BOOT_TIMEOUT)) |
| 89 | 20 | until RESPONSE="$(curl -sS --max-time 30 -X POST "$INVOKE_URL" -d "$EVENT" 2>/dev/null)" \ |
| 90 | 6 | && [ -n "$RESPONSE" ]; do |
| 91 | 4 | if [ "$SECONDS" -ge "$deadline" ]; then |
| 92 | 1 | echo "::error::RIE endpoint for ${IMAGE} did not answer within ${BOOT_TIMEOUT}s" >&2 |
| 93 | 1 | docker logs "$NAME" >&2 || true |
| 94 | 1 | exit 1 |
| 95 | fi | |
| 96 | 6 | if [ -z "$(docker ps -q --filter "name=^${NAME}$")" ]; then |
| 97 | 1 | echo "::error::container ${NAME} exited before serving an invocation" >&2 |
| 98 | 1 | docker logs "$NAME" >&2 || true |
| 99 | 1 | exit 1 |
| 100 | fi | |
| 101 | 2 | sleep 2 |
| 102 | done | |
| 103 | ||
| 104 | 6 | echo "RIE response: $RESPONSE" |
| 105 | ||
| 106 | 24 | if ! RESPONSE="$RESPONSE" \ |
| 107 | EXPECT_ERROR_TYPE="$EXPECT_ERROR_TYPE" \ | |
| 108 | EXPECT_MESSAGE_SUBSTRING="$EXPECT_MESSAGE_SUBSTRING" \ | |
| 109 | python3 - <<'PY' | |
| 110 | import json | |
| 111 | import os | |
| 112 | import sys | |
| 113 | ||
| 114 | response = os.environ["RESPONSE"] | |
| 115 | try: | |
| 116 | envelope = json.loads(response) | |
| 117 | except ValueError: | |
| 118 | sys.exit(f"RIE reply is not JSON: {response[:400]}") | |
| 119 | if not isinstance(envelope, dict): | |
| 120 | sys.exit(f"RIE reply is not an error envelope: {response[:400]}") | |
| 121 | ||
| 122 | error_type = envelope.get("errorType", "") | |
| 123 | error_message = envelope.get("errorMessage", "") | |
| 124 | expected_type = os.environ["EXPECT_ERROR_TYPE"] | |
| 125 | expected_substring = os.environ["EXPECT_MESSAGE_SUBSTRING"] | |
| 126 | ||
| 127 | if 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 | ) | |
| 132 | if 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 | ) | |
| 137 | print(f"handler raised {error_type} as designed: {error_message[:120]}") | |
| 138 | PY | |
| 139 | then | |
| 140 | 4 | docker logs "$NAME" >&2 || true |
| 141 | 4 | exit 1 |
| 142 | fi |