From e10e97dd27258d3d620cafdefdec1a0c8bf24282 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 07:44:47 +0530 Subject: [PATCH 1/2] Fix the shutdown hook and add CI for both examples Fix the shutdown hook and add CI for both examples Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .github/workflows/main.yml | 102 ++++++++++++++++++ example-docker-compose/README.md | 10 +- .../on-shutdown-hooks/save-pod.sh | 6 +- example-docker-compose/test.sh | 44 ++++++++ example-localstack-cli/README.md | 4 + example-localstack-cli/test.sh | 40 +++++++ 6 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100755 example-docker-compose/test.sh create mode 100755 example-localstack-cli/test.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..e3f28fc --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,102 @@ +name: Test init hooks + +on: + push: + paths-ignore: + - '**.md' + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + lstk: + name: Init hooks with lstk + runs-on: ubuntu-latest + timeout-minutes: 15 + defaults: + run: + working-directory: example-localstack-cli + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install lstk + run: | + npm install -g @localstack/lstk + lstk setup aws --force --non-interactive + + - name: Start LocalStack with the mounted init hook + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + run: lstk start + + - name: Run tests + run: ./test.sh + + - name: Show LocalStack logs + if: failure() + run: lstk logs | tail -200 + + docker-compose: + name: Init hooks with docker compose + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + working-directory: example-docker-compose + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install lstk + run: | + npm install -g @localstack/lstk + lstk setup aws --force --non-interactive + + - name: Start LocalStack + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + run: | + docker compose up -d + for i in $(seq 1 60); do + curl -sf localhost:4566/_localstack/health > /dev/null && exit 0 + sleep 2 + done + docker compose logs + exit 1 + + - name: Run tests + run: ./test.sh + + - name: Stop LocalStack and check the shutdown hook exported the state + run: | + docker compose down --timeout 60 + ls -l volume/my-pod.zip + unzip -l volume/my-pod.zip | grep secretsmanager + + - name: Load the exported state into a fresh LocalStack + env: + LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }} + run: | + lstk start + lstk load ./volume/my-pod.zip + lstk aws secretsmanager get-secret-value --secret-id my-platform-secret --query SecretString --output text | grep hello-there + + - name: Show LocalStack logs + if: failure() + run: | + docker compose logs --tail 200 || true + lstk logs | tail -200 || true diff --git a/example-docker-compose/README.md b/example-docker-compose/README.md index 746e1da..3ce9ffa 100644 --- a/example-docker-compose/README.md +++ b/example-docker-compose/README.md @@ -6,7 +6,7 @@ This example has two scripts that are executed as soon as localstack is ready: * a python script that creates parameters based on a list of values defined in the script And one script that runs when localstack shuts down: -* a shell script that saves the state into `./volume/my-pod.zip` before localstack shuts down +* a shell script that exports the state into `./volume/my-pod.zip` before localstack shuts down ## Start LocalStack @@ -98,5 +98,9 @@ lstk aws ssm get-parameter --name "/my/param-1" ## Shutdown -After you've shut down localstack, you should see a zipfile `my-pod.zip` in `./volume`, which is created by the shutdown hook. -This is a state export that you can import into a fresh localstack instance with `localstack state import <...>`. +After you've shut down localstack, you should see a zipfile `my-pod.zip` in `./volume`, which is created by the shutdown hook through the `/_localstack/pods/state` endpoint. +This is a state export that you can load into a fresh localstack instance with `lstk load ./volume/my-pod.zip`. + +## Test + +With LocalStack running, `./test.sh` checks that both ready hooks succeeded and created the secret and parameters. diff --git a/example-docker-compose/on-shutdown-hooks/save-pod.sh b/example-docker-compose/on-shutdown-hooks/save-pod.sh index fbc5348..34ef541 100755 --- a/example-docker-compose/on-shutdown-hooks/save-pod.sh +++ b/example-docker-compose/on-shutdown-hooks/save-pod.sh @@ -1,5 +1,5 @@ #!/bin/bash -# use the localstack CLI within the container -# and save the state it into the localstack volume which is mounted into `./volume`. -/opt/code/localstack/.venv/bin/localstack state export /var/lib/localstack/my-pod.zip +# export the state through the LocalStack API +# and save it into the localstack volume which is mounted into `./volume`. +curl -sf -o /var/lib/localstack/my-pod.zip http://localhost:4566/_localstack/pods/state diff --git a/example-docker-compose/test.sh b/example-docker-compose/test.sh new file mode 100755 index 0000000..f0f482e --- /dev/null +++ b/example-docker-compose/test.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Check the ready hooks ran and created their resources: +# - 01-init-secrets.sh creates my-platform-secret from MY_SECRET_TOKEN_VALUE +# - 02-init-params.py creates the /my/param-1 and /my/param-2 parameters +set -euo pipefail + +ENDPOINT=${ENDPOINT:-http://localhost:4566} +MAX_ATTEMPTS=${MAX_ATTEMPTS:-60} +export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1 + +ready="" +for attempt in $(seq 1 "$MAX_ATTEMPTS"); do + ready=$(curl -s "$ENDPOINT/_localstack/init/ready" || true) + echo "$ready" | jq -e '.completed == true' > /dev/null 2>&1 && break + echo "Attempt $attempt/$MAX_ATTEMPTS: ready hooks not finished" + sleep 2 +done +echo "$ready" | jq . +for script in 01-init-secrets.sh 02-init-params.py; do + if ! echo "$ready" | jq -e --arg s "$script" 'any(.scripts[]; .name == $s and .state == "SUCCESSFUL")' > /dev/null; then + echo "Test failed: ready hook $script did not succeed" + exit 1 + fi +done +echo "PASS: both ready hooks succeeded" + +secret=$(aws --endpoint-url "$ENDPOINT" secretsmanager get-secret-value --secret-id my-platform-secret \ + --query SecretString --output text) +if [[ "$secret" != *'hello-there'* ]]; then + echo "Test failed: my-platform-secret is '$secret'" + exit 1 +fi +echo "PASS: secret created with the value from MY_SECRET_TOKEN_VALUE" + +for param in "/my/param-1=my-param" "/my/param-2=my-other-param"; do + value=$(aws --endpoint-url "$ENDPOINT" ssm get-parameter --name "${param%%=*}" --query Parameter.Value --output text) + if [ "$value" != "${param#*=}" ]; then + echo "Test failed: ${param%%=*} is '$value'" + exit 1 + fi +done +echo "PASS: SSM parameters created" + +echo "Test passed: the ready hooks seeded LocalStack" diff --git a/example-localstack-cli/README.md b/example-localstack-cli/README.md index ea1f2bb..aafdc25 100644 --- a/example-localstack-cli/README.md +++ b/example-localstack-cli/README.md @@ -45,3 +45,7 @@ You can check that the stage was executed successfully: ] } ``` + +## Test + +With LocalStack running, `./test.sh` checks that `init-vpc.sh` succeeded and created the VPC with three isolated subnets. diff --git a/example-localstack-cli/test.sh b/example-localstack-cli/test.sh new file mode 100755 index 0000000..40956c5 --- /dev/null +++ b/example-localstack-cli/test.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Check the init-vpc.sh ready hook mounted through .lstk/config.toml ran and +# created the VPC with its three tagged subnets. +set -euo pipefail + +ENDPOINT=${ENDPOINT:-http://localhost:4566} +MAX_ATTEMPTS=${MAX_ATTEMPTS:-60} +VPC_NAME=${VPC_NAME:-MyVPC} + +ready="" +for attempt in $(seq 1 "$MAX_ATTEMPTS"); do + ready=$(curl -s "$ENDPOINT/_localstack/init/ready" || true) + echo "$ready" | jq -e '.completed == true' > /dev/null 2>&1 && break + echo "Attempt $attempt/$MAX_ATTEMPTS: ready hooks not finished" + sleep 2 +done +echo "$ready" | jq . +if ! echo "$ready" | jq -e 'any(.scripts[]; .name == "init-vpc.sh" and .state == "SUCCESSFUL")' > /dev/null; then + echo "Test failed: ready hook init-vpc.sh did not succeed" + exit 1 +fi +echo "PASS: init-vpc.sh succeeded" + +vpc_id=$(lstk aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$VPC_NAME" --query 'Vpcs[0].VpcId' --output text) +if [ -z "$vpc_id" ] || [ "$vpc_id" = "None" ]; then + echo "Test failed: no VPC tagged Name=$VPC_NAME" + exit 1 +fi +echo "PASS: found VPC $vpc_id" + +subnets=$(lstk aws ec2 describe-subnets \ + --filters "Name=vpc-id,Values=$vpc_id" "Name=tag:aws-cdk:subnet-type,Values=Isolated" \ + --query 'length(Subnets)' --output text) +if [ "$subnets" != "3" ]; then + echo "Test failed: expected 3 isolated subnets in $vpc_id, found $subnets" + exit 1 +fi +echo "PASS: VPC has 3 isolated subnets" + +echo "Test passed: the init hook created the VPC" From 04f04d16fe81e4fb8f3403d6e17fb3c55f0cdde4 Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Tue, 13 Oct 2026 08:24:20 +0530 Subject: [PATCH 2/2] Default region and credentials in the init hooks Current images do not pass AWS settings to init scripts, so the aws CLI failed with 'You must specify a region' and boto3 with 'Unable to locate credentials'. Fail the shell hooks on the first error so a broken hook is not reported as successful. Claude-Session: https://claude.ai/code/session_01DZfDmzUBPqNAu3yFKyfCNM --- .../on-ready-hooks/01-init-secrets.sh | 6 ++++++ .../on-ready-hooks/02-init-params.py | 2 ++ .../__pycache__/02-init-params.cpython-311.pyc | Bin 0 -> 959 bytes example-localstack-cli/init-vpc.sh | 6 ++++++ 4 files changed, 14 insertions(+) create mode 100644 example-docker-compose/on-ready-hooks/__pycache__/02-init-params.cpython-311.pyc diff --git a/example-docker-compose/on-ready-hooks/01-init-secrets.sh b/example-docker-compose/on-ready-hooks/01-init-secrets.sh index 325f98b..ab646f1 100755 --- a/example-docker-compose/on-ready-hooks/01-init-secrets.sh +++ b/example-docker-compose/on-ready-hooks/01-init-secrets.sh @@ -1,4 +1,10 @@ #!/bin/bash +set -eo pipefail + +# Init hooks do not inherit AWS settings, so default to the LocalStack test credentials +export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-us-east-1} +export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-test} +export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-test} aws --endpoint-url=http://localhost:4566 secretsmanager create-secret --name my-platform-secret aws --endpoint-url=http://localhost:4566 secretsmanager put-secret-value --secret-id my-platform-secret --secret-string "{\"MY_TOKEN\": \"${MY_SECRET_TOKEN_VALUE}\"}" diff --git a/example-docker-compose/on-ready-hooks/02-init-params.py b/example-docker-compose/on-ready-hooks/02-init-params.py index 8096853..eda2372 100644 --- a/example-docker-compose/on-ready-hooks/02-init-params.py +++ b/example-docker-compose/on-ready-hooks/02-init-params.py @@ -3,6 +3,8 @@ os.environ["AWS_ENDPOINT_URL"] = "http://localhost:4566" os.environ["AWS_DEFAULT_REGION"] = "us-east-1" +os.environ.setdefault("AWS_ACCESS_KEY_ID", "test") +os.environ.setdefault("AWS_SECRET_ACCESS_KEY", "test") ssm = boto3.client("ssm") diff --git a/example-docker-compose/on-ready-hooks/__pycache__/02-init-params.cpython-311.pyc b/example-docker-compose/on-ready-hooks/__pycache__/02-init-params.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e2c8c6dd1cbc8e810b4891d7da2d323b1abe69a GIT binary patch literal 959 zcma)3&uZTkO_i(=;Xo&@?vG6snPE$jXdDyUT8NhekN9 zNsk`9=|K}tScCtH3EPmA%&CbdPm=W3le16=jThhSz8~NB-rM(PzlA~pz~t`Np9g;+ z0KeQ|B*K+*vB8*6009J5P#9WRfQZB3fEy(vev7%ta}D#l*d)^ANBNT$323UtIb83W7+7=7iV;~J)MPIV13ci9L zdz*v18k1KJ_F+E?{g`gC-P+^*Ao#F^U=GA?{C4xQjYydgV>2FC^^&5Nb&G7?Ur!|H zO!9fIn8~J}?rvxE#r?gVhQDS>Sh0w-O2aOh&OA!)@8pYnnaA6^Svu=t$y6$n%N3tw zUKF>})I+dEXv8IQnbck;KfzPpvb2Uzt4oGrDq3Tjc{(X$a~Q|Ch8WlC@c}8VP6jd+ za>T4u-^O_A$toJA-e-zh!_I)Z;SmiPHBuat-@%1@U6&8=J4G{8EFryg$c8QHnxR`b zu2&@!E2u7&b^Xwa-;T(zm7QsHWnF=1tbe{q>f^RG)_s_JH3WoE`|; zgL8g+!{&|y9Wm;FrQV&DVQ|X<%RRvGGY~lik|Fo= literal 0 HcmV?d00001 diff --git a/example-localstack-cli/init-vpc.sh b/example-localstack-cli/init-vpc.sh index 5bc97ab..d6441d1 100755 --- a/example-localstack-cli/init-vpc.sh +++ b/example-localstack-cli/init-vpc.sh @@ -1,4 +1,10 @@ #!/bin/bash +set -eo pipefail + +# Init hooks do not inherit AWS settings, so default to the LocalStack test credentials +export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-us-east-1} +export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-test} +export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-test} # variables that can be overwritten via environment variables vpc_name=${VPC_NAME-"MyVPC"}