diff --git a/modules/aws/README.rst b/modules/aws/README.rst new file mode 100644 index 00000000..a44dc856 --- /dev/null +++ b/modules/aws/README.rst @@ -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. diff --git a/modules/aws/testcontainers/aws/__init__.py b/modules/aws/testcontainers/aws/__init__.py new file mode 100644 index 00000000..f16705c8 --- /dev/null +++ b/modules/aws/testcontainers/aws/__init__.py @@ -0,0 +1 @@ +from .aws_lambda import AWSLambdaContainer # noqa: F401 diff --git a/modules/aws/testcontainers/aws/aws_lambda.py b/modules/aws/testcontainers/aws/aws_lambda.py new file mode 100644 index 00000000..30a1f0af --- /dev/null +++ b/modules/aws/testcontainers/aws/aws_lambda.py @@ -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) diff --git a/modules/aws/tests/lambda_sample/Dockerfile b/modules/aws/tests/lambda_sample/Dockerfile new file mode 100644 index 00000000..5d071c80 --- /dev/null +++ b/modules/aws/tests/lambda_sample/Dockerfile @@ -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" ] diff --git a/modules/aws/tests/lambda_sample/lambda_function.py b/modules/aws/tests/lambda_sample/lambda_function.py new file mode 100644 index 00000000..b253ed17 --- /dev/null +++ b/modules/aws/tests/lambda_sample/lambda_function.py @@ -0,0 +1,5 @@ +import sys + + +def handler(event, context): + return "Hello from AWS Lambda using Python" + sys.version + "!" diff --git a/modules/aws/tests/test_aws.py b/modules/aws/tests/test_aws.py new file mode 100644 index 00000000..873b8735 --- /dev/null +++ b/modules/aws/tests/test_aws.py @@ -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 diff --git a/poetry.lock b/poetry.lock index 56f42ea3..a5d95631 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4558,6 +4558,7 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [extras] arangodb = ["python-arango"] +aws = ["boto3", "httpx"] azurite = ["azure-storage-blob"] cassandra = [] chroma = ["chromadb-client"] @@ -4602,4 +4603,4 @@ weaviate = ["weaviate-client"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "ef48ca48ddc2bc6ac68487e1674d1e6973c3a14b2b5c41235262af20695fe432" +content-hash = "00155615fffa7f316221c1fafb895105911a3cce003b57713d9b76b7fd3e3214" diff --git a/pyproject.toml b/pyproject.toml index 76ca27ec..35edc0d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" }, @@ -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"]