diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index d2696e6af..7b35c34ef 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -5110,7 +5110,7 @@ - [ ] delete_alias - [X] delete_event_source_mapping - [X] delete_function -- [ ] delete_function_concurrency +- [X] delete_function_concurrency - [ ] delete_function_event_invoke_config - [ ] delete_layer_version - [ ] delete_provisioned_concurrency_config @@ -5118,7 +5118,7 @@ - [ ] get_alias - [X] get_event_source_mapping - [X] get_function -- [ ] get_function_concurrency +- [X] get_function_concurrency - [ ] get_function_configuration - [ ] get_function_event_invoke_config - [ ] get_layer_version @@ -5139,7 +5139,7 @@ - [X] list_versions_by_function - [ ] publish_layer_version - [ ] publish_version -- [ ] put_function_concurrency +- [X] put_function_concurrency - [ ] put_function_event_invoke_config - [ ] put_provisioned_concurrency_config - [ ] remove_layer_version_permission diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 2c0d4d9e2..2aa207da9 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -165,6 +165,7 @@ class LambdaFunction(CloudFormationModel): self.docker_client = docker.from_env() self.policy = None self.state = "Active" + self.reserved_concurrency = spec.get("ReservedConcurrentExecutions", None) # Unfortunately mocking replaces this method w/o fallback enabled, so we # need to replace it if we detect it's been mocked @@ -285,7 +286,7 @@ class LambdaFunction(CloudFormationModel): return config def get_code(self): - return { + code = { "Code": { "Location": "s3://awslambda-{0}-tasks.s3-{0}.amazonaws.com/{1}".format( self.region, self.code["S3Key"] @@ -294,6 +295,15 @@ class LambdaFunction(CloudFormationModel): }, "Configuration": self.get_configuration(), } + if self.reserved_concurrency: + code.update( + { + "Concurrency": { + "ReservedConcurrentExecutions": self.reserved_concurrency + } + } + ) + return code def update_configuration(self, config_updates): for key, value in config_updates.items(): @@ -511,6 +521,15 @@ class LambdaFunction(CloudFormationModel): cls, resource_name, cloudformation_json, region_name ): properties = cloudformation_json["Properties"] + optional_properties = ( + "Description", + "MemorySize", + "Publish", + "Timeout", + "VpcConfig", + "Environment", + "ReservedConcurrentExecutions", + ) # required spec = { @@ -520,9 +539,7 @@ class LambdaFunction(CloudFormationModel): "Role": properties["Role"], "Runtime": properties["Runtime"], } - optional_properties = ( - "Description MemorySize Publish Timeout VpcConfig Environment".split() - ) + # NOTE: Not doing `properties.get(k, DEFAULT)` to avoid duplicating the # default logic for prop in optional_properties: @@ -1157,6 +1174,20 @@ class LambdaBackend(BaseBackend): else: return None + def put_function_concurrency(self, function_name, reserved_concurrency): + fn = self.get_function(function_name) + fn.reserved_concurrency = reserved_concurrency + return fn.reserved_concurrency + + def delete_function_concurrency(self, function_name): + fn = self.get_function(function_name) + fn.reserved_concurrency = None + return fn.reserved_concurrency + + def get_function_concurrency(self, function_name): + fn = self.get_function(function_name) + return fn.reserved_concurrency + def do_validate_s3(): return os.environ.get("VALIDATE_LAMBDA_S3", "") in ["", "1", "true"] diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index a4f559fc2..6447cde13 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -141,6 +141,19 @@ class LambdaResponse(BaseResponse): else: raise ValueError("Cannot handle request") + def function_concurrency(self, request, full_url, headers): + http_method = request.method + self.setup_class(request, full_url, headers) + + if http_method == "GET": + return self._get_function_concurrency(request) + elif http_method == "DELETE": + return self._delete_function_concurrency(request) + elif http_method == "PUT": + return self._put_function_concurrency(request) + else: + raise ValueError("Cannot handle request") + def _add_policy(self, request, full_url, headers): path = request.path if hasattr(request, "path") else path_url(request.url) function_name = path.split("/")[-2] @@ -359,3 +372,38 @@ class LambdaResponse(BaseResponse): return 200, {}, json.dumps(resp) else: return 404, {}, "{}" + + def _get_function_concurrency(self, request): + path_function_name = self.path.rsplit("/", 2)[-2] + function_name = self.lambda_backend.get_function(path_function_name) + + if function_name is None: + return 404, {}, "{}" + + resp = self.lambda_backend.get_function_concurrency(path_function_name) + return 200, {}, json.dumps({"ReservedConcurrentExecutions": resp}) + + def _delete_function_concurrency(self, request): + path_function_name = self.path.rsplit("/", 2)[-2] + function_name = self.lambda_backend.get_function(path_function_name) + + if function_name is None: + return 404, {}, "{}" + + self.lambda_backend.delete_function_concurrency(path_function_name) + + return 204, {}, "{}" + + def _put_function_concurrency(self, request): + path_function_name = self.path.rsplit("/", 2)[-2] + function = self.lambda_backend.get_function(path_function_name) + + if function is None: + return 404, {}, "{}" + + concurrency = self._get_param("ReservedConcurrentExecutions", None) + resp = self.lambda_backend.put_function_concurrency( + path_function_name, concurrency + ) + + return 200, {}, json.dumps({"ReservedConcurrentExecutions": resp}) diff --git a/moto/awslambda/urls.py b/moto/awslambda/urls.py index c25e58dba..03cedc5e4 100644 --- a/moto/awslambda/urls.py +++ b/moto/awslambda/urls.py @@ -19,4 +19,5 @@ url_paths = { r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/policy/?$": response.policy, r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/configuration/?$": response.configuration, r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/code/?$": response.code, + r"{0}/(?P[^/]+)/functions/(?P[\w_-]+)/concurrency/?$": response.function_concurrency, } diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 1cd943f04..ca05d4aa4 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -489,7 +489,7 @@ def test_get_function(): {"test_variable": "test_value"} ) - # Test get function with + # Test get function with qualifier result = conn.get_function(FunctionName="testFunction", Qualifier="$LATEST") result["Configuration"]["Version"].should.equal("$LATEST") result["Configuration"]["FunctionArn"].should.equal( @@ -1721,6 +1721,82 @@ def test_remove_function_permission(): policy["Statement"].should.equal([]) +@mock_lambda +def test_put_function_concurrency(): + expected_concurrency = 15 + function_name = "test" + + conn = boto3.client("lambda", _lambda_region) + conn.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=(get_role_name()), + Handler="lambda_function.handler", + Code={"ZipFile": get_test_zip_file1()}, + Description="test lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + ) + result = conn.put_function_concurrency( + FunctionName=function_name, ReservedConcurrentExecutions=expected_concurrency + ) + + result["ReservedConcurrentExecutions"].should.equal(expected_concurrency) + + +@mock_lambda +def test_delete_function_concurrency(): + function_name = "test" + + conn = boto3.client("lambda", _lambda_region) + conn.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=(get_role_name()), + Handler="lambda_function.handler", + Code={"ZipFile": get_test_zip_file1()}, + Description="test lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + ) + conn.put_function_concurrency( + FunctionName=function_name, ReservedConcurrentExecutions=15 + ) + + conn.delete_function_concurrency(FunctionName=function_name) + result = conn.get_function(FunctionName=function_name) + + result.doesnt.have.key("Concurrency") + + +@mock_lambda +def test_get_function_concurrency(): + expected_concurrency = 15 + function_name = "test" + + conn = boto3.client("lambda", _lambda_region) + conn.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=(get_role_name()), + Handler="lambda_function.handler", + Code={"ZipFile": get_test_zip_file1()}, + Description="test lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + ) + conn.put_function_concurrency( + FunctionName=function_name, ReservedConcurrentExecutions=expected_concurrency + ) + + result = conn.get_function_concurrency(FunctionName=function_name) + + result["ReservedConcurrentExecutions"].should.equal(expected_concurrency) + + def create_invalid_lambda(role): conn = boto3.client("lambda", _lambda_region) zip_content = get_test_zip_file1() diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 5a8e9cd68..ee2fbc94c 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -1777,6 +1777,7 @@ def lambda_handler(event, context): "Role": {"Fn::GetAtt": ["MyRole", "Arn"]}, "Runtime": "python2.7", "Environment": {"Variables": {"TEST_ENV_KEY": "test-env-val"}}, + "ReservedConcurrentExecutions": 10, }, }, "MyRole": { @@ -1811,6 +1812,11 @@ def lambda_handler(event, context): {"Variables": {"TEST_ENV_KEY": "test-env-val"}} ) + function_name = result["Functions"][0]["FunctionName"] + result = conn.get_function(FunctionName=function_name) + + result["Concurrency"]["ReservedConcurrentExecutions"].should.equal(10) + @mock_cloudformation @mock_ec2