diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dcabbf4..bb66b03 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,5 +3,5 @@ repos: rev: 19.3b0 hooks: - id: black - language_version: python3.6 + language_version: python3.9 exclude_types: ['markdown', 'ini', 'toml', 'rst'] diff --git a/README.md b/README.md index 8a448bd..4a96a3f 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ We have open-sourced a set of software packages, Runtime Interface Clients (RIC) base images to be Lambda compatible. The Lambda Runtime Interface Client is a lightweight interface that allows your runtime to receive requests from and send requests to the Lambda service. -The Lambda Python Runtime Interface Client is vended through [pip](https://pypi.org/project/awslambdaric). +The Lambda Python Runtime Interface Client is vended through [pip](https://pypi.org/project/awslambdaric). You can include this package in your preferred base image to make that base image Lambda compatible. ## Requirements The Python Runtime Interface Client package currently supports Python versions: - - 3.7.x up to and including 3.12.x + - 3.9.x up to and including 3.13.x ## Usage @@ -103,18 +103,18 @@ def handler(event, context): ### Local Testing -To make it easy to locally test Lambda functions packaged as container images we open-sourced a lightweight web-server, Lambda Runtime Interface Emulator (RIE), which allows your function packaged as a container image to accept HTTP requests. You can install the [AWS Lambda Runtime Interface Emulator](https://github.com/aws/aws-lambda-runtime-interface-emulator) on your local machine to test your function. Then when you run the image function, you set the entrypoint to be the emulator. +To make it easy to locally test Lambda functions packaged as container images we open-sourced a lightweight web-server, Lambda Runtime Interface Emulator (RIE), which allows your function packaged as a container image to accept HTTP requests. You can install the [AWS Lambda Runtime Interface Emulator](https://github.com/aws/aws-lambda-runtime-interface-emulator) on your local machine to test your function. Then when you run the image function, you set the entrypoint to be the emulator. *To install the emulator and test your Lambda function* -1) From your project directory, run the following command to download the RIE from GitHub and install it on your local machine. +1) From your project directory, run the following command to download the RIE from GitHub and install it on your local machine. ```shell script mkdir -p ~/.aws-lambda-rie && \ curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \ chmod +x ~/.aws-lambda-rie/aws-lambda-rie ``` -2) Run your Lambda image function using the docker run command. +2) Run your Lambda image function using the docker run command. ```shell script docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \ @@ -123,9 +123,9 @@ docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \ /usr/local/bin/python -m awslambdaric app.handler ``` -This runs the image as a container and starts up an endpoint locally at `http://localhost:9000/2015-03-31/functions/function/invocations`. +This runs the image as a container and starts up an endpoint locally at `http://localhost:9000/2015-03-31/functions/function/invocations`. -3) Post an event to the following endpoint using a curl command: +3) Post an event to the following endpoint using a curl command: ```shell script curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' @@ -174,4 +174,4 @@ If you discover a potential security issue in this project we ask that you notif ## License -This project is licensed under the Apache-2.0 License. \ No newline at end of file +This project is licensed under the Apache-2.0 License. diff --git a/RELEASE.CHANGELOG.md b/RELEASE.CHANGELOG.md index ec192cd..fc45791 100644 --- a/RELEASE.CHANGELOG.md +++ b/RELEASE.CHANGELOG.md @@ -1,3 +1,24 @@ +### May 26, 2025 +`3.1.1` +- Move unhandled exception warning message to init errors. ([#189](https://github.com/aws/aws-lambda-python-runtime-interface-client/pull/189)) + +### May 21, 2025 +`3.1.0` +- Add support for multi tenancy ([#187](https://github.com/aws/aws-lambda-python-runtime-interface-client/pull/187)) + +### February 27, 2024 +`3.0.2` +- Update `simplejson` to `3.20.1`([#184](https://github.com/aws/aws-lambda-python-runtime-interface-client/pull/184)) + +### January 27, 2024 +`3.0.1` +- Don't enforce text format on uncaught exception warning message ([#182](https://github.com/aws/aws-lambda-python-runtime-interface-client/pull/182)) + +### November 19, 2024 +`3.0.0` +- Drop support for deprecated python versions ([#179](https://github.com/aws/aws-lambda-python-runtime-interface-client/pull/179)) +- Add support for snapstart runtime hooks ([#176](https://github.com/aws/aws-lambda-python-runtime-interface-client/pull/176)) + ### August 23, 2024 `2.2.1`: - Patch libcurl configure.ac to work with later versions of autoconf ([#166](https://github.com/aws/aws-lambda-python-runtime-interface-client/pull/168)) diff --git a/awslambdaric/__init__.py b/awslambdaric/__init__.py index 755de6a..5605903 100644 --- a/awslambdaric/__init__.py +++ b/awslambdaric/__init__.py @@ -2,4 +2,4 @@ Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. """ -__version__ = "2.2.1" +__version__ = "3.1.1" diff --git a/awslambdaric/bootstrap.py b/awslambdaric/bootstrap.py index 0f19f56..cb8d5c3 100644 --- a/awslambdaric/bootstrap.py +++ b/awslambdaric/bootstrap.py @@ -31,6 +31,8 @@ _AWS_LAMBDA_LOG_LEVEL = _get_log_level_from_env_var( os.environ.get("AWS_LAMBDA_LOG_LEVEL") ) +AWS_LAMBDA_INITIALIZATION_TYPE = "AWS_LAMBDA_INITIALIZATION_TYPE" +INIT_TYPE_SNAP_START = "snap-start" def _get_handler(handler): @@ -100,7 +102,6 @@ def replace_line_indentation(line, indent_char, new_indent_char): if _AWS_LAMBDA_LOG_FORMAT == LogFormat.JSON: _ERROR_FRAME_TYPE = _JSON_FRAME_TYPES[logging.ERROR] - _WARNING_FRAME_TYPE = _JSON_FRAME_TYPES[logging.WARNING] def log_error(error_result, log_sink): error_result = { @@ -116,7 +117,6 @@ def log_error(error_result, log_sink): else: _ERROR_FRAME_TYPE = _TEXT_FRAME_TYPES[logging.ERROR] - _WARNING_FRAME_TYPE = _TEXT_FRAME_TYPES[logging.WARNING] def log_error(error_result, log_sink): error_description = "[ERROR]" @@ -158,6 +158,7 @@ def handle_event_request( cognito_identity_json, invoked_function_arn, epoch_deadline_time_in_ms, + tenant_id, log_sink, ): error_result = None @@ -168,6 +169,7 @@ def handle_event_request( epoch_deadline_time_in_ms, invoke_id, invoked_function_arn, + tenant_id, ) event = lambda_runtime_client.marshaller.unmarshal_request( event_body, content_type @@ -199,9 +201,7 @@ def handle_event_request( ) if error_result is not None: - from .lambda_literals import lambda_unhandled_exception_warning_message - log_sink.log(lambda_unhandled_exception_warning_message, _WARNING_FRAME_TYPE) log_error(error_result, log_sink) lambda_runtime_client.post_invocation_error( invoke_id, to_json(error_result), to_json(xray_fault) @@ -229,6 +229,7 @@ def create_lambda_context( epoch_deadline_time_in_ms, invoke_id, invoked_function_arn, + tenant_id, ): client_context = None if client_context_json: @@ -243,6 +244,7 @@ def create_lambda_context( cognito_identity, epoch_deadline_time_in_ms, invoked_function_arn, + tenant_id, ) @@ -286,6 +288,29 @@ def extract_traceback(tb): ] +def on_init_complete(lambda_runtime_client, log_sink): + from . import lambda_runtime_hooks_runner + + try: + lambda_runtime_hooks_runner.run_before_snapshot() + lambda_runtime_client.restore_next() + except: + error_result = build_fault_result(sys.exc_info(), None) + log_error(error_result, log_sink) + lambda_runtime_client.post_init_error( + error_result, FaultException.BEFORE_SNAPSHOT_ERROR + ) + sys.exit(64) + + try: + lambda_runtime_hooks_runner.run_after_restore() + except: + error_result = build_fault_result(sys.exc_info(), None) + log_error(error_result, log_sink) + lambda_runtime_client.report_restore_error(error_result) + sys.exit(65) + + class LambdaLoggerHandler(logging.Handler): def __init__(self, log_sink): logging.Handler.__init__(self) @@ -314,6 +339,7 @@ def emit(self, record): class LambdaLoggerFilter(logging.Filter): def filter(self, record): record.aws_request_id = _GLOBAL_AWS_REQUEST_ID or "" + record.tenant_id = _GLOBAL_TENANT_ID return True @@ -422,6 +448,7 @@ def create_log_sink(): _GLOBAL_AWS_REQUEST_ID = None +_GLOBAL_TENANT_ID = None def _setup_logging(log_format, log_level, log_sink): @@ -454,9 +481,10 @@ def run(app_root, handler, lambda_runtime_api_addr): sys.stdout = Unbuffered(sys.stdout) sys.stderr = Unbuffered(sys.stderr) - use_thread_for_polling_next = ( - os.environ.get("AWS_EXECUTION_ENV") == "AWS_Lambda_python3.12" - ) + use_thread_for_polling_next = os.environ.get("AWS_EXECUTION_ENV") in { + "AWS_Lambda_python3.12", + "AWS_Lambda_python3.13", + } with create_log_sink() as log_sink: lambda_runtime_client = LambdaRuntimeClient( @@ -466,7 +494,7 @@ def run(app_root, handler, lambda_runtime_api_addr): try: _setup_logging(_AWS_LAMBDA_LOG_FORMAT, _AWS_LAMBDA_LOG_LEVEL, log_sink) - global _GLOBAL_AWS_REQUEST_ID + global _GLOBAL_AWS_REQUEST_ID, _GLOBAL_TENANT_ID request_handler = _get_handler(handler) except FaultException as e: @@ -479,15 +507,22 @@ def run(app_root, handler, lambda_runtime_api_addr): error_result = build_fault_result(sys.exc_info(), None) if error_result is not None: + from .lambda_literals import lambda_unhandled_exception_warning_message + + logging.warning(lambda_unhandled_exception_warning_message) log_error(error_result, log_sink) lambda_runtime_client.post_init_error(error_result) sys.exit(1) + if os.environ.get(AWS_LAMBDA_INITIALIZATION_TYPE) == INIT_TYPE_SNAP_START: + on_init_complete(lambda_runtime_client, log_sink) + while True: event_request = lambda_runtime_client.wait_next_invocation() _GLOBAL_AWS_REQUEST_ID = event_request.invoke_id + _GLOBAL_TENANT_ID = event_request.tenant_id update_xray_env_variable(event_request.x_amzn_trace_id) @@ -501,5 +536,6 @@ def run(app_root, handler, lambda_runtime_api_addr): event_request.cognito_identity, event_request.invoked_function_arn, event_request.deadline_time_in_ms, + event_request.tenant_id, log_sink, ) diff --git a/awslambdaric/lambda_context.py b/awslambdaric/lambda_context.py index 1465827..e0a3363 100644 --- a/awslambdaric/lambda_context.py +++ b/awslambdaric/lambda_context.py @@ -16,6 +16,7 @@ def __init__( cognito_identity, epoch_deadline_time_in_ms, invoked_function_arn=None, + tenant_id=None, ): self.aws_request_id = invoke_id self.log_group_name = os.environ.get("AWS_LAMBDA_LOG_GROUP_NAME") @@ -24,6 +25,7 @@ def __init__( self.memory_limit_in_mb = os.environ.get("AWS_LAMBDA_FUNCTION_MEMORY_SIZE") self.function_version = os.environ.get("AWS_LAMBDA_FUNCTION_VERSION") self.invoked_function_arn = invoked_function_arn + self.tenant_id = tenant_id self.client_context = make_obj_from_dict(ClientContext, client_context) if self.client_context is not None: @@ -65,7 +67,8 @@ def __repr__(self): f"function_version={self.function_version}," f"invoked_function_arn={self.invoked_function_arn}," f"client_context={self.client_context}," - f"identity={self.identity}" + f"identity={self.identity}," + f"tenant_id={self.tenant_id}" "])" ) diff --git a/awslambdaric/lambda_runtime_client.py b/awslambdaric/lambda_runtime_client.py index 036d10b..ba4ad92 100644 --- a/awslambdaric/lambda_runtime_client.py +++ b/awslambdaric/lambda_runtime_client.py @@ -62,25 +62,57 @@ def __init__(self, lambda_runtime_address, use_thread_for_polling_next=False): # Not defining symbol as global to avoid relying on TPE being imported unconditionally. self.ThreadPoolExecutor = ThreadPoolExecutor - def post_init_error(self, error_response_data): + def call_rapid( + self, http_method, endpoint, expected_http_code, payload=None, headers=None + ): # These imports are heavy-weight. They implicitly trigger `import ssl, hashlib`. # Importing them lazily to speed up critical path of a common case. - import http import http.client runtime_connection = http.client.HTTPConnection(self.lambda_runtime_address) runtime_connection.connect() - endpoint = "/2018-06-01/runtime/init/error" - headers = {ERROR_TYPE_HEADER: error_response_data["errorType"]} - runtime_connection.request( - "POST", endpoint, to_json(error_response_data), headers=headers - ) + if http_method == "GET": + runtime_connection.request(http_method, endpoint) + else: + runtime_connection.request( + http_method, endpoint, to_json(payload), headers=headers + ) + response = runtime_connection.getresponse() response_body = response.read() - - if response.code != http.HTTPStatus.ACCEPTED: + if response.code != expected_http_code: raise LambdaRuntimeClientError(endpoint, response.code, response_body) + def post_init_error(self, error_response_data, error_type_override=None): + import http + + endpoint = "/2018-06-01/runtime/init/error" + headers = { + ERROR_TYPE_HEADER: ( + error_type_override + if error_type_override + else error_response_data["errorType"] + ) + } + self.call_rapid( + "POST", endpoint, http.HTTPStatus.ACCEPTED, error_response_data, headers + ) + + def restore_next(self): + import http + + endpoint = "/2018-06-01/runtime/restore/next" + self.call_rapid("GET", endpoint, http.HTTPStatus.OK) + + def report_restore_error(self, restore_error_data): + import http + + endpoint = "/2018-06-01/runtime/restore/error" + headers = {ERROR_TYPE_HEADER: FaultException.AFTER_RESTORE_ERROR} + self.call_rapid( + "POST", endpoint, http.HTTPStatus.ACCEPTED, restore_error_data, headers + ) + def wait_next_invocation(self): # Calling runtime_client.next() from a separate thread unblocks the main thread, # which can then process signals. @@ -105,6 +137,7 @@ def wait_next_invocation(self): deadline_time_in_ms=headers.get("Lambda-Runtime-Deadline-Ms"), client_context=headers.get("Lambda-Runtime-Client-Context"), cognito_identity=headers.get("Lambda-Runtime-Cognito-Identity"), + tenant_id=headers.get("Lambda-Runtime-Aws-Tenant-Id"), content_type=headers.get("Content-Type"), event_body=response_body, ) diff --git a/awslambdaric/lambda_runtime_exception.py b/awslambdaric/lambda_runtime_exception.py index e09af70..3ea5b29 100644 --- a/awslambdaric/lambda_runtime_exception.py +++ b/awslambdaric/lambda_runtime_exception.py @@ -11,6 +11,8 @@ class FaultException(Exception): IMPORT_MODULE_ERROR = "Runtime.ImportModuleError" BUILT_IN_MODULE_CONFLICT = "Runtime.BuiltInModuleConflict" MALFORMED_HANDLER_NAME = "Runtime.MalformedHandlerName" + BEFORE_SNAPSHOT_ERROR = "Runtime.BeforeSnapshotError" + AFTER_RESTORE_ERROR = "Runtime.AfterRestoreError" LAMBDA_CONTEXT_UNMARSHAL_ERROR = "Runtime.LambdaContextUnmarshalError" LAMBDA_RUNTIME_CLIENT_ERROR = "Runtime.LambdaRuntimeClientError" diff --git a/awslambdaric/lambda_runtime_hooks_runner.py b/awslambdaric/lambda_runtime_hooks_runner.py new file mode 100644 index 0000000..8aee181 --- /dev/null +++ b/awslambdaric/lambda_runtime_hooks_runner.py @@ -0,0 +1,18 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from snapshot_restore_py import get_before_snapshot, get_after_restore + + +def run_before_snapshot(): + before_snapshot_callables = get_before_snapshot() + while before_snapshot_callables: + # Using pop as before checkpoint callables are executed in the reverse order of their registration + func, args, kwargs = before_snapshot_callables.pop() + func(*args, **kwargs) + + +def run_after_restore(): + after_restore_callables = get_after_restore() + for func, args, kwargs in after_restore_callables: + func(*args, **kwargs) diff --git a/awslambdaric/lambda_runtime_log_utils.py b/awslambdaric/lambda_runtime_log_utils.py index 7ed9940..9ddbcfb 100644 --- a/awslambdaric/lambda_runtime_log_utils.py +++ b/awslambdaric/lambda_runtime_log_utils.py @@ -30,6 +30,7 @@ "processName", "process", "aws_request_id", + "tenant_id", "_frame_type", } @@ -124,6 +125,9 @@ def format(self, record: logging.LogRecord) -> str: "requestId": getattr(record, "aws_request_id", None), "location": self.__format_location(record), } + if hasattr(record, "tenant_id") and record.tenant_id is not None: + result["tenantId"] = record.tenant_id + result.update( (key, value) for key, value in record.__dict__.items() diff --git a/awslambdaric/lambda_runtime_marshaller.py b/awslambdaric/lambda_runtime_marshaller.py index 3b28313..4256066 100644 --- a/awslambdaric/lambda_runtime_marshaller.py +++ b/awslambdaric/lambda_runtime_marshaller.py @@ -15,7 +15,10 @@ # We also set 'ensure_ascii=False' so that the encoded json contains unicode characters instead of unicode escape sequences class Encoder(json.JSONEncoder): def __init__(self): - if os.environ.get("AWS_EXECUTION_ENV") == "AWS_Lambda_python3.12": + if os.environ.get("AWS_EXECUTION_ENV") in { + "AWS_Lambda_python3.12", + "AWS_Lambda_python3.13", + }: super().__init__(use_decimal=False, ensure_ascii=False, allow_nan=True) else: super().__init__(use_decimal=False, allow_nan=True) diff --git a/awslambdaric/runtime_client.cpp b/awslambdaric/runtime_client.cpp index 66252bf..7fb2e95 100644 --- a/awslambdaric/runtime_client.cpp +++ b/awslambdaric/runtime_client.cpp @@ -52,9 +52,10 @@ static PyObject *method_next(PyObject *self) { auto client_context = response.client_context.c_str(); auto content_type = response.content_type.c_str(); auto cognito_id = response.cognito_identity.c_str(); + auto tenant_id = response.tenant_id.c_str(); PyObject *payload_bytes = PyBytes_FromStringAndSize(payload.c_str(), payload.length()); - PyObject *result = Py_BuildValue("(O,{s:s,s:s,s:s,s:l,s:s,s:s,s:s})", + PyObject *result = Py_BuildValue("(O,{s:s,s:s,s:s,s:l,s:s,s:s,s:s,s:s})", payload_bytes, //Py_BuildValue() increments reference counter "Lambda-Runtime-Aws-Request-Id", request_id, "Lambda-Runtime-Trace-Id", NULL_IF_EMPTY(trace_id), @@ -62,7 +63,8 @@ static PyObject *method_next(PyObject *self) { "Lambda-Runtime-Deadline-Ms", deadline, "Lambda-Runtime-Client-Context", NULL_IF_EMPTY(client_context), "Content-Type", NULL_IF_EMPTY(content_type), - "Lambda-Runtime-Cognito-Identity", NULL_IF_EMPTY(cognito_id) + "Lambda-Runtime-Cognito-Identity", NULL_IF_EMPTY(cognito_id), + "Lambda-Runtime-Aws-Tenant-Id", NULL_IF_EMPTY(tenant_id) ); Py_XDECREF(payload_bytes); diff --git a/deps/aws-lambda-cpp-0.2.6.tar.gz b/deps/aws-lambda-cpp-0.2.6.tar.gz index 26fa498..51d7f51 100644 Binary files a/deps/aws-lambda-cpp-0.2.6.tar.gz and b/deps/aws-lambda-cpp-0.2.6.tar.gz differ diff --git a/deps/patches/aws-lambda-cpp-add-tenant-id.patch b/deps/patches/aws-lambda-cpp-add-tenant-id.patch new file mode 100644 index 0000000..a7b7172 --- /dev/null +++ b/deps/patches/aws-lambda-cpp-add-tenant-id.patch @@ -0,0 +1,39 @@ +diff --git a/include/aws/lambda-runtime/runtime.h b/include/aws/lambda-runtime/runtime.h +index 7812ff6..96be869 100644 +--- a/include/aws/lambda-runtime/runtime.h ++++ b/include/aws/lambda-runtime/runtime.h +@@ -61,6 +61,11 @@ struct invocation_request { + */ + std::string content_type; + ++ /** ++ * The Tenant ID of the current invocation. ++ */ ++ std::string tenant_id; ++ + /** + * Function execution deadline counted in milliseconds since the Unix epoch. + */ +diff --git a/src/runtime.cpp b/src/runtime.cpp +index e53b2b8..9763282 100644 +--- a/src/runtime.cpp ++++ b/src/runtime.cpp +@@ -40,6 +40,7 @@ static constexpr auto CLIENT_CONTEXT_HEADER = "lambda-runtime-client-context"; + static constexpr auto COGNITO_IDENTITY_HEADER = "lambda-runtime-cognito-identity"; + static constexpr auto DEADLINE_MS_HEADER = "lambda-runtime-deadline-ms"; + static constexpr auto FUNCTION_ARN_HEADER = "lambda-runtime-invoked-function-arn"; ++static constexpr auto TENANT_ID_HEADER = "lambda-runtime-aws-tenant-id"; + + enum Endpoints { + INIT, +@@ -289,6 +290,10 @@ runtime::next_outcome runtime::get_next() + req.function_arn = resp.get_header(FUNCTION_ARN_HEADER); + } + ++ if (resp.has_header(TENANT_ID_HEADER)) { ++ req.tenant_id = resp.get_header(TENANT_ID_HEADER); ++ } ++ + if (resp.has_header(DEADLINE_MS_HEADER)) { + auto const& deadline_string = resp.get_header(DEADLINE_MS_HEADER); + constexpr int base = 10; diff --git a/requirements/base.txt b/requirements/base.txt index 819c723..4bb251e 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1 +1,2 @@ -simplejson>=3.18.4 +simplejson>=3.20.1 +snapshot-restore-py>=1.0.0 diff --git a/scripts/update_deps.sh b/scripts/update_deps.sh index 0baa3f9..4799a6f 100755 --- a/scripts/update_deps.sh +++ b/scripts/update_deps.sh @@ -30,7 +30,8 @@ wget -c https://github.com/awslabs/aws-lambda-cpp/archive/v$AWS_LAMBDA_CPP_RELEA patch -p1 < ../patches/aws-lambda-cpp-posting-init-errors.patch && \ patch -p1 < ../patches/aws-lambda-cpp-make-the-runtime-client-user-agent-overrideable.patch && \ patch -p1 < ../patches/aws-lambda-cpp-make-lto-optional.patch && \ - patch -p1 < ../patches/aws-lambda-cpp-add-content-type.patch + patch -p1 < ../patches/aws-lambda-cpp-add-content-type.patch && \ + patch -p1 < ../patches/aws-lambda-cpp-add-tenant-id.patch ) ## Pack again and remove the folder diff --git a/setup.py b/setup.py index 2544b21..2bf28ef 100644 --- a/setup.py +++ b/setup.py @@ -84,17 +84,15 @@ def read_requirements(req="base.txt"): "Intended Audience :: Developers", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", ], - python_requires=">=3.6", + python_requires=">=3.9", ext_modules=get_runtime_client_extension(), test_suite="tests", ) diff --git a/tests/integration/codebuild/buildspec.os.alpine.yml b/tests/integration/codebuild/buildspec.os.alpine.yml index f3c53e1..8b290f5 100644 --- a/tests/integration/codebuild/buildspec.os.alpine.yml +++ b/tests/integration/codebuild/buildspec.os.alpine.yml @@ -22,6 +22,7 @@ batch: - "3.10" - "3.11" - "3.12" + - "3.13" phases: pre_build: commands: diff --git a/tests/integration/codebuild/buildspec.os.amazonlinux.2.yml b/tests/integration/codebuild/buildspec.os.amazonlinux.2.yml index f8b7126..05722bb 100644 --- a/tests/integration/codebuild/buildspec.os.amazonlinux.2.yml +++ b/tests/integration/codebuild/buildspec.os.amazonlinux.2.yml @@ -2,7 +2,7 @@ version: 0.2 env: variables: - OS_DISTRIBUTION: amazonlinux + OS_DISTRIBUTION: amazonlinux2 PYTHON_LOCATION: "/usr/local/bin/python3" TEST_NAME: "aws-lambda-python-rtc-amazonlinux-test" batch: @@ -20,7 +20,6 @@ batch: - "3.9" - "3.10" - "3.11" - - "3.12" phases: pre_build: commands: diff --git a/tests/integration/codebuild/buildspec.os.amazonlinux.2023.yml b/tests/integration/codebuild/buildspec.os.amazonlinux.2023.yml new file mode 100644 index 0000000..9d6d20f --- /dev/null +++ b/tests/integration/codebuild/buildspec.os.amazonlinux.2023.yml @@ -0,0 +1,105 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: amazonlinux2023 + PYTHON_LOCATION: "/usr/local/bin/python3" + TEST_NAME: "aws-lambda-python-rtc-amazonlinux-test" +batch: + build-matrix: + static: + ignore-failure: false + env: + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "2023" + RUNTIME_VERSION: + - "3.12" + - "3.13" +phases: + pre_build: + commands: + - export IMAGE_TAG="python-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including the Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - ARCHITECTURE=$(arch) + - > + if [[ "$ARCHITECTURE" == "x86_64" ]]; then + RIE="aws-lambda-rie" + elif [[ "$ARCHITECTURE" == "aarch64" ]]; then + RIE="aws-lambda-rie-arm64" + else + echo "Architecture $ARCHITECTURE is not currently supported." + exit 1 + fi + - tar -xvf tests/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "tests/integration/docker/Dockerfile.echo.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/${RIE} /usr/bin/${RIE}" >> \ + "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" + - > + echo '{"registry-mirrors": ["https://mirror.gcr.io"]}' > /etc/docker/daemon.json + service docker restart + - echo "Building image ${IMAGE_TAG}" + - > + docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.echo.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" \ + --build-arg ARCHITECTURE="${ARCHITECTURE}" \ + --load + build: + commands: + - set -x + - echo "Running Image ${IMAGE_TAG}" + - docker network create "${TEST_NAME}-network" + - > + docker run \ + --detach \ + --name "${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c "/usr/bin/${RIE} ${PYTHON_LOCATION} -m awslambdaric app.handler" + - sleep 2 + - > + docker run \ + --name "${TEST_NAME}-tester" \ + --env "TARGET=${TEST_NAME}-app" \ + --network "${TEST_NAME}-network" \ + --entrypoint="" \ + "${IMAGE_TAG}" \ + sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' + - actual="$(docker logs --tail 1 "${TEST_NAME}-tester" | xargs)" + - expected='success' + - | + echo "Response: ${actual}" + if [[ "$actual" != "$expected" ]]; then + echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" + exit -1 + fi + finally: + - | + echo "---------Container Logs: ${TEST_NAME}-app----------" + echo + docker logs "${TEST_NAME}-app" || true + echo + echo "---------------------------------------------------" + echo "--------Container Logs: ${TEST_NAME}-tester--------" + echo + docker logs "${TEST_NAME}-tester" || true + echo + echo "---------------------------------------------------" + - echo "Cleaning up..." + - docker stop "${TEST_NAME}-app" || true + - docker rm --force "${TEST_NAME}-app" || true + - docker stop "${TEST_NAME}-tester" || true + - docker rm --force "${TEST_NAME}-tester" || true + - docker network rm "${TEST_NAME}-network" || true diff --git a/tests/integration/codebuild/buildspec.os.debian.yml b/tests/integration/codebuild/buildspec.os.debian.yml index 008e6e8..44c061f 100644 --- a/tests/integration/codebuild/buildspec.os.debian.yml +++ b/tests/integration/codebuild/buildspec.os.debian.yml @@ -22,6 +22,7 @@ batch: - "3.10" - "3.11" - "3.12" + - "3.13" phases: pre_build: commands: diff --git a/tests/integration/codebuild/buildspec.os.ubuntu.yml b/tests/integration/codebuild/buildspec.os.ubuntu.yml index ac7c6db..a6e556d 100644 --- a/tests/integration/codebuild/buildspec.os.ubuntu.yml +++ b/tests/integration/codebuild/buildspec.os.ubuntu.yml @@ -22,6 +22,7 @@ batch: - "3.10" - "3.11" - "3.12" + - "3.13" phases: pre_build: commands: diff --git a/tests/integration/docker/Dockerfile.echo.alpine b/tests/integration/docker/Dockerfile.echo.alpine index 7e77e16..f6790fa 100644 --- a/tests/integration/docker/Dockerfile.echo.alpine +++ b/tests/integration/docker/Dockerfile.echo.alpine @@ -31,6 +31,7 @@ RUN mkdir -p ${RIC_BUILD_DIR} # Copy function code and Runtime Interface Client .tgz WORKDIR ${RIC_BUILD_DIR} COPY . . +RUN pip3 install setuptools RUN make init build test && \ mv ./dist/awslambdaric-*.tar.gz ./dist/awslambdaric-test.tar.gz diff --git a/tests/integration/docker/Dockerfile.echo.amazonlinux b/tests/integration/docker/Dockerfile.echo.amazonlinux2 similarity index 94% rename from tests/integration/docker/Dockerfile.echo.amazonlinux rename to tests/integration/docker/Dockerfile.echo.amazonlinux2 index 168c6a2..be05aa1 100644 --- a/tests/integration/docker/Dockerfile.echo.amazonlinux +++ b/tests/integration/docker/Dockerfile.echo.amazonlinux2 @@ -81,10 +81,6 @@ RUN mkdir -p ${RIC_BUILD_DIR} WORKDIR ${RIC_BUILD_DIR} COPY . . -# distutils no longer available in python3.12 and later -# https://docs.python.org/3/whatsnew/3.12.html -# https://peps.python.org/pep-0632/ -RUN if [ $(cut -d '.' -f 2 <<< ${RUNTIME_VERSION}) -ge 12 ]; then pip3 install setuptools; fi RUN make init build test && \ mv ./dist/awslambdaric-*.tar.gz ./dist/awslambdaric-test.tar.gz diff --git a/tests/integration/docker/Dockerfile.echo.amazonlinux2023 b/tests/integration/docker/Dockerfile.echo.amazonlinux2023 new file mode 100644 index 0000000..16bbc79 --- /dev/null +++ b/tests/integration/docker/Dockerfile.echo.amazonlinux2023 @@ -0,0 +1,127 @@ +ARG DISTRO_VERSION +# Stage 1 - bundle base image + runtime interface client +# Grab a fresh copy of the image and install Python +FROM public.ecr.aws/amazonlinux/amazonlinux:${DISTRO_VERSION} AS python-amazonlinux-builder + +ARG RUNTIME_VERSION + +# Install apt dependencies +RUN dnf install -y \ + gcc \ + gcc-c++ \ + tar \ + gzip \ + make \ + autoconf \ + automake \ + freetype-devel \ + yum-utils \ + findutils \ + wget \ + openssl \ + openssl-devel \ + bzip2-devel \ + libffi-devel \ + sqlite-devel + +RUN RUNTIME_LATEST_VERSION=${RUNTIME_VERSION}.$(curl -s https://www.python.org/ftp/python/ | \ + grep -oE "href=\"$(echo ${RUNTIME_VERSION} | sed "s/\\./\\\./g")\.[0-9]+" | \ + cut -d. -f3 | \ + sort -rn | \ + while read -r patch; do \ + $(wget -c https://www.python.org/ftp/python/${RUNTIME_VERSION}.$patch/Python-${RUNTIME_VERSION}.$patch.tgz -O Python-${RUNTIME_VERSION}.$patch.tgz); \ + [ $? -eq 0 ] && echo $patch && break; \ + done) \ + && tar -xzf Python-${RUNTIME_LATEST_VERSION}.tgz \ + && cd Python-${RUNTIME_LATEST_VERSION} \ + && ./configure --prefix=/usr/local --enable-shared \ + && make \ + && make install \ + && ln -s /usr/local/bin/python${RUNTIME_VERSION} /usr/local/bin/python${RUNTIME_LATEST_VERSION} + +# Stage 2 - clean python build dependencies +FROM public.ecr.aws/amazonlinux/amazonlinux:${DISTRO_VERSION} AS python-amazonlinux +RUN dnf install -y \ + libffi-devel + +# Copy the compiled python to /usr/local +COPY --from=python-amazonlinux-builder /usr/local /usr/local +ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + +# Stage 3 - build function and dependencies +FROM python-amazonlinux-builder AS build-image +ARG RUNTIME_VERSION +ARG ARCHITECTURE + +# Install aws-lambda-cpp build dependencies +RUN dnf install -y \ + tar \ + gzip \ + make \ + autoconf \ + automake \ + libtool \ + libcurl-devel \ + gcc-c++ \ + wget \ + sqlite-devel + +# Install a modern CMake +RUN wget --quiet -O cmake-install https://github.com/Kitware/CMake/releases/download/v3.20.0/cmake-3.20.0-linux-${ARCHITECTURE}.sh && \ + sh cmake-install --skip-license --prefix=/usr --exclude-subdirectory; + +ENV PATH=/usr/local/bin:$PATH +ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + + +# Include global args in this stage of the build +ARG RIC_BUILD_DIR="/home/build/" +# Create function directory +RUN mkdir -p ${RIC_BUILD_DIR} +# Copy function code and Runtime Interface Client .tgz +WORKDIR ${RIC_BUILD_DIR} +COPY . . + +# distutils no longer available in python3.12 and later +# https://docs.python.org/3/whatsnew/3.12.html +# https://peps.python.org/pep-0632/ +RUN pip3 install setuptools +RUN make init build + +RUN mv ./dist/awslambdaric-*.tar.gz ./dist/awslambdaric-test.tar.gz +RUN python${RUNTIME_VERSION} -m pip install \ + ./dist/awslambdaric-test.tar.gz \ + --target ${RIC_BUILD_DIR} + +RUN make test + +# Include global args in this stage of the build +ARG FUNCTION_DIR="/home/app/" +# Create function directory +RUN mkdir -p ${FUNCTION_DIR} +# Copy function code +COPY tests/integration/test-handlers/echo/* ${FUNCTION_DIR} +# Copy Runtime Interface Client .tgz +RUN cp ./dist/awslambdaric-test.tar.gz ${FUNCTION_DIR}/awslambdaric-test.tar.gz + +# Install the function's dependencies +WORKDIR ${FUNCTION_DIR} +RUN python${RUNTIME_VERSION} -m pip install \ + awslambdaric-test.tar.gz \ + --target ${FUNCTION_DIR} && \ + rm awslambdaric-test.tar.gz + + +# Stage 4 - final runtime interface client image +# Grab a fresh copy of the Python image +FROM python-amazonlinux +RUN dnf install -y brotli +# Include global arg in this stage of the build +ARG FUNCTION_DIR="/home/app/" +# Set working directory to function root directory +WORKDIR ${FUNCTION_DIR} +# Copy in the built dependencies +COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR} + +ENTRYPOINT [ "/usr/local/bin/python3", "-m", "awslambdaric" ] +CMD [ "app.handler" ] diff --git a/tests/integration/docker/Dockerfile.echo.debian b/tests/integration/docker/Dockerfile.echo.debian index 8ac660b..bf0f4fa 100644 --- a/tests/integration/docker/Dockerfile.echo.debian +++ b/tests/integration/docker/Dockerfile.echo.debian @@ -19,6 +19,7 @@ RUN mkdir -p ${RIC_BUILD_DIR} # Copy function code and Runtime Interface Client .tgz WORKDIR ${RIC_BUILD_DIR} COPY . . +RUN pip3 install setuptools RUN make init build test && \ mv ./dist/awslambdaric-*.tar.gz ./dist/awslambdaric-test.tar.gz diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 7bc2ad2..33afb1c 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -14,15 +14,20 @@ import unittest from io import StringIO from tempfile import NamedTemporaryFile -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, patch, ANY import awslambdaric.bootstrap as bootstrap from awslambdaric.lambda_runtime_exception import FaultException -from awslambdaric.lambda_runtime_log_utils import LogFormat, _get_log_level_from_env_var +from awslambdaric.lambda_runtime_log_utils import ( + LogFormat, + _get_log_level_from_env_var, + JsonFormatter, +) from awslambdaric.lambda_runtime_marshaller import LambdaMarshaller from awslambdaric.lambda_literals import ( lambda_unhandled_exception_warning_message, ) +import snapshot_restore_py class TestUpdateXrayEnv(unittest.TestCase): @@ -60,6 +65,14 @@ def setUp(self): self.event_body = '"event_body"' self.working_directory = os.getcwd() + logging.getLogger().handlers.clear() + + def tearDown(self) -> None: + logging.getLogger().handlers.clear() + logging.getLogger().level = logging.NOTSET + + return super().tearDown() + @staticmethod def dummy_handler(json_input, lambda_context): return {"input": json_input, "aws_request_id": lambda_context.aws_request_id} @@ -75,6 +88,7 @@ def test_handle_event_request_happy_case(self): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) self.lambda_runtime.post_invocation_result.assert_called_once_with( @@ -98,6 +112,7 @@ def test_handle_event_request_invalid_client_context(self): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) args, _ = self.lambda_runtime.post_invocation_error.call_args @@ -139,6 +154,7 @@ def test_handle_event_request_invalid_cognito_idenity(self): "invalid_cognito_identity", "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) args, _ = self.lambda_runtime.post_invocation_error.call_args @@ -181,6 +197,7 @@ def test_handle_event_request_invalid_event_body(self): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) args, _ = self.lambda_runtime.post_invocation_error.call_args @@ -225,6 +242,7 @@ def invalid_json_response(json_input, lambda_context): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) args, _ = self.lambda_runtime.post_invocation_error.call_args @@ -270,6 +288,7 @@ def __init__(self, message): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) args, _ = self.lambda_runtime.post_invocation_error.call_args @@ -322,6 +341,7 @@ def __init__(self, message): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) args, _ = self.lambda_runtime.post_invocation_error.call_args @@ -373,6 +393,7 @@ def unable_to_import_module(json_input, lambda_context): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) args, _ = self.lambda_runtime.post_invocation_error.call_args @@ -412,6 +433,7 @@ def raise_exception_handler(json_input, lambda_context): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) args, _ = self.lambda_runtime.post_invocation_error.call_args @@ -450,6 +472,8 @@ def raise_exception_handler(json_input, lambda_context): ), ) + logging.getLogger().addHandler(logging.StreamHandler(mock_stdout)) + bootstrap.handle_event_request( self.lambda_runtime, raise_exception_handler, @@ -460,14 +484,12 @@ def raise_exception_handler(json_input, lambda_context): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) # NOTE: Indentation characters are NO-BREAK SPACE (U+00A0) not SPACE (U+0020) - error_logs = ( - lambda_unhandled_exception_warning_message - + "[ERROR] FaultExceptionType: Fault exception msg\r" - ) + error_logs = "[ERROR] FaultExceptionType: Fault exception msg\r" error_logs += "Traceback (most recent call last):\r" error_logs += '  File "spam.py", line 3, in \r' error_logs += "    spam.eggs()\r" @@ -486,6 +508,8 @@ def raise_exception_handler(json_input, lambda_context): "FaultExceptionType", "Fault exception msg", None ) + logging.getLogger().addHandler(logging.StreamHandler(mock_stdout)) + bootstrap.handle_event_request( self.lambda_runtime, raise_exception_handler, @@ -496,12 +520,10 @@ def raise_exception_handler(json_input, lambda_context): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) - error_logs = ( - lambda_unhandled_exception_warning_message - + "[ERROR] FaultExceptionType: Fault exception msg\rTraceback (most recent call last):\n" - ) + error_logs = "[ERROR] FaultExceptionType: Fault exception msg\rTraceback (most recent call last):\n" self.assertEqual(mock_stdout.getvalue(), error_logs) @@ -515,6 +537,8 @@ def raise_exception_handler(json_input, lambda_context): except ImportError: raise bootstrap.FaultException("FaultExceptionType", None, None) + logging.getLogger().addHandler(logging.StreamHandler(mock_stdout)) + bootstrap.handle_event_request( self.lambda_runtime, raise_exception_handler, @@ -525,12 +549,10 @@ def raise_exception_handler(json_input, lambda_context): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) - error_logs = ( - lambda_unhandled_exception_warning_message - + "[ERROR] FaultExceptionType\rTraceback (most recent call last):\n" - ) + error_logs = "[ERROR] FaultExceptionType\rTraceback (most recent call last):\n" self.assertEqual(mock_stdout.getvalue(), error_logs) @@ -544,6 +566,8 @@ def raise_exception_handler(json_input, lambda_context): except ImportError: raise bootstrap.FaultException(None, "Fault exception msg", None) + logging.getLogger().addHandler(logging.StreamHandler(mock_stdout)) + bootstrap.handle_event_request( self.lambda_runtime, raise_exception_handler, @@ -554,12 +578,10 @@ def raise_exception_handler(json_input, lambda_context): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) - error_logs = ( - lambda_unhandled_exception_warning_message - + "[ERROR] Fault exception msg\rTraceback (most recent call last):\n" - ) + error_logs = "[ERROR] Fault exception msg\rTraceback (most recent call last):\n" self.assertEqual(mock_stdout.getvalue(), error_logs) @@ -582,6 +604,8 @@ def raise_exception_handler(json_input, lambda_context): ), ) + logging.getLogger().addHandler(logging.StreamHandler(mock_stdout)) + bootstrap.handle_event_request( self.lambda_runtime, raise_exception_handler, @@ -592,9 +616,10 @@ def raise_exception_handler(json_input, lambda_context): {}, "invoked_function_arn", 0, + "tenant_id", bootstrap.StandardLogSink(), ) - error_logs = lambda_unhandled_exception_warning_message + "[ERROR]\r" + error_logs = "[ERROR]\r" error_logs += "Traceback (most recent call last):\r" error_logs += '  File "spam.py", line 3, in \r' error_logs += "    spam.eggs()\r" @@ -603,6 +628,39 @@ def raise_exception_handler(json_input, lambda_context): self.assertEqual(mock_stdout.getvalue(), error_logs) + @patch("sys.stdout", new_callable=StringIO) + def test_handle_event_request_fault_exception_logging_in_json(self, mock_stdout): + def raise_exception_handler(json_input, lambda_context): + try: + import invalid_module # noqa: F401 + except ImportError: + raise bootstrap.FaultException("FaultExceptionType", None, None) + + logging_handler = logging.StreamHandler(mock_stdout) + logging_handler.setFormatter(JsonFormatter()) + logging.getLogger().addHandler(logging_handler) + + bootstrap.handle_event_request( + self.lambda_runtime, + raise_exception_handler, + "invoke_id", + self.event_body, + "application/json", + {}, + {}, + "invoked_function_arn", + 0, + "tenant_id", + bootstrap.StandardLogSink(), + ) + + stdout_value = mock_stdout.getvalue() + + # this line is not in json because of the way the test runtime is bootstrapped + error_logs = "[ERROR] FaultExceptionType\rTraceback (most recent call last):\n" + + self.assertEqual(stdout_value, error_logs) + class TestXrayFault(unittest.TestCase): def test_make_xray(self): @@ -799,6 +857,7 @@ def test_application_json(self): cognito_identity_json=None, invoked_function_arn="invocation-arn", epoch_deadline_time_in_ms=1415836801003, + tenant_id=None, log_sink=bootstrap.StandardLogSink(), ) @@ -818,6 +877,7 @@ def test_binary_request_binary_response(self): cognito_identity_json=None, invoked_function_arn="invocation-arn", epoch_deadline_time_in_ms=1415836801003, + tenant_id=None, log_sink=bootstrap.StandardLogSink(), ) @@ -837,6 +897,7 @@ def test_json_request_binary_response(self): cognito_identity_json=None, invoked_function_arn="invocation-arn", epoch_deadline_time_in_ms=1415836801003, + tenant_id=None, log_sink=bootstrap.StandardLogSink(), ) @@ -855,6 +916,7 @@ def test_binary_with_application_json(self): cognito_identity_json=None, invoked_function_arn="invocation-arn", epoch_deadline_time_in_ms=1415836801003, + tenant_id=None, log_sink=bootstrap.StandardLogSink(), ) @@ -1288,6 +1350,31 @@ def test_json_formatter(self, mock_stderr): ) self.assertEqual(mock_stderr.getvalue(), "") + @patch("awslambdaric.bootstrap._GLOBAL_TENANT_ID", "test-tenant-id") + @patch("sys.stderr", new_callable=StringIO) + def test_json_formatter_with_tenant_id(self, mock_stderr): + logger = logging.getLogger("a.b") + level = logging.INFO + message = "Test json formatting with tenant id" + expected = { + "level": "INFO", + "logger": "a.b", + "message": message, + "requestId": "", + "tenantId": "test-tenant-id", + } + + with patch("sys.stdout", new_callable=StringIO) as mock_stdout: + logger.log(level, message) + + data = json.loads(mock_stdout.getvalue()) + data.pop("timestamp") + self.assertEqual( + data, + expected, + ) + self.assertEqual(mock_stderr.getvalue(), "") + @patch("sys.stdout", new_callable=StringIO) @patch("sys.stderr", new_callable=StringIO) def test_exception(self, mock_stderr, mock_stdout): @@ -1457,5 +1544,53 @@ class TestException(Exception): mock_sys.exit.assert_called_once_with(1) +class TestOnInitComplete(unittest.TestCase): + def tearDown(self): + # We are accessing private filed for cleaning up + snapshot_restore_py._before_snapshot_registry = [] + snapshot_restore_py._after_restore_registry = [] + + # We are using ANY over here as the main thing we want to test is teh errorType propogation and stack trace generation + error_result = { + "errorMessage": "This is a Dummy type error", + "errorType": "TypeError", + "requestId": "", + "stackTrace": ANY, + } + + def raise_type_error(self): + raise TypeError("This is a Dummy type error") + + @patch("awslambdaric.bootstrap.LambdaRuntimeClient") + def test_before_snapshot_exception(self, mock_runtime_client): + snapshot_restore_py.register_before_snapshot(self.raise_type_error) + + with self.assertRaises(SystemExit) as cm: + bootstrap.on_init_complete( + mock_runtime_client, log_sink=bootstrap.StandardLogSink() + ) + + self.assertEqual(cm.exception.code, 64) + mock_runtime_client.post_init_error.assert_called_once_with( + self.error_result, + FaultException.BEFORE_SNAPSHOT_ERROR, + ) + + @patch("awslambdaric.bootstrap.LambdaRuntimeClient") + def test_after_restore_exception(self, mock_runtime_client): + snapshot_restore_py.register_after_restore(self.raise_type_error) + + with self.assertRaises(SystemExit) as cm: + bootstrap.on_init_complete( + mock_runtime_client, log_sink=bootstrap.StandardLogSink() + ) + + self.assertEqual(cm.exception.code, 65) + mock_runtime_client.restore_next.assert_called_once() + mock_runtime_client.report_restore_error.assert_called_once_with( + self.error_result + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_lambda_context.py b/tests/test_lambda_context.py index 34d59da..f7959ab 100644 --- a/tests/test_lambda_context.py +++ b/tests/test_lambda_context.py @@ -37,6 +37,7 @@ def test_init(self): self.assertEqual(context.memory_limit_in_mb, "1234") self.assertEqual(context.function_version, "version1") self.assertEqual(context.invoked_function_arn, "arn:test1") + self.assertEqual(context.tenant_id, None) self.assertEqual(context.identity.cognito_identity_id, None) self.assertEqual(context.identity.cognito_identity_pool_id, None) self.assertEqual(context.client_context.client.installation_id, None) @@ -74,6 +75,21 @@ def test_init_cognito(self): self.assertEqual(context.identity.cognito_identity_id, "id1") self.assertEqual(context.identity.cognito_identity_pool_id, "poolid1") + def test_init_tenant_id(self): + client_context = {} + cognito_identity = {} + tenant_id = "blue" + + context = LambdaContext( + "invoke-id1", + client_context, + cognito_identity, + 1415836801000, + "arn:test", + tenant_id, + ) + self.assertEqual(context.tenant_id, "blue") + def test_init_client_context(self): client_context = { "client": { diff --git a/tests/test_lambda_runtime_client.py b/tests/test_lambda_runtime_client.py index e09130b..fc4af65 100644 --- a/tests/test_lambda_runtime_client.py +++ b/tests/test_lambda_runtime_client.py @@ -26,6 +26,7 @@ def test_constructor(self): deadline_time_in_ms="Lambda-Runtime-Deadline-Ms", client_context="Lambda-Runtime-Client-Context", cognito_identity="Lambda-Runtime-Cognito-Identity", + tenant_id="Lambda-Runtime-Aws-Tenant-Id", content_type="Content-Type", event_body="response_body", ) @@ -37,6 +38,7 @@ def test_constructor(self): deadline_time_in_ms="Lambda-Runtime-Deadline-Ms", client_context="Lambda-Runtime-Client-Context", cognito_identity="Lambda-Runtime-Cognito-Identity", + tenant_id="Lambda-Runtime-Aws-Tenant-Id", content_type="Content-Type", event_body="response_body", ) @@ -48,6 +50,7 @@ def test_constructor(self): deadline_time_in_ms="Lambda-Runtime-Deadline-Ms", client_context="Lambda-Runtime-Client-Context", cognito_identity="Lambda-Runtime-Cognito-Identity", + tenant_id="Lambda-Runtime-Aws-Tenant-Id", content_type="Content-Type", event_body="another_response_body", ) @@ -68,6 +71,7 @@ def test_wait_next_invocation(self, mock_runtime_client): "Lambda-Runtime-Deadline-Ms": 12, "Lambda-Runtime-Client-Context": "client_context", "Lambda-Runtime-Cognito-Identity": "cognito_identity", + "Lambda-Runtime-Aws-Tenant-Id": "tenant_id", "Content-Type": "application/json", } mock_runtime_client.next.return_value = response_body, headears @@ -82,6 +86,7 @@ def test_wait_next_invocation(self, mock_runtime_client): self.assertEqual(event_request.deadline_time_in_ms, 12) self.assertEqual(event_request.client_context, "client_context") self.assertEqual(event_request.cognito_identity, "cognito_identity") + self.assertEqual(event_request.tenant_id, "tenant_id") self.assertEqual(event_request.content_type, "application/json") self.assertEqual(event_request.event_body, response_body) @@ -97,9 +102,77 @@ def test_wait_next_invocation(self, mock_runtime_client): self.assertEqual(event_request.deadline_time_in_ms, 12) self.assertEqual(event_request.client_context, "client_context") self.assertEqual(event_request.cognito_identity, "cognito_identity") + self.assertEqual(event_request.tenant_id, "tenant_id") self.assertEqual(event_request.content_type, "application/json") self.assertEqual(event_request.event_body, response_body) + @patch("awslambdaric.lambda_runtime_client.runtime_client") + def test_wait_next_invocation_without_tenant_id_header(self, mock_runtime_client): + response_body = b"{}" + headers = { + "Lambda-Runtime-Aws-Request-Id": "RID1234", + "Lambda-Runtime-Trace-Id": "TID1234", + "Lambda-Runtime-Invoked-Function-Arn": "FARN1234", + "Lambda-Runtime-Deadline-Ms": 12, + "Lambda-Runtime-Client-Context": "client_context", + "Lambda-Runtime-Cognito-Identity": "cognito_identity", + "Content-Type": "application/json", + } + mock_runtime_client.next.return_value = response_body, headers + runtime_client = LambdaRuntimeClient("localhost:1234") + + event_request = runtime_client.wait_next_invocation() + + self.assertIsNotNone(event_request) + self.assertIsNone(event_request.tenant_id) + self.assertEqual(event_request.event_body, response_body) + + @patch("awslambdaric.lambda_runtime_client.runtime_client") + def test_wait_next_invocation_with_null_tenant_id_header(self, mock_runtime_client): + response_body = b"{}" + headers = { + "Lambda-Runtime-Aws-Request-Id": "RID1234", + "Lambda-Runtime-Trace-Id": "TID1234", + "Lambda-Runtime-Invoked-Function-Arn": "FARN1234", + "Lambda-Runtime-Deadline-Ms": 12, + "Lambda-Runtime-Client-Context": "client_context", + "Lambda-Runtime-Cognito-Identity": "cognito_identity", + "Lambda-Runtime-Aws-Tenant-Id": None, + "Content-Type": "application/json", + } + mock_runtime_client.next.return_value = response_body, headers + runtime_client = LambdaRuntimeClient("localhost:1234") + + event_request = runtime_client.wait_next_invocation() + + self.assertIsNotNone(event_request) + self.assertIsNone(event_request.tenant_id) + self.assertEqual(event_request.event_body, response_body) + + @patch("awslambdaric.lambda_runtime_client.runtime_client") + def test_wait_next_invocation_with_empty_tenant_id_header( + self, mock_runtime_client + ): + response_body = b"{}" + headers = { + "Lambda-Runtime-Aws-Request-Id": "RID1234", + "Lambda-Runtime-Trace-Id": "TID1234", + "Lambda-Runtime-Invoked-Function-Arn": "FARN1234", + "Lambda-Runtime-Deadline-Ms": 12, + "Lambda-Runtime-Client-Context": "client_context", + "Lambda-Runtime-Cognito-Identity": "cognito_identity", + "Lambda-Runtime-Aws-Tenant-Id": "", + "Content-Type": "application/json", + } + mock_runtime_client.next.return_value = response_body, headers + runtime_client = LambdaRuntimeClient("localhost:1234") + + event_request = runtime_client.wait_next_invocation() + + self.assertIsNotNone(event_request) + self.assertEqual(event_request.tenant_id, "") + self.assertEqual(event_request.event_body, response_body) + error_result = { "errorMessage": "Dummy message", "errorType": "Runtime.DummyError", @@ -109,6 +182,21 @@ def test_wait_next_invocation(self, mock_runtime_client): headers = {"Lambda-Runtime-Function-Error-Type": error_result["errorType"]} + restore_error_result = { + "errorMessage": "Dummy Restore error", + "errorType": "Runtime.DummyRestoreError", + "requestId": "", + "stackTrace": [], + } + + restore_error_header = { + "Lambda-Runtime-Function-Error-Type": "Runtime.AfterRestoreError" + } + + before_snapshot_error_header = { + "Lambda-Runtime-Function-Error-Type": "Runtime.BeforeSnapshotError" + } + @patch("http.client.HTTPConnection", autospec=http.client.HTTPConnection) def test_post_init_error(self, MockHTTPConnection): mock_conn = MockHTTPConnection.return_value @@ -225,6 +313,64 @@ def test_post_invocation_error_with_too_large_xray_cause(self, mock_runtime_clie invoke_id, error_data, "" ) + @patch("http.client.HTTPConnection", autospec=http.client.HTTPConnection) + def test_restore_next(self, MockHTTPConnection): + mock_conn = MockHTTPConnection.return_value + mock_response = MagicMock(autospec=http.client.HTTPResponse) + mock_conn.getresponse.return_value = mock_response + mock_response.read.return_value = b"" + mock_response.code = http.HTTPStatus.OK + + runtime_client = LambdaRuntimeClient("localhost:1234") + runtime_client.restore_next() + + MockHTTPConnection.assert_called_with("localhost:1234") + mock_conn.request.assert_called_once_with( + "GET", + "/2018-06-01/runtime/restore/next", + ) + mock_response.read.assert_called_once() + + @patch("http.client.HTTPConnection", autospec=http.client.HTTPConnection) + def test_restore_error(self, MockHTTPConnection): + mock_conn = MockHTTPConnection.return_value + mock_response = MagicMock(autospec=http.client.HTTPResponse) + mock_conn.getresponse.return_value = mock_response + mock_response.read.return_value = b"" + mock_response.code = http.HTTPStatus.ACCEPTED + + runtime_client = LambdaRuntimeClient("localhost:1234") + runtime_client.report_restore_error(self.restore_error_result) + + MockHTTPConnection.assert_called_with("localhost:1234") + mock_conn.request.assert_called_once_with( + "POST", + "/2018-06-01/runtime/restore/error", + to_json(self.restore_error_result), + headers=self.restore_error_header, + ) + mock_response.read.assert_called_once() + + @patch("http.client.HTTPConnection", autospec=http.client.HTTPConnection) + def test_init_before_snapshot_error(self, MockHTTPConnection): + mock_conn = MockHTTPConnection.return_value + mock_response = MagicMock(autospec=http.client.HTTPResponse) + mock_conn.getresponse.return_value = mock_response + mock_response.read.return_value = b"" + mock_response.code = http.HTTPStatus.ACCEPTED + + runtime_client = LambdaRuntimeClient("localhost:1234") + runtime_client.post_init_error(self.error_result, "Runtime.BeforeSnapshotError") + + MockHTTPConnection.assert_called_with("localhost:1234") + mock_conn.request.assert_called_once_with( + "POST", + "/2018-06-01/runtime/init/error", + to_json(self.error_result), + headers=self.before_snapshot_error_header, + ) + mock_response.read.assert_called_once() + def test_connection_refused(self): with self.assertRaises(ConnectionRefusedError): runtime_client = LambdaRuntimeClient("127.0.0.1:1") diff --git a/tests/test_lambda_runtime_marshaller.py b/tests/test_lambda_runtime_marshaller.py index 7cd73b4..843bcee 100644 --- a/tests/test_lambda_runtime_marshaller.py +++ b/tests/test_lambda_runtime_marshaller.py @@ -11,13 +11,17 @@ class TestLambdaRuntimeMarshaller(unittest.TestCase): execution_envs = ( + "AWS_Lambda_python3.13", "AWS_Lambda_python3.12", "AWS_Lambda_python3.11", "AWS_Lambda_python3.10", "AWS_Lambda_python3.9", ) - envs_lambda_marshaller_ensure_ascii_false = {"AWS_Lambda_python3.12"} + envs_lambda_marshaller_ensure_ascii_false = { + "AWS_Lambda_python3.12", + "AWS_Lambda_python3.13", + } execution_envs_lambda_marshaller_ensure_ascii_true = tuple( set(execution_envs).difference(envs_lambda_marshaller_ensure_ascii_false) diff --git a/tests/test_runtime_hooks.py b/tests/test_runtime_hooks.py new file mode 100644 index 0000000..e73204f --- /dev/null +++ b/tests/test_runtime_hooks.py @@ -0,0 +1,65 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import unittest +from unittest.mock import patch, call +from awslambdaric import lambda_runtime_hooks_runner +import snapshot_restore_py + + +def fun_test1(): + print("In function ONE") + + +def fun_test2(): + print("In function TWO") + + +def fun_with_args_kwargs(x, y, **kwargs): + print("Here are the args:", x, y) + print("Here are the keyword args:", kwargs) + + +class TestRuntimeHooks(unittest.TestCase): + def tearDown(self): + # We are accessing private filed for cleaning up + snapshot_restore_py._before_snapshot_registry = [] + snapshot_restore_py._after_restore_registry = [] + + @patch("builtins.print") + def test_before_snapshot_execution_order(self, mock_print): + snapshot_restore_py.register_before_snapshot( + fun_with_args_kwargs, 5, 7, arg1="Lambda", arg2="SnapStart" + ) + snapshot_restore_py.register_before_snapshot(fun_test2) + snapshot_restore_py.register_before_snapshot(fun_test1) + + lambda_runtime_hooks_runner.run_before_snapshot() + + calls = [] + calls.append(call("In function ONE")) + calls.append(call("In function TWO")) + calls.append(call("Here are the args:", 5, 7)) + calls.append( + call("Here are the keyword args:", {"arg1": "Lambda", "arg2": "SnapStart"}) + ) + self.assertEqual(calls, mock_print.mock_calls) + + @patch("builtins.print") + def test_after_restore_execution_order(self, mock_print): + snapshot_restore_py.register_after_restore( + fun_with_args_kwargs, 11, 13, arg1="Lambda", arg2="SnapStart" + ) + snapshot_restore_py.register_after_restore(fun_test2) + snapshot_restore_py.register_after_restore(fun_test1) + + lambda_runtime_hooks_runner.run_after_restore() + + calls = [] + calls.append(call("Here are the args:", 11, 13)) + calls.append( + call("Here are the keyword args:", {"arg1": "Lambda", "arg2": "SnapStart"}) + ) + calls.append(call("In function TWO")) + calls.append(call("In function ONE")) + self.assertEqual(calls, mock_print.mock_calls)