From 9161cb64a0a13b54a981b2b846a4d073db8c30a2 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Wed, 31 Jul 2024 16:14:03 +0300 Subject: [PATCH] feat(new): Added AWS Lambda module (#655) As part of the effort described, detailed and presented on https://github.com/testcontainers/testcontainers-python/pull/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: - https://github.com/testcontainers/testcontainers-python/pull/585 - https://github.com/testcontainers/testcontainers-python/pull/595 - https://github.com/testcontainers/testcontainers-python/pull/612 Expended from issue https://github.com/testcontainers/testcontainers-python/issues/83 --- modules/aws/README.rst | 22 ++++++++ modules/aws/testcontainers/aws/__init__.py | 1 + modules/aws/testcontainers/aws/aws_lambda.py | 53 ++++++++++++++++++ modules/aws/tests/lambda_sample/Dockerfile | 10 ++++ .../tests/lambda_sample/lambda_function.py | 5 ++ modules/aws/tests/test_aws.py | 56 +++++++++++++++++++ poetry.lock | 3 +- pyproject.toml | 2 + 8 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 modules/aws/README.rst create mode 100644 modules/aws/testcontainers/aws/__init__.py create mode 100644 modules/aws/testcontainers/aws/aws_lambda.py create mode 100644 modules/aws/tests/lambda_sample/Dockerfile create mode 100644 modules/aws/tests/lambda_sample/lambda_function.py create mode 100644 modules/aws/tests/test_aws.py 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"]