Skip to content

Commit

Permalink
feat(new): Added AWS Lambda module (#655)
Browse files Browse the repository at this point in the history
As part of the effort described, detailed and presented on
#559
This is the 4th (and final in this track) PR that should provide support
for AWS Lambda containers.

This module will add the ability to test and run Amazon Lambdas (using
the built-in runtime interface emulator)
For example:

```python
from testcontainers.aws import AWSLambdaContainer
from testcontainers.core.waiting_utils import wait_for_logs
from testcontainers.core.image import DockerImage

with DockerImage(path="./modules/aws/tests/lambda_sample", tag="test-lambda:latest") as image:
    with AWSLambdaContainer(image=image, port=8080) as func:
        response = func.send_request(data={'payload': 'some data'})
        assert response.status_code == 200
        assert "Hello from AWS Lambda using Python" in response.json()
        delay = wait_for_logs(func, "START RequestId:")
```

This can (and probably will) be used with the provided
[LocalStackContainer](https://testcontainers-python.readthedocs.io/en/latest/modules/localstack/README.html)
to help simulate more advance AWS cases.

---

Based on the work done on:
- #585 
- #595 
- #612

Expended from issue
#83
  • Loading branch information
Tranquility2 authored Jul 31, 2024
1 parent 068c431 commit 9161cb6
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 1 deletion.
22 changes: 22 additions & 0 deletions modules/aws/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
:code:`testcontainers-aws` is a set of AWS containers modules that can be used to create AWS containers.

.. autoclass:: testcontainers.aws.AWSLambdaContainer
.. title:: testcontainers.aws.AWSLambdaContainer

The following environment variables are used by the AWS Lambda container:

+-------------------------------+--------------------------+------------------------------+
| Env Variable | Default | Notes |
+===============================+==========================+==============================+
| ``AWS_DEFAULT_REGION`` | ``us-west-1`` | Fetched from os environment |
+-------------------------------+--------------------------+------------------------------+
| ``AWS_ACCESS_KEY_ID`` | ``testcontainers-aws`` | Fetched from os environment |
+-------------------------------+--------------------------+------------------------------+
| ``AWS_SECRET_ACCESS_KEY`` | ``testcontainers-aws`` | Fetched from os environment |
+-------------------------------+--------------------------+------------------------------+

Each one of the environment variables is expected to be set in the host machine where the test is running.

Make sure you are using an image based on :code:`public.ecr.aws/lambda/python`

Please checkout https://docs.aws.amazon.com/lambda/latest/dg/python-image.html for more information on how to run AWS Lambda functions locally.
1 change: 1 addition & 0 deletions modules/aws/testcontainers/aws/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .aws_lambda import AWSLambdaContainer # noqa: F401
53 changes: 53 additions & 0 deletions modules/aws/testcontainers/aws/aws_lambda.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import os
from typing import Union

import httpx

from testcontainers.core.image import DockerImage
from testcontainers.generic.server import ServerContainer

RIE_PATH = "/2015-03-31/functions/function/invocations"
# AWS OS-only base images contain an Amazon Linux distribution and the runtime interface emulator (RIE) for Lambda.


class AWSLambdaContainer(ServerContainer):
"""
AWS Lambda container that is based on a custom image.
Example:
.. doctest::
>>> from testcontainers.aws import AWSLambdaContainer
>>> from testcontainers.core.waiting_utils import wait_for_logs
>>> from testcontainers.core.image import DockerImage
>>> with DockerImage(path="./modules/aws/tests/lambda_sample", tag="test-lambda:latest") as image:
... with AWSLambdaContainer(image=image, port=8080) as func:
... response = func.send_request(data={'payload': 'some data'})
... assert response.status_code == 200
... assert "Hello from AWS Lambda using Python" in response.json()
... delay = wait_for_logs(func, "START RequestId:")
:param image: Docker image to be used for the container.
:param port: Port to be exposed on the container (default: 8080).
"""

def __init__(self, image: Union[str, DockerImage], port: int = 8080) -> None:
super().__init__(port, str(image))
self.with_env("AWS_DEFAULT_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-west-1"))
self.with_env("AWS_ACCESS_KEY_ID", os.environ.get("AWS_ACCESS_KEY_ID", "testcontainers-aws"))
self.with_env("AWS_SECRET_ACCESS_KEY", os.environ.get("AWS_SECRET_ACCESS_KEY", "testcontainers-aws"))

def get_api_url(self) -> str:
return self._create_connection_url() + RIE_PATH

def send_request(self, data: dict) -> httpx.Response:
"""
Send a request to the AWS Lambda function.
:param data: Data to be sent to the AWS Lambda function.
:return: Response from the AWS Lambda function.
"""
client = self.get_client()
return client.post(self.get_api_url(), json=data)
10 changes: 10 additions & 0 deletions modules/aws/tests/lambda_sample/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
FROM public.ecr.aws/lambda/python:3.9

RUN pip install boto3

COPY lambda_function.py ${LAMBDA_TASK_ROOT}

EXPOSE 8080

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "lambda_function.handler" ]
5 changes: 5 additions & 0 deletions modules/aws/tests/lambda_sample/lambda_function.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import sys


def handler(event, context):
return "Hello from AWS Lambda using Python" + sys.version + "!"
56 changes: 56 additions & 0 deletions modules/aws/tests/test_aws.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import re
import os

import pytest
from unittest.mock import patch

from testcontainers.core.image import DockerImage
from testcontainers.aws import AWSLambdaContainer
from testcontainers.aws.aws_lambda import RIE_PATH

DOCKER_FILE_PATH = "./modules/aws/tests/lambda_sample"
IMAGE_TAG = "lambda:test"


def test_aws_lambda_container():
with DockerImage(path=DOCKER_FILE_PATH, tag="test-lambda:latest") as image:
with AWSLambdaContainer(image=image, port=8080) as func:
assert func.get_container_host_ip() == "localhost"
assert func.internal_port == 8080
assert func.env["AWS_DEFAULT_REGION"] == "us-west-1"
assert func.env["AWS_ACCESS_KEY_ID"] == "testcontainers-aws"
assert func.env["AWS_SECRET_ACCESS_KEY"] == "testcontainers-aws"
assert re.match(rf"http://localhost:\d+{RIE_PATH}", func.get_api_url())
response = func.send_request(data={"payload": "test"})
assert response.status_code == 200
assert "Hello from AWS Lambda using Python" in response.json()
for log_str in ["START RequestId", "END RequestId", "REPORT RequestId"]:
assert log_str in func.get_stdout()


def test_aws_lambda_container_external_env_vars():
vars = {
"AWS_DEFAULT_REGION": "region",
"AWS_ACCESS_KEY_ID": "id",
"AWS_SECRET_ACCESS_KEY": "key",
}
with patch.dict(os.environ, vars):
with DockerImage(path=DOCKER_FILE_PATH, tag="test-lambda-env-vars:latest") as image:
with AWSLambdaContainer(image=image, port=8080) as func:
assert func.env["AWS_DEFAULT_REGION"] == "region"
assert func.env["AWS_ACCESS_KEY_ID"] == "id"
assert func.env["AWS_SECRET_ACCESS_KEY"] == "key"


def test_aws_lambda_container_no_port():
with DockerImage(path=DOCKER_FILE_PATH, tag="test-lambda-no-port:latest") as image:
with AWSLambdaContainer(image=image) as func:
response = func.send_request(data={"payload": "test"})
assert response.status_code == 200


def test_aws_lambda_container_no_path():
with pytest.raises(TypeError):
with DockerImage(path=DOCKER_FILE_PATH, tag="test-lambda-no-path:latest") as image:
with AWSLambdaContainer() as func: # noqa: F841
pass
3 changes: 2 additions & 1 deletion poetry.lock

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

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ classifiers = [
packages = [
{ include = "testcontainers", from = "core" },
{ include = "testcontainers", from = "modules/arangodb" },
{ include = "testcontainers", from = "modules/aws"},
{ include = "testcontainers", from = "modules/azurite" },
{ include = "testcontainers", from = "modules/cassandra" },
{ include = "testcontainers", from = "modules/chroma" },
Expand Down Expand Up @@ -116,6 +117,7 @@ trino = { version = "*", optional = true }

[tool.poetry.extras]
arangodb = ["python-arango"]
aws = ["boto3", "httpx"]
azurite = ["azure-storage-blob"]
cassandra = []
clickhouse = ["clickhouse-driver"]
Expand Down

0 comments on commit 9161cb6

Please sign in to comment.