Skip to content

Commit

Permalink
[MS-755] feat: Add aws local developemnt tools
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrgrundas committed Oct 4, 2024
1 parent 5a559d4 commit 02f0afd
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 114 deletions.
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
SALEOR_URL=https://your.eu.saleor.cloud
STATIC_URL=https://your.storefront.com/public/emails
SQS_QUEUE_URL=
FROM_EMAIL=
SQS_QUEUE_URL=http://localhost:4566/000000000000/nimara-mailer-queue
FROM_EMAIL=hello@mirumee.com
FROM_DOMAIN=mirumee.com # Required only for localstack
FROM_NAME=

AWS_ACCESS_KEY_ID=
AWS_REGION=
AWS_SECRET_ACCESS_KEY=
AWS_ENDPOINT_URL=http://localhost:4566 # Required only for localstack
SECRET_MANAGER_APP_CONFIG_PATH=nimara-mailer
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ yarn-error.log*
vite.config.ts.timestamp-*

!build.sh

localstack
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,57 @@ Alternatively, you can use docker to run the app.

2. `docker compose build` - build the app.
3. `docker compose run --rm --service-ports app` - run the app.

## Localstack

### Install [awscli-local](https://github.com/localstack/awscli-local)

```
$ brew install awscli-local
```

or

```
$ pip3 install awscli-local
```

### Running localstack

To run localstack in the background:

```
$ docker compose up localstack -d
```

On startup, a queue will be created automatically with name `nimara-mailer-queue`.

Also the script will confirm email (hello@mirumee.com) identity and domain (mirumee.com) identity.

Check the [init-aws.sh](/etc/init-aws.sh) script for more details.

### Helpful commands:

Creating queue:

```
$ awslocal sqs create-queue --region ap-southeast-1 --queue-name nimara-mailer-queue
```

Purging queue:

```
$ awslocal sqs purge-queue --region ap-southeast-1 --queue-url http://localhost:4566/000000000000/nimara-mailer-queue
```

Verifying email identity:

```
$ awslocal ses verify-email-identity --region ap-southeast-1 --email-address hello@mirumee.com --endpoint-url=http://localhost:4566
```

Verifying domain identity:

```
$ awslocal ses verify-domain-identity --region ap-southeast-1 --domain mirumee.com --endpoint-url=http://localhost:4566
```
37 changes: 27 additions & 10 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,32 @@ services:
env_file:
- .env
environment:
LOG_LEVEL: info
NODE_ENV: development
SALEOR_URL: ${SALEOR_URL:?SALEOR_URL env var is required}
STATIC_URL: ${STATIC_URL:?STATIC_URL env is required}
SQS_QUEUE_URL: ${SQS_QUEUE_URL:?SQS_QUEUE_URL env is required}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID?:AWS_ACCESS_KEY_ID env is required}
AWS_REGION: ${AWS_REGION?:AWS_REGION env is required}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY?:AWS_SECRET_ACCESS_KEY env is required}
SECRET_MANAGER_APP_CONFIG_PATH: ${SECRET_MANAGER_APP_CONFIG_PATH?:SECRET_MANAGER_APP_CONFIG_PATH env is required}

- LOG_LEVEL=info
- NODE_ENV=development
- SALEOR_URL=${SALEOR_URL:?SALEOR_URL env var is required}
- STATIC_URL=${STATIC_URL:?STATIC_URL env is required}
- SQS_QUEUE_URL=${SQS_QUEUE_URL:?SQS_QUEUE_URL env is required}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID?:AWS_ACCESS_KEY_ID env is required}
- AWS_REGION=${AWS_REGION?:AWS_REGION env is required}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY?:AWS_SECRET_ACCESS_KEY env is required}
- SECRET_MANAGER_APP_CONFIG_PATH=${SECRET_MANAGER_APP_CONFIG_PATH?:SECRET_MANAGER_APP_CONFIG_PATH env is required}
volumes:
- ./src/:/app/src/

localstack:
container_name: "${LOCALSTACK_DOCKER_NAME:-localstack}"
image: localstack/localstack:latest
environment:
# LocalStack configuration: https://docs.localstack.cloud/references/configuration/
- AWS_DEFAULT_REGION=${AWS_REGION?:AWS_REGION env is required}
- SERVICES=sqs,ses
- DEBUG=${DEBUG:-0}
- FROM_EMAIL=${FROM_EMAIL?:FROM_DOMAIN env is required}
- FROM_DOMAIN=${FROM_DOMAIN?:FROM_DOMAIN env is required for running SES locally}
ports:
- "127.0.0.1:4566:4566" # LocalStack Gateway
- "127.0.0.1:4510-4559:4510-4559" # external services port range
volumes:
- "./localstack:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
- "./etc/init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh" # ready hook
27 changes: 27 additions & 0 deletions etc/init-aws.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash
set -eo pipefail

# https://docs.localstack.cloud/references/init-hooks/

SEPARATOR='----------------------------------------------------------------------------'
ENDPOINT_URL="http://localhost:4566"
QUEUE_NAME="nimara-mailer-queue"

echo -e "\n"
echo $SEPARATOR
echo -e "Running AWS create queue command for ${QUEUE_NAME}.\n"
aws sqs create-queue --region ${AWS_DEFAULT_REGION} --endpoint-url=${ENDPOINT_URL} --queue-name ${QUEUE_NAME}
echo -e "\nCreated queue ${QUEUE_NAME}"


echo $SEPARATOR
echo -e "Running AWS verify email identity command for ${FROM_EMAIL}.\n"
aws ses verify-email-identity --region ${AWS_DEFAULT_REGION} --endpoint-url=${ENDPOINT_URL} --email-address ${FROM_EMAIL}
echo -e "\nVerified ${FROM_EMAIL}"

echo $SEPARATOR
echo -e "Running AWS verify domain identity command for ${FROM_DOMAIN}.\n"
aws ses verify-domain-identity --region ${AWS_DEFAULT_REGION} --endpoint-url=${ENDPOINT_URL} --domain ${FROM_DOMAIN}
echo -e "\nVerified ${FROM_DOMAIN}"

echo $SEPARATOR
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"lint-staged": "^15.2.10",
"nodemon": "^3.1.7",
"prettier": "^3.3.2",
"sqs-consumer": "^11.1.0",
"ts-essentials": "^10.0.1",
"typescript-eslint": "^7.18.0",
"vitest": "^2.0.3"
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/api/rest/saleor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const saleorRoutes: FastifyPluginAsync = async (
"MANAGE_GIFT_CARD",
"MANAGE_ORDERS",
"MANAGE_PRODUCTS",
"MANAGE_SHIPPING",
],
tokenTargetUrl: request.urlFor("saleor:register"),
version: CONFIG.VERSION,
Expand Down
22 changes: 1 addition & 21 deletions src/api/rest/saleor/webhooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReceiveMessageCommand, SendMessageCommand } from "@aws-sdk/client-sqs";
import { SendMessageCommand } from "@aws-sdk/client-sqs";
import type { FastifyPluginAsync } from "fastify/types/plugin";
import rawBody from "fastify-raw-body";
import type { ZodTypeProvider } from "fastify-type-provider-zod";
Expand Down Expand Up @@ -117,26 +117,6 @@ export const webhooks: FastifyPluginAsync = async (fastify) => {

await fastify.sqs.send(command);

/**
* FIXME: Remove when proxy setup is ready.
* Temporary solution to mimic proxy behavior.
*/
const { Messages } = await fastify.sqs.send(
new ReceiveMessageCommand({ QueueUrl: CONFIG.SQS_QUEUE_URL })
);
await fetch(`http://0.0.0.0:${CONFIG.PROXY_PORT}`, {
method: "POST",
body: JSON.stringify({
format: "application/vnd.mirumee.nimara.event_proxy.v1+json",
event: {
Records: Messages,
},
}),
headers: {
"Content-Type": "application/json",
},
});

return reply.status(200).send({ status: "ok" });
}
);
Expand Down
99 changes: 34 additions & 65 deletions src/emails-sender-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,40 @@
import http, { type IncomingMessage } from "http";
import { SQSClient } from "@aws-sdk/client-sqs";
import { type Context, type SQSEvent, type SQSRecord } from "aws-lambda";
import { Consumer } from "sqs-consumer";

import { CONFIG } from "@/config";
import { handler, logger } from "@/emails-sender";
import { getJSONFormatHeader } from "@/lib/saleor/apps/utils";

/**
{
"format":"application/vnd.mirumee.nimara.event_proxy.v1+json",
"event":{
"Records":[
{
"messageId":"231b2b63-61d7-4aee-8c5f-41ec515637e5",
"receiptHandle":"AQEBuKEWUWVS2EIIN6TH3tCvQUS0u9ZnBihWHNuIiHN2pi7UZ2FqZgwvYOpuraVopaN2fVdoCuqSHIfQyYk/YviQLWiQkItLVV7UygQ8pM+lTuCbBFQkIEP9fMA25mHojkR2PROb2Jz+nZb3tQl/LZ1QjR+Y97cpBTegTeEHCnf2IJc/3TxWV3UuNid+BCTfW/2stA2xy5y5BjBHkxO9nG62ohD7abxOyWXXKEQAjtjI9WwVsF4MLSTUgP9n6rY417NRJIXzMvE1lJa+oli/U0IJllLcihchupoHn3VsniFFr2GOu4EUZPZ9SU5aM7y0pAsstHlQrqpdW+4en3LJbZ/acmhw01N4ABPeSND+md0+6cKnW5lqY9ShfBgz/FXCFNl3KNn5XSfhtmisKO0+GjezBg==",
"body":"example body",
"attributes":{
"ApproximateReceiveCount":"1",
"SentTimestamp":"1725968725915",
"SenderId":"AROAY2QPQY6ZKITOQN3Q4:piotr.grundas@mirumee.com",
"ApproximateFirstReceiveTimestamp":"1725968725922"
},
"messageAttributes":{
},
"md5OfBody":"358f217052892dd75464e55c13cbde78",
"eventSource":"aws:sqs",
"eventSourceARN":"arn:aws:sqs:eu-central-1:606696687538:peteTSBEApp",
"awsRegion":"eu-central-1"
}
]
}
}
*/

const proxyEventToLambdaHandler = async (request: IncomingMessage) => {
/**
* Passthrough event data from the event proxy to the handler.
*/
let body = "";

request.on("data", (chunk) => {
body += chunk;
const app = Consumer.create({
queueUrl: CONFIG.SQS_QUEUE_URL,
sqs: new SQSClient({
useQueueUrlAsEndpoint: false,
endpoint: CONFIG.SQS_QUEUE_URL,
}),
handleMessageBatch: async (messages) => {
const event: SQSEvent = {
Records: messages as SQSRecord[],
};
const context = {} as Context;

await handler(event, context);
},
});

app.on("error", (error) => {
logger.error("Proxy error.");
logger.error(error.message);
});

app.on("processing_error", (error) => {
logger.error("Proxy processing error.");
logger.error(error.message);
});

app.on("started", () => {
logger.info("SQS consumer started and listening for events.", {
queueUrl: CONFIG.SQS_QUEUE_URL,
});
});

request.on("end", async () => {
const json = JSON.parse(body);

if (json.format === getJSONFormatHeader({ name: "event_proxy" })) {
await handler(json.event, {} as any);
}
});
};

http
.createServer(async (request, response) => {
await proxyEventToLambdaHandler(request);

response.writeHead(200, { "Content-Type": "application/json" });
response.write("OK");
response.end();
})
.on("error", (error) => {
logger.error("Proxy error.", { error });
})
.on("clientError", (error) => {
logger.error("Proxy client error.", { error });
})
.on("listening", () =>
logger.info(`Proxy is listening on port ${CONFIG.PROXY_PORT}.`)
)
.listen({ port: CONFIG.PROXY_PORT, host: "0.0.0.0" });
app.start();
4 changes: 3 additions & 1 deletion src/emails-sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,16 @@ export const handler = async (event: SQSEvent, context: Context) => {
});

const html = await sender.render({
props: data,
props: { data },
template,
});

await sender.send({
html,
subject: template.Subject,
});

logger.info("Email sent successfully.", { toEmail, event });
} else {
return logger.warn("Received payload with unsupported format.", {
format,
Expand Down
Loading

0 comments on commit 02f0afd

Please sign in to comment.