diff --git a/moto/awslambda/exceptions.py b/moto/awslambda/exceptions.py index 9107e1ac5..d759060c5 100644 --- a/moto/awslambda/exceptions.py +++ b/moto/awslambda/exceptions.py @@ -89,3 +89,9 @@ class UnknownPolicyException(LambdaClientError): "ResourceNotFoundException", "No policy is associated with the given resource.", ) + + +class ValidationException(LambdaClientError): + def __init__(self, value: str, property_name: str, specific_message: str): + message = f"1 validation error detected: Value '{value}' at '{property_name}' failed to satisfy constraint: {specific_message}" + super().__init__("ValidationException", message) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 2aa35c9d0..75bbac5c1 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -45,6 +45,7 @@ from .exceptions import ( UnknownLayerVersionException, UnknownFunctionException, UnknownAliasException, + ValidationException, ) from .utils import ( make_function_arn, @@ -196,6 +197,22 @@ def _validate_s3_bucket_and_key( return key +class ImageConfig: + def __init__(self, config: Dict[str, Any]) -> None: + self.cmd = config.get("Command", []) + self.entry_point = config.get("EntryPoint", []) + self.working_directory = config.get("WorkingDirectory", "") + + def response(self) -> Dict[str, Any]: + return dict( + { + "Command": self.cmd, + "EntryPoint": self.entry_point, + "WorkingDirectory": self.working_directory, + } + ) + + class Permission(CloudFormationModel): def __init__(self, region: str): self.region = region @@ -429,6 +446,10 @@ class LambdaFunction(CloudFormationModel, DockerModel): self.reserved_concurrency = spec.get("ReservedConcurrentExecutions", None) # optional + self.ephemeral_storage: str + self.code_digest: str + self.code_bytes: bytes + self.description = spec.get("Description", "") self.memory_size = spec.get("MemorySize", 128) self.package_type = spec.get("PackageType", None) @@ -439,6 +460,13 @@ class LambdaFunction(CloudFormationModel, DockerModel): self.signing_job_arn = spec.get("SigningJobArn") self.code_signing_config_arn = spec.get("CodeSigningConfigArn") self.tracing_config = spec.get("TracingConfig") or {"Mode": "PassThrough"} + self.architectures: List[str] = spec.get("Architectures", ["x86_64"]) + self.image_config: ImageConfig = ImageConfig(spec.get("ImageConfig", {})) + _es = spec.get("EphemeralStorage") + if _es: + self.ephemeral_storage = _es["Size"] + else: + self.ephemeral_storage = 512 self.logs_group_name = f"/aws/lambda/{self.function_name}" @@ -531,6 +559,41 @@ class LambdaFunction(CloudFormationModel, DockerModel): datetime.datetime.utcnow() ) + @property + def architectures(self) -> List[str]: + return self._architectures + + @architectures.setter + def architectures(self, architectures: List[str]) -> None: + if ( + len(architectures) > 1 + or not architectures + or architectures[0] not in ("x86_64", "arm64") + ): + raise ValidationException( + str(architectures), + "architectures", + "Member must satisfy constraint: " + "[Member must satisfy enum value set: [x86_64, arm64], Member must not be null]", + ) + self._architectures = architectures + + @property + def ephemeral_storage(self) -> int: + return self._ephemeral_storage + + @ephemeral_storage.setter + def ephemeral_storage(self, ephemeral_storage: int) -> None: + if ephemeral_storage > 10240: + raise ValidationException( + str(ephemeral_storage), + "ephemeralStorage.size", + "Member must have value less than or equal to 10240", + ) + + # ephemeral_storage < 512 is handled by botocore 1.30.0 + self._ephemeral_storage = ephemeral_storage + @property def vpc_config(self) -> Dict[str, Any]: # type: ignore[misc] config = self._vpc_config.copy() @@ -582,7 +645,16 @@ class LambdaFunction(CloudFormationModel, DockerModel): "SigningProfileVersionArn": self.signing_profile_version_arn, "SigningJobArn": self.signing_job_arn, "TracingConfig": self.tracing_config, + "Architectures": self.architectures, + "EphemeralStorage": { + "Size": self.ephemeral_storage, + }, + "SnapStart": {"ApplyOn": "None", "OptimizationStatus": "Off"}, } + if self.package_type == "Image": + config["ImageConfigResponse"] = { + "ImageConfig": self.image_config.response(), + } if not on_create: # Only return this variable after the first creation config["LastUpdateStatus"] = "Successful" diff --git a/tests/terraformtests/README.md b/tests/terraformtests/README.md index ea386aa80..9b36f6e27 100644 --- a/tests/terraformtests/README.md +++ b/tests/terraformtests/README.md @@ -2,23 +2,33 @@ Documentation on how to run Terraform Tests can be found here: http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html#terraform-tests +To get started you need to have [Go](https://go.dev/doc/install) installed. + +One time setup: + +```bash +go mod init moto +``` + To see a list of available tests: -``` +```bash cd tests/terraformtests/terraform-provider-aws +git submodule init +git submodule update go test ./internal/service/elb/ -v -list TestAcc ``` To run a specific test: -``` +```bash moto_server -p 4566 make terraformtests SERVICE_NAME=elb TEST_NAMES=NewTestName ``` To see the list of tests that currently pass: -``` +```bash python tests/terraformtests/get_tf_services.py python tests/terraformtests/get_tf_tests.py ec2 ``` diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 0a52ebafc..93f716126 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -6,7 +6,7 @@ import boto3 import hashlib import pytest -from botocore.exceptions import ClientError +from botocore.exceptions import ClientError, ParamValidationError from freezegun import freeze_time from tests.test_ecr.test_ecr_helpers import _create_image_manifest from moto import mock_lambda, mock_s3, mock_ecr, settings @@ -172,6 +172,8 @@ def test_create_function_from_zipfile(): result.pop("LastModified") assert result == { + "Architectures": ["x86_64"], + "EphemeralStorage": {"Size": 512}, "FunctionName": function_name, "FunctionArn": f"arn:aws:lambda:{_lambda_region}:{ACCOUNT_ID}:function:{function_name}", "Runtime": "python2.7", @@ -190,9 +192,122 @@ def test_create_function_from_zipfile(): "State": "Active", "Layers": [], "TracingConfig": {"Mode": "PassThrough"}, + "SnapStart": {"ApplyOn": "None", "OptimizationStatus": "Off"}, } +@mock_lambda +def test_create_function_from_image(): + conn = boto3.client("lambda", _lambda_region) + function_name = str(uuid4())[0:6] + image_uri = f"{ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/testlambdaecr:prod" + image_config = { + "EntryPoint": [ + "python", + ], + "Command": [ + "/opt/app.py", + ], + "WorkingDirectory": "/opt", + } + conn.create_function( + FunctionName=function_name, + Role=get_role_name(), + Code={"ImageUri": image_uri}, + Description="test lambda function", + ImageConfig=image_config, + PackageType="Image", + Timeout=3, + MemorySize=128, + Publish=True, + ) + + result = conn.get_function(FunctionName=function_name) + + assert "ImageConfigResponse" in result["Configuration"] + assert result["Configuration"]["ImageConfigResponse"]["ImageConfig"] == image_config + + +@mock_lambda +def test_create_function_error_bad_architecture(): + conn = boto3.client("lambda", _lambda_region) + function_name = str(uuid4())[0:6] + image_uri = f"{ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/testlambdaecr:prod" + + with pytest.raises(ClientError) as exc: + conn.create_function( + Architectures=["foo"], + FunctionName=function_name, + Role=get_role_name(), + Code={"ImageUri": image_uri}, + Description="test lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + ) + + err = exc.value.response + + assert err["Error"]["Code"] == "ValidationException" + assert ( + err["Error"]["Message"] + == "1 validation error detected: Value '['foo']' at 'architectures' failed to satisfy" + " constraint: Member must satisfy constraint: [Member must satisfy enum value set: " + "[x86_64, arm64], Member must not be null]" + ) + + +@mock_lambda +def test_create_function_error_ephemeral_too_big(): + conn = boto3.client("lambda", _lambda_region) + function_name = str(uuid4())[0:6] + image_uri = f"{ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/testlambdaecr:prod" + + with pytest.raises(ClientError) as exc: + conn.create_function( + FunctionName=function_name, + Role=get_role_name(), + Code={"ImageUri": image_uri}, + Description="test lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + EphemeralStorage={"Size": 3000000}, + ) + + err = exc.value.response + + assert err["Error"]["Code"] == "ValidationException" + assert ( + err["Error"]["Message"] + == "1 validation error detected: Value '3000000' at 'ephemeralStorage.size' " + "failed to satisfy constraint: " + "Member must have value less than or equal to 10240" + ) + + +@mock_lambda +def test_create_function_error_ephemeral_too_small(): + conn = boto3.client("lambda", _lambda_region) + function_name = str(uuid4())[0:6] + image_uri = f"{ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/testlambdaecr:prod" + + with pytest.raises(ParamValidationError) as exc: + conn.create_function( + FunctionName=function_name, + Role=get_role_name(), + Code={"ImageUri": image_uri}, + Description="test lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + EphemeralStorage={"Size": 200}, + ) + + # this one is handled by botocore, not moto + assert exc.typename == "ParamValidationError" + + @mock_lambda @pytest.mark.parametrize( "tracing_mode", @@ -762,6 +877,7 @@ def test_list_create_list_get_delete_list(): Handler="lambda_function.lambda_handler", Code={"S3Bucket": bucket_name, "S3Key": "test.zip"}, Description="test lambda function", + EphemeralStorage={"Size": 2500}, Timeout=3, MemorySize=128, Publish=True, @@ -789,6 +905,9 @@ def test_list_create_list_get_delete_list(): "Layers": [], "LastUpdateStatus": "Successful", "TracingConfig": {"Mode": "PassThrough"}, + "Architectures": ["x86_64"], + "EphemeralStorage": {"Size": 2500}, + "SnapStart": {"ApplyOn": "None", "OptimizationStatus": "Off"}, }, "ResponseMetadata": {"HTTPStatusCode": 200}, }