diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03e29508fe..d9af2f5e4a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -210,6 +210,7 @@ jobs: with: # These are the versions of Python that correspond to the supported Lambda runtimes python-version: | + 3.11 3.10 3.9 3.8 diff --git a/requirements/base.txt b/requirements/base.txt index 57a67e078b..8bbc0b0f2b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -15,7 +15,7 @@ docker~=6.1.0 dateparser~=1.1 requests~=2.31.0 serverlessrepo==0.1.10 -aws_lambda_builders==1.34.0 +aws_lambda_builders==1.35.0 tomlkit==0.11.8 watchdog==2.1.2 rich~=13.4.2 diff --git a/requirements/pyinstaller-build.txt b/requirements/pyinstaller-build.txt index 400eaac4e8..3d1a3e564b 100644 --- a/requirements/pyinstaller-build.txt +++ b/requirements/pyinstaller-build.txt @@ -1,3 +1,3 @@ # Executable binary builder requirements setuptools==67.7.2 -pyinstaller==5.10.1 +pyinstaller==5.13.0 diff --git a/requirements/reproducible-linux.txt b/requirements/reproducible-linux.txt index 8e993c68de..fa6e599a8b 100644 --- a/requirements/reproducible-linux.txt +++ b/requirements/reproducible-linux.txt @@ -15,9 +15,9 @@ attrs==23.1.0 \ # jschema-to-python # jsonschema # sarif-om -aws-lambda-builders==1.34.0 \ - --hash=sha256:0790f7e9b7ee7286b96fbcf49450c5be0341bb7cb852ca7d74beae190139eb48 \ - --hash=sha256:20456a942a417407b42ecf8ab7fce6a47306fd063051e7cb09d02d1be24d5cf3 +aws-lambda-builders==1.35.0 \ + --hash=sha256:419d766e60ac2a7303a23889b354d108a4244ce8d467dcf9dc71a62461895780 \ + --hash=sha256:cf462961ec8c9d493f82b955d6b76630dc0ed356b166caddd5436719ce599586 # via aws-sam-cli (setup.py) aws-sam-translator==1.71.0 \ --hash=sha256:17fb87c8137d8d49e7a978396b2b3b279211819dee44618415aab1e99c2cb659 \ @@ -302,9 +302,9 @@ junit-xml==1.9 \ --hash=sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f \ --hash=sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732 # via cfn-lint -markdown-it-py==2.2.0 \ - --hash=sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30 \ - --hash=sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1 +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb # via rich markupsafe==2.1.3 \ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ diff --git a/requirements/reproducible-mac.txt b/requirements/reproducible-mac.txt index c7ba6d447e..48560f78cb 100644 --- a/requirements/reproducible-mac.txt +++ b/requirements/reproducible-mac.txt @@ -15,9 +15,9 @@ attrs==23.1.0 \ # jschema-to-python # jsonschema # sarif-om -aws-lambda-builders==1.34.0 \ - --hash=sha256:0790f7e9b7ee7286b96fbcf49450c5be0341bb7cb852ca7d74beae190139eb48 \ - --hash=sha256:20456a942a417407b42ecf8ab7fce6a47306fd063051e7cb09d02d1be24d5cf3 +aws-lambda-builders==1.35.0 \ + --hash=sha256:419d766e60ac2a7303a23889b354d108a4244ce8d467dcf9dc71a62461895780 \ + --hash=sha256:cf462961ec8c9d493f82b955d6b76630dc0ed356b166caddd5436719ce599586 # via aws-sam-cli (setup.py) aws-sam-translator==1.71.0 \ --hash=sha256:17fb87c8137d8d49e7a978396b2b3b279211819dee44618415aab1e99c2cb659 \ @@ -328,9 +328,9 @@ junit-xml==1.9 \ --hash=sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f \ --hash=sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732 # via cfn-lint -markdown-it-py==2.2.0 \ - --hash=sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30 \ - --hash=sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1 +markdown-it-py==3.0.0 \ + --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ + --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb # via rich markupsafe==2.1.3 \ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ diff --git a/samcli/__init__.py b/samcli/__init__.py index 15e5e50faa..4e33785960 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = "1.91.0" +__version__ = "1.92.0" diff --git a/samcli/commands/build/command.py b/samcli/commands/build/command.py index 86327d411d..80463d9d6b 100644 --- a/samcli/commands/build/command.py +++ b/samcli/commands/build/command.py @@ -51,7 +51,7 @@ \b Supported Runtimes ------------------ - 1. Python 3.7, 3.8, 3.9, 3.10 using PIP\n + 1. Python 3.7, 3.8, 3.9, 3.10, 3.11 using PIP\n 2. Nodejs 18.x, 16.x, 14.x, 12.x using NPM\n 3. Ruby 2.7, 3.2 using Bundler\n 4. Java 8, Java 11, Java 17 using Gradle and Maven\n diff --git a/samcli/lib/build/workflow_config.py b/samcli/lib/build/workflow_config.py index b9ac751aed..713c57a751 100644 --- a/samcli/lib/build/workflow_config.py +++ b/samcli/lib/build/workflow_config.py @@ -89,6 +89,7 @@ def get_layer_subfolder(build_workflow: str) -> str: "python3.8": "python", "python3.9": "python", "python3.10": "python", + "python3.11": "python", "nodejs4.3": "nodejs", "nodejs6.10": "nodejs", "nodejs8.10": "nodejs", @@ -155,6 +156,7 @@ def get_workflow_config( "python3.8": BasicWorkflowSelector(PYTHON_PIP_CONFIG), "python3.9": BasicWorkflowSelector(PYTHON_PIP_CONFIG), "python3.10": BasicWorkflowSelector(PYTHON_PIP_CONFIG), + "python3.11": BasicWorkflowSelector(PYTHON_PIP_CONFIG), "nodejs12.x": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "nodejs14.x": BasicWorkflowSelector(NODEJS_NPM_CONFIG), "nodejs16.x": BasicWorkflowSelector(NODEJS_NPM_CONFIG), diff --git a/samcli/lib/providers/cfn_api_provider.py b/samcli/lib/providers/cfn_api_provider.py index 93e2915276..ca2531362a 100644 --- a/samcli/lib/providers/cfn_api_provider.py +++ b/samcli/lib/providers/cfn_api_provider.py @@ -522,7 +522,7 @@ def _extract_cfn_gateway_v2_stage( api_resource_type = resources.get(api_id, {}).get("Type") if api_resource_type != AWS_APIGATEWAY_V2_API: raise InvalidSamTemplateException( - "The AWS::ApiGatewayV2::Stag must have a valid ApiId that points to Api resource {}".format(api_id) + "The AWS::ApiGatewayV2::Stage must have a valid ApiId that points to Api resource {}".format(api_id) ) collector.stage_name = stage_name diff --git a/samcli/lib/remote_invoke/lambda_invoke_executors.py b/samcli/lib/remote_invoke/lambda_invoke_executors.py index 911bdb0a96..23b8bc6020 100644 --- a/samcli/lib/remote_invoke/lambda_invoke_executors.py +++ b/samcli/lib/remote_invoke/lambda_invoke_executors.py @@ -4,6 +4,7 @@ import base64 import json import logging +import typing from abc import ABC, abstractmethod from json import JSONDecodeError from typing import cast @@ -11,7 +12,9 @@ from botocore.eventstream import EventStream from botocore.exceptions import ClientError, ParamValidationError from botocore.response import StreamingBody -from mypy_boto3_lambda.client import LambdaClient + +if typing.TYPE_CHECKING: # pragma: no cover + from mypy_boto3_lambda.client import LambdaClient from samcli.lib.remote_invoke.exceptions import ( ErrorBotoApiCallException, @@ -47,11 +50,13 @@ class AbstractLambdaInvokeExecutor(BotoActionExecutor, ABC): For Payload parameter, if a file location provided, the file handle will be passed as Payload object """ - _lambda_client: LambdaClient + _lambda_client: "LambdaClient" _function_name: str _remote_output_format: RemoteInvokeOutputFormat - def __init__(self, lambda_client: LambdaClient, function_name: str, remote_output_format: RemoteInvokeOutputFormat): + def __init__( + self, lambda_client: "LambdaClient", function_name: str, remote_output_format: RemoteInvokeOutputFormat + ): self._lambda_client = lambda_client self._function_name = function_name self._remote_output_format = remote_output_format @@ -159,16 +164,16 @@ class DefaultConvertToJSON(RemoteInvokeRequestResponseMapper[RemoteInvokeExecuti def map(self, test_input: RemoteInvokeExecutionInfo) -> RemoteInvokeExecutionInfo: if not test_input.is_file_provided(): if not test_input.payload: - LOG.debug("Input event not found, invoking Lambda Function with an empty event") + LOG.debug("Input event not found, invoking resource with an empty event") test_input.payload = "{}" - LOG.debug("Mapping input Payload to JSON string object") + LOG.debug("Mapping input event to JSON string object") try: _ = json.loads(cast(str, test_input.payload)) except JSONDecodeError: json_value = json.dumps(test_input.payload) LOG.info( "Auto converting value '%s' into JSON '%s'. " - "If you don't want auto-conversion, please provide a JSON string as payload", + "If you don't want auto-conversion, please provide a JSON string as event", test_input.payload, json_value, ) @@ -219,7 +224,7 @@ def map(self, remote_invoke_input: RemoteInvokeResponse) -> RemoteInvokeResponse return remote_invoke_input -def _is_function_invoke_mode_response_stream(lambda_client: LambdaClient, function_name: str): +def _is_function_invoke_mode_response_stream(lambda_client: "LambdaClient", function_name: str): """ Returns True if given function has RESPONSE_STREAM as InvokeMode, False otherwise """ diff --git a/samcli/lib/remote_invoke/stepfunctions_invoke_executors.py b/samcli/lib/remote_invoke/stepfunctions_invoke_executors.py index df8c9b2b4e..47e87395e9 100644 --- a/samcli/lib/remote_invoke/stepfunctions_invoke_executors.py +++ b/samcli/lib/remote_invoke/stepfunctions_invoke_executors.py @@ -3,11 +3,14 @@ """ import logging import time +import typing from datetime import datetime from typing import cast from botocore.exceptions import ClientError, ParamValidationError -from mypy_boto3_stepfunctions import SFNClient + +if typing.TYPE_CHECKING: # pragma: no cover + from mypy_boto3_stepfunctions import SFNClient from samcli.lib.remote_invoke.exceptions import ( ErrorBotoApiCallException, @@ -38,13 +41,13 @@ class StepFunctionsStartExecutionExecutor(BotoActionExecutor): execution details. """ - _stepfunctions_client: SFNClient + _stepfunctions_client: "SFNClient" _state_machine_arn: str _remote_output_format: RemoteInvokeOutputFormat request_parameters: dict def __init__( - self, stepfunctions_client: SFNClient, physical_id: str, remote_output_format: RemoteInvokeOutputFormat + self, stepfunctions_client: "SFNClient", physical_id: str, remote_output_format: RemoteInvokeOutputFormat ): self._stepfunctions_client = stepfunctions_client self._remote_output_format = remote_output_format diff --git a/samcli/lib/utils/architecture.py b/samcli/lib/utils/architecture.py index a8025d8b3f..0121d5d8f0 100644 --- a/samcli/lib/utils/architecture.py +++ b/samcli/lib/utils/architecture.py @@ -22,6 +22,7 @@ "python3.8": [ARM64, X86_64], "python3.9": [ARM64, X86_64], "python3.10": [ARM64, X86_64], + "python3.11": [ARM64, X86_64], "ruby2.7": [ARM64, X86_64], "ruby3.2": [ARM64, X86_64], "java8": [X86_64], diff --git a/samcli/lib/utils/preview_runtimes.py b/samcli/lib/utils/preview_runtimes.py index c17ae95cf8..cffd6b2338 100644 --- a/samcli/lib/utils/preview_runtimes.py +++ b/samcli/lib/utils/preview_runtimes.py @@ -4,4 +4,4 @@ """ from typing import Set -PREVIEW_RUNTIMES: Set[str] = set() +PREVIEW_RUNTIMES: Set[str] = {"python3.11"} diff --git a/samcli/local/common/runtime_template.py b/samcli/local/common/runtime_template.py index 697631adea..707ea0f3f3 100644 --- a/samcli/local/common/runtime_template.py +++ b/samcli/local/common/runtime_template.py @@ -16,7 +16,7 @@ RUNTIME_DEP_TEMPLATE_MAPPING = { "python": [ { - "runtimes": ["python3.10", "python3.9", "python3.8", "python3.7"], + "runtimes": ["python3.11", "python3.10", "python3.9", "python3.8", "python3.7"], "dependency_manager": "pip", "init_location": os.path.join(_templates, "cookiecutter-aws-sam-hello-python"), "build": True, @@ -113,6 +113,7 @@ def get_local_lambda_images_location(mapping, runtime): "provided.al2", "provided", # python runtimes in descending order + "python3.11", "python3.10", "python3.9", "python3.8", @@ -135,6 +136,7 @@ def get_local_lambda_images_location(mapping, runtime): "nodejs16.x": "amazon/nodejs16.x-base", "nodejs14.x": "amazon/nodejs14.x-base", "nodejs12.x": "amazon/nodejs12.x-base", + "python3.11": "amazon/python3.11-base", "python3.10": "amazon/python3.10-base", "python3.9": "amazon/python3.9-base", "python3.8": "amazon/python3.8-base", @@ -156,6 +158,7 @@ def get_local_lambda_images_location(mapping, runtime): "python3.8": "Python36", "python3.9": "Python36", "python3.10": "Python36", + "python3.11": "Python36", "dotnet6": "dotnet6", "go1.x": "Go1", } diff --git a/samcli/local/docker/lambda_debug_settings.py b/samcli/local/docker/lambda_debug_settings.py index 8bffc6a3e2..a5e378dea1 100644 --- a/samcli/local/docker/lambda_debug_settings.py +++ b/samcli/local/docker/lambda_debug_settings.py @@ -179,6 +179,10 @@ def get_debug_settings(debug_port, debug_args_list, _container_env_vars, runtime entry + ["/var/lang/bin/python3.10"] + debug_args_list + ["/var/runtime/bootstrap.py"], container_env_vars=_container_env_vars, ), + Runtime.python311.value: lambda: DebugSettings( + entry + ["/var/lang/bin/python3.11"] + debug_args_list + ["/var/runtime/bootstrap.py"], + container_env_vars=_container_env_vars, + ), } try: return entrypoint_mapping[runtime]() diff --git a/samcli/local/docker/lambda_image.py b/samcli/local/docker/lambda_image.py index 23f0a770d9..923b740edc 100644 --- a/samcli/local/docker/lambda_image.py +++ b/samcli/local/docker/lambda_image.py @@ -39,6 +39,7 @@ class Runtime(Enum): python38 = "python3.8" python39 = "python3.9" python310 = "python3.10" + python311 = "python3.11" ruby27 = "ruby2.7" ruby32 = "ruby3.2" java8 = "java8" diff --git a/samcli/runtime_config.json b/samcli/runtime_config.json index 3609a8eea6..de059089e9 100644 --- a/samcli/runtime_config.json +++ b/samcli/runtime_config.json @@ -1,3 +1,3 @@ { - "app_template_repo_commit": "bb905c379830c3d8edbc196bda731076549028e3" + "app_template_repo_commit": "70788081366ff232a25a8b31961f59d27103e449" } diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index b45d2a2418..6e7787381e 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -440,6 +440,7 @@ def _validate_skipped_built_function( ("template.yaml", "Function", True, "python3.8", "Python", False, "CodeUri"), ("template.yaml", "Function", True, "python3.9", "Python", False, "CodeUri"), ("template.yaml", "Function", True, "python3.10", "Python", False, "CodeUri"), + ("template.yaml", "Function", True, "python3.11", "Python", False, "CodeUri"), ("template.yaml", "Function", True, "python3.7", "PythonPEP600", False, "CodeUri"), ("template.yaml", "Function", True, "python3.8", "PythonPEP600", False, "CodeUri"), ], @@ -479,6 +480,7 @@ def test_with_default_requirements(self): ("template.yaml", "Function", True, "python3.8", "Python", False, "CodeUri"), ("template.yaml", "Function", True, "python3.9", "Python", False, "CodeUri"), ("template.yaml", "Function", True, "python3.10", "Python", False, "CodeUri"), + ("template.yaml", "Function", True, "python3.11", "Python", False, "CodeUri"), ], ) class TestBuildCommand_PythonFunctions_WithDocker(BuildIntegPythonBase): diff --git a/tests/integration/remote/invoke/remote_invoke_integ_base.py b/tests/integration/remote/invoke/remote_invoke_integ_base.py index b0bd0cdaf8..4cf9d01a18 100644 --- a/tests/integration/remote/invoke/remote_invoke_integ_base.py +++ b/tests/integration/remote/invoke/remote_invoke_integ_base.py @@ -7,6 +7,7 @@ run_command, ) from tests.integration.deploy.deploy_integ_base import DeployIntegBase +from samcli.lib.remote_invoke.remote_invoke_executor_factory import RemoteInvokeExecutorFactory from samcli.lib.utils.boto_utils import get_boto_resource_provider_with_config, get_boto_client_provider_with_config from samcli.lib.utils.cloudformation import get_resource_summaries @@ -47,17 +48,17 @@ def remote_invoke_deploy_stack(stack_name, template_path): @classmethod def create_resources_and_boto_clients(cls): cls.remote_invoke_deploy_stack(cls.stack_name, cls.template_path) - stack_resource_summaries = get_resource_summaries( + boto_client_provider = get_boto_client_provider_with_config() + cls.stack_resource_summaries = get_resource_summaries( get_boto_resource_provider_with_config(), - get_boto_client_provider_with_config(), + boto_client_provider, cls.stack_name, ) - cls.stack_resources = { - resource_full_path: stack_resource_summary.physical_resource_id - for resource_full_path, stack_resource_summary in stack_resource_summaries.items() - } - cls.cfn_client = get_boto_client_provider_with_config()("cloudformation") - cls.lambda_client = get_boto_client_provider_with_config()("lambda") + cls.supported_resources = RemoteInvokeExecutorFactory.REMOTE_INVOKE_EXECUTOR_MAPPING.keys() + cls.cfn_client = boto_client_provider("cloudformation") + cls.lambda_client = boto_client_provider("lambda") + cls.stepfunctions_client = boto_client_provider("stepfunctions") + cls.xray_client = boto_client_provider("xray") @staticmethod def get_command_list( diff --git a/tests/integration/remote/invoke/test_lambda_invoke_response_stream.py b/tests/integration/remote/invoke/test_lambda_invoke_response_stream.py index 5adf7bdba5..ae07fc0a97 100644 --- a/tests/integration/remote/invoke/test_lambda_invoke_response_stream.py +++ b/tests/integration/remote/invoke/test_lambda_invoke_response_stream.py @@ -15,7 +15,7 @@ class TestInvokeResponseStreamingLambdas(RemoteInvokeIntegBase): @classmethod def setUpClass(cls): super().setUpClass() - cls.stack_name = f"{TestInvokeResponseStreamingLambdas.__name__}-{uuid.uuid4().hex}" + cls.stack_name = f"{cls.__name__}-{uuid.uuid4().hex}" cls.create_resources_and_boto_clients() def test_invoke_empty_event_provided(self): diff --git a/tests/integration/remote/invoke/test_remote_invoke.py b/tests/integration/remote/invoke/test_remote_invoke.py index e3eb6aa2de..7001af4ec2 100644 --- a/tests/integration/remote/invoke/test_remote_invoke.py +++ b/tests/integration/remote/invoke/test_remote_invoke.py @@ -1,8 +1,10 @@ import json import uuid import base64 +import time from parameterized import parameterized +from unittest import skip from tests.integration.remote.invoke.remote_invoke_integ_base import RemoteInvokeIntegBase from tests.testing_utils import run_command @@ -12,13 +14,13 @@ @pytest.mark.xdist_group(name="sam_remote_invoke_single_lambda_resource") -class TestSingleResourceInvoke(RemoteInvokeIntegBase): +class TestSingleLambdaInvoke(RemoteInvokeIntegBase): template = Path("template-single-lambda.yaml") @classmethod def setUpClass(cls): super().setUpClass() - cls.stack_name = f"{TestSingleResourceInvoke.__name__}-{uuid.uuid4().hex}" + cls.stack_name = f"{cls.__name__}-{uuid.uuid4().hex}" cls.create_resources_and_boto_clients() def test_invoke_empty_event_provided(self): @@ -29,7 +31,7 @@ def test_invoke_empty_event_provided(self): remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) self.assertEqual(remote_invoke_result_stdout["errorType"], "KeyError") - def test_invoke_with_only_event_provided(self): + def test_invoke_with_event_provided(self): command_list = self.get_command_list( stack_name=self.stack_name, event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', @@ -41,7 +43,7 @@ def test_invoke_with_only_event_provided(self): remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) - def test_invoke_with_only_event_file_provided(self): + def test_invoke_with_event_file_provided(self): event_file_path = str(self.events_folder_path.joinpath("default_event.json")) command_list = self.get_command_list( stack_name=self.stack_name, resource_id="HelloWorldFunction", event_file=event_file_path @@ -55,7 +57,7 @@ def test_invoke_with_only_event_file_provided(self): def test_invoke_with_resource_id_provided_as_arn(self): resource_id = "HelloWorldFunction" - lambda_name = self.stack_resources[resource_id] + lambda_name = self.stack_resource_summaries[resource_id].physical_resource_id lambda_arn = self.lambda_client.get_function(FunctionName=lambda_name)["Configuration"]["FunctionArn"] command_list = self.get_command_list( @@ -116,23 +118,88 @@ def test_invoke_response_json_output_format(self): self.assertEqual(remote_invoke_result_stdout["StatusCode"], 200) +@skip("Skip remote invoke Step function integration tests") +@pytest.mark.xdist_group(name="sam_remote_invoke_sfn_resource_priority") +class TestSFNPriorityInvoke(RemoteInvokeIntegBase): + template = Path("template-step-function-priority.yaml") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.stack_name = f"{cls.__name__}-{uuid.uuid4().hex}" + cls.create_resources_and_boto_clients() + + def test_invoke_empty_event_provided(self): + command_list = self.get_command_list(stack_name=self.stack_name) + expected_response = "Hello World" + + remote_invoke_result = run_command(command_list) + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + @parameterized.expand( + [('{"is_developer": false}', "Hello World"), ('{"is_developer": true}', "Hello Developer World")] + ) + def test_invoke_with_event_provided(self, event, expected_response): + command_list = self.get_command_list( + stack_name=self.stack_name, + event=event, + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + def test_invoke_with_event_file_provided(self): + event_file_path = str(self.events_folder_path.joinpath("sfn_input_event.json")) + expected_response = "Hello Developer World" + command_list = self.get_command_list(stack_name=self.stack_name, event_file=event_file_path) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + def test_invoke_boto_parameters(self): + expected_response = "Hello World" + command_list = self.get_command_list( + stack_name=self.stack_name, + event='{"is_developer": false}', + parameter_list=[("name", "custom-execution-name"), ("traceHeader", "Root=not enabled;Sampled=0")], + output="json", + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(json.loads(remote_invoke_result_stdout["output"]), expected_response) + + # Overriding traceHeader with not enabled will return a dummy value + dummy_trace_id_returned = remote_invoke_result_stdout["traceHeader"][5:40] + time.sleep(3) + + get_xrays_response = self.xray_client.batch_get_traces(TraceIds=[dummy_trace_id_returned]) + self.assertEqual([], get_xrays_response["Traces"]) + + @pytest.mark.xdist_group(name="sam_remote_invoke_multiple_resources") class TestMultipleResourcesInvoke(RemoteInvokeIntegBase): template = Path("template-multiple-resources.yaml") - @classmethod - def tearDownClass(cls): - # Delete the deployed stack - cls.cfn_client.delete_stack(StackName=cls.stack_name) - @classmethod def setUpClass(cls): super().setUpClass() - cls.stack_name = f"{TestMultipleResourcesInvoke.__name__}-{uuid.uuid4().hex}" + cls.stack_name = f"{cls.__name__}-{uuid.uuid4().hex}" cls.create_resources_and_boto_clients() def test_invoke_empty_event_provided(self): - command_list = self.get_command_list(stack_name=self.stack_name, resource_id="EchoEventFunction") + resource_id = "EchoEventFunction" + command_list = self.get_command_list(stack_name=self.stack_name, resource_id=resource_id) remote_invoke_result = run_command(command_list) self.assertEqual(0, remote_invoke_result.process.returncode) @@ -141,16 +208,29 @@ def test_invoke_empty_event_provided(self): @parameterized.expand( [ - ("HelloWorldServerlessFunction", {"message": "Hello world"}), - ("EchoCustomEnvVarFunction", "MyOtherVar"), - ("EchoEventFunction", {"key1": "Hello", "key2": "serverless", "key3": "world"}), + ( + "HelloWorldServerlessFunction", + '{"key1": "Hello", "key2": "serverless", "key3": "world"}', + {"message": "Hello world"}, + ), + ("EchoCustomEnvVarFunction", '{"key1": "Hello", "key2": "serverless", "key3": "world"}', "MyOtherVar"), + ( + "EchoEventFunction", + '{"key1": "Hello", "key2": "serverless", "key3": "world"}', + {"key1": "Hello", "key2": "serverless", "key3": "world"}, + ), + ("StockPriceGuideStateMachine", '{"stock_price": 60, "balance": 200, "qty": 2}', {"balance": 320}), + ("StockPriceGuideStateMachine", '{"stock_price": 30, "balance": 200, "qty": 2}', {"balance": 140}), ] ) - def test_invoke_with_only_event_provided(self, resource_id, expected_response): + def test_invoke_with_only_event_provided(self, resource_id, event, expected_response): + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") + command_list = self.get_command_list( stack_name=self.stack_name, resource_id=resource_id, - event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + event=event, ) remote_invoke_result = run_command(command_list) @@ -166,8 +246,8 @@ def test_invoke_with_only_event_provided(self, resource_id, expected_response): ("EchoEventFunction", {"key1": "Hello", "key2": "serverless", "key3": "world"}), ] ) - def test_invoke_with_resource_id_provided_as_arn(self, resource_id, expected_response): - lambda_name = self.stack_resources[resource_id] + def test_lambda_invoke_with_resource_id_provided_as_arn(self, resource_id, expected_response): + lambda_name = self.stack_resource_summaries[resource_id].physical_resource_id lambda_arn = self.lambda_client.get_function(FunctionName=lambda_name)["Configuration"]["FunctionArn"] command_list = self.get_command_list( @@ -182,7 +262,7 @@ def test_invoke_with_resource_id_provided_as_arn(self, resource_id, expected_res self.assertEqual(remote_invoke_result_stdout, expected_response) def test_lambda_writes_to_stderr_invoke(self): - command_list = RemoteInvokeIntegBase.get_command_list( + command_list = self.get_command_list( stack_name=self.stack_name, resource_id="WriteToStderrFunction", event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', @@ -229,48 +309,112 @@ def test_lambda_invoke_client_context_boto_parameter(self): remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) self.assertEqual(remote_invoke_result_stdout, custom_json_str["custom"]) + def test_sfn_invoke_with_resource_id_provided_as_arn(self): + resource_id = "StockPriceGuideStateMachine" + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") + expected_response = {"balance": 320} + state_machine_arn = self.stack_resource_summaries[resource_id].physical_resource_id + + command_list = self.get_command_list( + resource_id=state_machine_arn, + event='{"stock_price": 60, "balance": 200, "qty": 2}', + ) + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + def test_sfn_invoke_boto_parameters(self): + resource_id = "StockPriceGuideStateMachine" + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") + expected_response = {"balance": 320} + name = "custom-execution-name" + command_list = self.get_command_list( + stack_name=self.stack_name, + resource_id=resource_id, + event='{"stock_price": 60, "balance": 200, "qty": 2}', + parameter_list=[("name", name)], + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + def test_sfn_invoke_execution_fails(self): + resource_id = "StateMachineExecutionFails" + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") + expected_response = "The execution failed due to the error: MockError and cause: Mock Invalid response." + command_list = self.get_command_list( + stack_name=self.stack_name, + resource_id=resource_id, + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + ) + + remote_invoke_result = run_command(command_list) + remote_invoke_stderr = remote_invoke_result.stderr.strip().decode() + + self.assertEqual(0, remote_invoke_result.process.returncode) + self.assertIn(expected_response, remote_invoke_stderr) + @pytest.mark.xdist_group(name="sam_remote_invoke_nested_resources") class TestNestedTemplateResourcesInvoke(RemoteInvokeIntegBase): template = Path("nested_templates/template.yaml") - @classmethod - def tearDownClass(cls): - # Delete the deployed stack - cls.cfn_client.delete_stack(StackName=cls.stack_name) - @classmethod def setUpClass(cls): super().setUpClass() - cls.stack_name = f"{TestNestedTemplateResourcesInvoke.__name__}-{uuid.uuid4().hex}" + cls.stack_name = f"{cls.__name__}-{uuid.uuid4().hex}" cls.create_resources_and_boto_clients() - def test_invoke_empty_event_provided(self): - command_list = self.get_command_list( - stack_name=self.stack_name, - ) + @parameterized.expand( + [ + ("ChildStack/HelloWorldFunction", {"message": "Hello world"}), + ("ChildStack/HelloWorldStateMachine", "World"), + ] + ) + def test_invoke_empty_event_provided(self, resource_id, expected_response): + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") + command_list = self.get_command_list(stack_name=self.stack_name, resource_id=resource_id) remote_invoke_result = run_command(command_list) self.assertEqual(0, remote_invoke_result.process.returncode) remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) - self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) + self.assertEqual(remote_invoke_result_stdout, expected_response) - def test_invoke_with_only_event_provided(self): + @parameterized.expand( + [ + ("ChildStack/HelloWorldFunction", '{"key1": "Hello", "key2": "world"}', {"message": "Hello world"}), + ("ChildStack/HelloWorldStateMachine", '{"key1": "Hello", "key2": "world"}', "World"), + ] + ) + def test_invoke_with_event_provided(self, resource_id, event, expected_response): + if self.stack_resource_summaries[resource_id].resource_type not in self.supported_resources: + pytest.skip("Skip remote invoke Step function integration tests as resource is not supported") command_list = self.get_command_list( stack_name=self.stack_name, - resource_id="ChildStack/HelloWorldFunction", - event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + resource_id=resource_id, + event=event, ) remote_invoke_result = run_command(command_list) self.assertEqual(0, remote_invoke_result.process.returncode) remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) - self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) + self.assertEqual(remote_invoke_result_stdout, expected_response) - def test_invoke_default_lambda_function(self): + def test_invoke_event_file_provided(self): event_file_path = str(self.events_folder_path.joinpath("default_event.json")) - command_list = self.get_command_list(stack_name=self.stack_name, event_file=event_file_path) + command_list = self.get_command_list( + stack_name=self.stack_name, resource_id="ChildStack/HelloWorldFunction", event_file=event_file_path + ) remote_invoke_result = run_command(command_list) diff --git a/tests/integration/testdata/remote_invoke/events/sfn_input_event.json b/tests/integration/testdata/remote_invoke/events/sfn_input_event.json new file mode 100644 index 0000000000..f9b87ca27e --- /dev/null +++ b/tests/integration/testdata/remote_invoke/events/sfn_input_event.json @@ -0,0 +1,3 @@ +{ + "is_developer": true +} \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/lambda-fns/main.py b/tests/integration/testdata/remote_invoke/lambda-fns/main.py index b3424d1b70..5770775adf 100644 --- a/tests/integration/testdata/remote_invoke/lambda-fns/main.py +++ b/tests/integration/testdata/remote_invoke/lambda-fns/main.py @@ -28,4 +28,28 @@ def echo_event(event, context): return event def raise_exception(event, context): - raise Exception("Lambda is raising an exception") \ No newline at end of file + raise Exception("Lambda is raising an exception") + +def stock_transaction_recommender(event, context): + stock_price = int(event["stock_price"]) + balance = event["balance"] + qty = event["qty"] + if qty*stock_price < 100: + stock_action = "Buy" + else: + stock_action = "Sell" + return {"stock_price": stock_price, "action": stock_action, "balance": balance, "qty": qty} + +def stock_buyer(event, context): + current_balance = event["balance"] + new_balance = current_balance - (event["qty"]*event["stock_price"]) + return { + "balance": new_balance + } + +def stock_seller(event, context): + current_balance = event["balance"] + new_balance = current_balance + (event["qty"]*event["stock_price"]) + return { + "balance": new_balance + } \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py b/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py index cce4a03dca..a6856732b2 100644 --- a/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py +++ b/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py @@ -1,4 +1,4 @@ def handler(event, context): return { "message": "Hello world", - } \ No newline at end of file + } diff --git a/tests/integration/testdata/remote_invoke/nested_templates/childstack/state-machines/hello-world-state-machine-definition.asl.json b/tests/integration/testdata/remote_invoke/nested_templates/childstack/state-machines/hello-world-state-machine-definition.asl.json new file mode 100644 index 0000000000..d2b8ad8e1a --- /dev/null +++ b/tests/integration/testdata/remote_invoke/nested_templates/childstack/state-machines/hello-world-state-machine-definition.asl.json @@ -0,0 +1,16 @@ +{ + "Comment": "A Hello World example of the Amazon States Language using Pass states", + "StartAt": "Hello", + "States": { + "Hello": { + "Type": "Pass", + "Result": "Hello", + "Next": "World" + }, + "World": { + "Type": "Pass", + "Result": "World", + "End": true + } + } +} \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml b/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml index c082eb0fe4..0f78d5a2a2 100644 --- a/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml +++ b/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml @@ -8,4 +8,9 @@ Resources: Handler: app.handler Runtime: python3.9 CodeUri: function/ - Timeout: 30 \ No newline at end of file + Timeout: 30 + + HelloWorldStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + DefinitionUri: ./state-machines/hello-world-state-machine-definition.asl.json \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/state-machines/execution-fails-state-machine-definition.asl.json b/tests/integration/testdata/remote_invoke/state-machines/execution-fails-state-machine-definition.asl.json new file mode 100644 index 0000000000..319d062841 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/state-machines/execution-fails-state-machine-definition.asl.json @@ -0,0 +1,16 @@ +{ + "Comment": "A Simple example of the Amazon States Language using Pass and Fail states", + "StartAt": "Hello", + "States": { + "Hello": { + "Type": "Pass", + "Result": "Hello", + "Next": "World" + }, + "World": { + "Type": "Fail", + "Cause": "Mock Invalid response.", + "Error": "MockError" + } + } + } \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/state-machines/hello-world-state-machine-definition.asl.json b/tests/integration/testdata/remote_invoke/state-machines/hello-world-state-machine-definition.asl.json new file mode 100644 index 0000000000..5cc4c63b8b --- /dev/null +++ b/tests/integration/testdata/remote_invoke/state-machines/hello-world-state-machine-definition.asl.json @@ -0,0 +1,32 @@ +{ + "Comment": "A Hello World example of the Amazon States Language using Pass states", + "StartAt": "Type of World", + "States": { + "Type of World": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.is_developer", + "IsPresent": false, + "Next": "World" + }, + { + "Variable": "$.is_developer", + "BooleanEquals": true, + "Next": "Developer World" + } + ], + "Default": "World" + }, + "World": { + "Type": "Pass", + "Result": "Hello World", + "End": true + }, + "Developer World": { + "Type": "Pass", + "Result": "Hello Developer World", + "End": true + } + } +} \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/state-machines/stock-trader-state-machine-definition.asl.json b/tests/integration/testdata/remote_invoke/state-machines/stock-trader-state-machine-definition.asl.json new file mode 100644 index 0000000000..5e23a065d1 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/state-machines/stock-trader-state-machine-definition.asl.json @@ -0,0 +1,62 @@ +{ + "Comment": "A state machine that does mock stock trading.", + "StartAt": "Recommend Stock Transaction", + "States": { + "Recommend Stock Transaction": { + "Type": "Task", + "Resource": "${StockActionRecommenderFunction}", + "Retry": [ + { + "ErrorEquals": [ + "States.TaskFailed" + ], + "IntervalSeconds": 14, + "MaxAttempts": 1, + "BackoffRate": 1.5 + } + ], + "Next": "Buy or Sell?" + }, + "Buy or Sell?": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.action", + "StringEquals": "Buy", + "Next": "Buy Stock" + } + ], + "Default": "Sell Stock" + }, + "Sell Stock": { + "Type": "Task", + "Resource": "${StockSellerFunctionArn}", + "Retry": [ + { + "ErrorEquals": [ + "States.TaskFailed" + ], + "IntervalSeconds": 2, + "MaxAttempts": 3, + "BackoffRate": 1 + } + ], + "End": true + }, + "Buy Stock": { + "Type": "Task", + "Resource": "${StockBuyerFunctionArn}", + "Retry": [ + { + "ErrorEquals": [ + "States.TaskFailed" + ], + "IntervalSeconds": 2, + "MaxAttempts": 3, + "BackoffRate": 1 + } + ], + "End": true + } + } +} \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml b/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml index ffebe530c1..a9e3f128a1 100644 --- a/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml +++ b/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml @@ -52,4 +52,46 @@ Resources: Handler: main.raise_exception Runtime: python3.9 CodeUri: ./lambda-fns - Timeout: 5 \ No newline at end of file + Timeout: 5 + + StateMachineExecutionFails: + Type: AWS::Serverless::StateMachine + Properties: + DefinitionUri: ./state-machines/execution-fails-state-machine-definition.asl.json + + StockPriceGuideStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + DefinitionUri: ./state-machines/stock-trader-state-machine-definition.asl.json + DefinitionSubstitutions: + StockActionRecommenderFunction: !GetAtt StockActionRecommenderFunction.Arn + StockSellerFunctionArn: !GetAtt StockSellerFunction.Arn + StockBuyerFunctionArn: !GetAtt StockBuyerFunction.Arn + Policies: # Find out more about SAM policy templates: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-policy-templates.html + - LambdaInvokePolicy: + FunctionName: !Ref StockActionRecommenderFunction + - LambdaInvokePolicy: + FunctionName: !Ref StockSellerFunction + - LambdaInvokePolicy: + FunctionName: !Ref StockBuyerFunction + + StockActionRecommenderFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html + Properties: + CodeUri: lambda-fns/ + Handler: main.stock_transaction_recommender + Runtime: python3.9 + + StockSellerFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: lambda-fns/ + Handler: main.stock_seller + Runtime: python3.9 + + StockBuyerFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: lambda-fns/ + Handler: main.stock_buyer + Runtime: python3.9 \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/template-step-function-priority.yaml b/tests/integration/testdata/remote_invoke/template-step-function-priority.yaml new file mode 100644 index 0000000000..e857d7d3d2 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/template-step-function-priority.yaml @@ -0,0 +1,11 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A hello world application with a step function. + +Resources: + HelloWorldStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + DefinitionUri: ./state-machines/hello-world-state-machine-definition.asl.json + Tracing: + Enabled: true \ No newline at end of file diff --git a/tests/unit/local/docker/test_lambda_container.py b/tests/unit/local/docker/test_lambda_container.py index 90893e4e49..bffa8bc1cc 100644 --- a/tests/unit/local/docker/test_lambda_container.py +++ b/tests/unit/local/docker/test_lambda_container.py @@ -23,6 +23,7 @@ Runtime.python38.value, Runtime.python39.value, Runtime.python310.value, + Runtime.python311.value, Runtime.dotnet6.value, ] diff --git a/tests/unit/local/docker/test_lambda_debug_settings.py b/tests/unit/local/docker/test_lambda_debug_settings.py index 1dd024ebc2..a72a9722a9 100644 --- a/tests/unit/local/docker/test_lambda_debug_settings.py +++ b/tests/unit/local/docker/test_lambda_debug_settings.py @@ -20,6 +20,7 @@ Runtime.python38, Runtime.python39, Runtime.python310, + Runtime.python311, ]