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-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 0000000..3e2c8c6 Binary files /dev/null and b/example-docker-compose/on-ready-hooks/__pycache__/02-init-params.cpython-311.pyc differ 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/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"} 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"