Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Deploy and test on LocalStack

on:
push:
paths-ignore:
- '**.md'
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:

permissions:
contents: read

jobs:
example:
name: ${{ matrix.example }}
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
example:
- appsync-dynamodb
- aws-apigateway-auth0
- aws-lambda-stepfunctions
- elastic-kubernetes-service
- lambda-kinesis-firehose-es
- lambda-layers
- s3-sqs-queues
- s3-static-website
- serverless-rest-api
- sns-sqs-subscription-lambda-trigger
env:
EXAMPLE: ${{ matrix.example }}
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install the Pulumi CLI
uses: pulumi/actions@v6

- name: Install lstk and pulumilocal
run: |
npm install -g @localstack/lstk
lstk setup aws --force --non-interactive
pip install pulumi-local

- name: Install the example's dependencies
run: make install

- name: Start LocalStack
env:
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
run: lstk start

- name: Deploy the example
run: make deploy

- name: Run tests
run: make test

- name: Show LocalStack logs
if: failure()
run: lstk logs | tail -300
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
venv/
__pycache__/
*.pyc
.pulumi-state/
40 changes: 40 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Deploy and test one example on LocalStack, for example:
# make install deploy test EXAMPLE=s3-static-website
EXAMPLE ?=
STACK ?= localstack

# Keep Pulumi state on disk and skip pulumilocal's prompt to overwrite the stack config
export PULUMI_BACKEND_URL ?= file://$(CURDIR)/.pulumi-state
export PULUMI_CONFIG_PASSPHRASE ?= localstack
export NON_INTERACTIVE ?= 1
export AWS_REGION ?= us-east-1

EXAMPLES := $(patsubst %/Pulumi.yaml,%,$(wildcard */Pulumi.yaml))

usage:
@echo "Usage: make install|deploy|test|destroy EXAMPLE=<example>"
@echo "Examples: $(EXAMPLES)"

check:
@test -n "$(EXAMPLE)" -a -f "$(EXAMPLE)/Pulumi.yaml" || { echo "Set EXAMPLE to one of: $(EXAMPLES)"; exit 1; }

install: check
cd $(EXAMPLE) && pulumi install

deploy: check
mkdir -p .pulumi-state
cd $(EXAMPLE) && pulumi stack select --create $(STACK)
cd $(EXAMPLE) && pulumi config set aws:region $(AWS_REGION)
# pulumilocal skips the account ID lookup by default, which leaves the account ID empty in
# ARNs the provider builds itself (SNS topics fail with "'' is not a valid AWS account ID")
cd $(EXAMPLE) && pulumi config set aws:skipRequestingAccountId false
cd $(EXAMPLE) && pulumilocal up --yes --skip-preview

test: check
cd $(EXAMPLE) && STACK=$(STACK) ./test.sh

destroy: check
cd $(EXAMPLE) && pulumilocal destroy --yes --skip-preview
cd $(EXAMPLE) && pulumi stack rm --yes --preserve-config $(STACK)

.PHONY: usage check install deploy test destroy
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Each example in the repository is self-contained and can be deployed individuall

- A valid [LocalStack for AWS license](https://localstack.cloud/pricing). Your license provides a [`LOCALSTACK_AUTH_TOKEN`](https://docs.localstack.cloud/getting-started/auth-token/).
- [Docker](https://docs.docker.com/get-docker/)
- [`localstack` CLI](https://docs.localstack.cloud/getting-started/installation/#localstack-cli)
- [`awslocal` CLI](https://docs.localstack.cloud/user-guide/integrations/aws-cli/)
- [`lstk`](https://docs.localstack.cloud/aws/developer-tools/running-localstack/lstk/) or the [`localstack` CLI](https://docs.localstack.cloud/getting-started/installation/#localstack-cli)
- [`awslocal` CLI](https://docs.localstack.cloud/user-guide/integrations/aws-cli/), or `lstk aws`
- [Pulumi](https://www.pulumi.com/docs/get-started/install/)
- [`pulumilocal` CLI](https://github.com/localstack/pulumi-local)
- `make` and `jq`
Expand All @@ -30,6 +30,20 @@ localstack auth set-token <your-auth-token>

You can find your auth token in the [LocalStack Web Application](https://app.localstack.cloud/workspace/auth-token).

## Deploying and testing an example

The `Makefile` deploys one example to LocalStack with a local Pulumi backend and runs its `test.sh`:

```bash
lstk start
make install deploy test EXAMPLE=s3-static-website
make destroy EXAMPLE=s3-static-website
```

`make deploy` uses the stack `localstack` and stores Pulumi state in `.pulumi-state/`. Each `test.sh` checks the example works the way its README describes, for example that the API counts requests or that a published message reaches the result queue. CI runs every example this way on each pull request.

The test scripts need `lstk`, `jq` and `curl`. The EKS test also needs `kubectl` and the AWS CLI.

## Outline

| Sample Name | Description |
Expand Down
17 changes: 17 additions & 0 deletions appsync-dynamodb/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Run the Automation API integration tests in test_appsync.py. They re-run `up` on the
# LocalStack-configured stack, add a tenant through GraphQL, check DynamoDB, then destroy the stack.
set -euo pipefail

STACK=${STACK:-localstack}
if [ "$STACK" != "localstack" ]; then
echo "test_appsync.py uses the stack named localstack"
exit 1
fi

# boto3 in the tests needs credentials; LocalStack accepts any
export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-test}
export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-test}

venv/bin/pip install --quiet boto3 requests
venv/bin/python -m unittest -v test_appsync
8 changes: 7 additions & 1 deletion aws-apigateway-auth0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ pulumilocal config set aws:region us-east-1
pulumilocal up
```

After deployment, run `pulumilocal stack output url` to get the API URL and test it with an `Authorization` header expected by the authorizer logic.
After deployment, get the API ID from `pulumilocal stack output url` (`https://<api-id>.execute-api.us-east-1.amazonaws.com/stage/`) and call the route through LocalStack. The authorizer only allows the token `Bearer goodToken`:

```bash
curl -H "Authorization: Bearer goodToken" http://<api-id>.execute-api.localhost.localstack.cloud:4566/stage/hello # 200, <h1>Hello world!</h1>
curl -H "Authorization: Bearer badToken" http://<api-id>.execute-api.localhost.localstack.cloud:4566/stage/hello # 404, awsx maps ACCESS_DENIED to 404
curl http://<api-id>.execute-api.localhost.localstack.cloud:4566/stage/hello # 401
```
11 changes: 4 additions & 7 deletions aws-apigateway-auth0/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,15 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import * as awsx from "@pulumi/awsx";
import * as awsx from "@pulumi/awsx/classic";
import * as pulumi from "@pulumi/pulumi";

import * as jwt from "jsonwebtoken";
import * as jwksClient from "jwks-rsa";
import * as util from "util";

const config = new pulumi.Config();

const authorizerLambda = async (event: awsx.apigateway.AuthorizerEvent) => {
const authorizerLambda = async (event: awsx.apigateway.AuthorizerEvent): Promise<awsx.apigateway.AuthorizerResponse> => {
try {
const effect = event.headers?.Authorization === "goodToken" ? "Allow" : "Deny";
// A TOKEN authorizer gets the header value in event.authorizationToken, not in event.headers
const effect = getToken(event) === "goodToken" ? "Allow" : "Deny";
return {
principalId: "my-user",
policyDocument: {
Expand Down
Loading
Loading