Lambda reserved concurrency (#3215)
* lambda-responses: add method to dispatch concurrency calls * lambda-resources: add route to handle concurrency requests * lambda-model: implement put_function_concurrency and concurrency attribute * put-concurrency-tests: add one simple test * get_function: add concurrency entry - with test * lambda-reserved-concurrency: cloudformation support * lambda-concurrency: implement delete_reserved with tests * lambda-concurrency: implement get_reserved with tests * lint * implementation-cov: mark delete_function_concurrency, put_function_concurrency and get_function_concurrency * botocore doesn't display concurrency entry for lambdas without it * lambda(refactor): improvements on response's handler
This commit is contained in:
parent
47a227921d
commit
f744356da7
@ -5110,7 +5110,7 @@
|
|||||||
- [ ] delete_alias
|
- [ ] delete_alias
|
||||||
- [X] delete_event_source_mapping
|
- [X] delete_event_source_mapping
|
||||||
- [X] delete_function
|
- [X] delete_function
|
||||||
- [ ] delete_function_concurrency
|
- [X] delete_function_concurrency
|
||||||
- [ ] delete_function_event_invoke_config
|
- [ ] delete_function_event_invoke_config
|
||||||
- [ ] delete_layer_version
|
- [ ] delete_layer_version
|
||||||
- [ ] delete_provisioned_concurrency_config
|
- [ ] delete_provisioned_concurrency_config
|
||||||
@ -5118,7 +5118,7 @@
|
|||||||
- [ ] get_alias
|
- [ ] get_alias
|
||||||
- [X] get_event_source_mapping
|
- [X] get_event_source_mapping
|
||||||
- [X] get_function
|
- [X] get_function
|
||||||
- [ ] get_function_concurrency
|
- [X] get_function_concurrency
|
||||||
- [ ] get_function_configuration
|
- [ ] get_function_configuration
|
||||||
- [ ] get_function_event_invoke_config
|
- [ ] get_function_event_invoke_config
|
||||||
- [ ] get_layer_version
|
- [ ] get_layer_version
|
||||||
@ -5139,7 +5139,7 @@
|
|||||||
- [X] list_versions_by_function
|
- [X] list_versions_by_function
|
||||||
- [ ] publish_layer_version
|
- [ ] publish_layer_version
|
||||||
- [ ] publish_version
|
- [ ] publish_version
|
||||||
- [ ] put_function_concurrency
|
- [X] put_function_concurrency
|
||||||
- [ ] put_function_event_invoke_config
|
- [ ] put_function_event_invoke_config
|
||||||
- [ ] put_provisioned_concurrency_config
|
- [ ] put_provisioned_concurrency_config
|
||||||
- [ ] remove_layer_version_permission
|
- [ ] remove_layer_version_permission
|
||||||
|
@ -165,6 +165,7 @@ class LambdaFunction(CloudFormationModel):
|
|||||||
self.docker_client = docker.from_env()
|
self.docker_client = docker.from_env()
|
||||||
self.policy = None
|
self.policy = None
|
||||||
self.state = "Active"
|
self.state = "Active"
|
||||||
|
self.reserved_concurrency = spec.get("ReservedConcurrentExecutions", None)
|
||||||
|
|
||||||
# Unfortunately mocking replaces this method w/o fallback enabled, so we
|
# Unfortunately mocking replaces this method w/o fallback enabled, so we
|
||||||
# need to replace it if we detect it's been mocked
|
# need to replace it if we detect it's been mocked
|
||||||
@ -285,7 +286,7 @@ class LambdaFunction(CloudFormationModel):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
def get_code(self):
|
def get_code(self):
|
||||||
return {
|
code = {
|
||||||
"Code": {
|
"Code": {
|
||||||
"Location": "s3://awslambda-{0}-tasks.s3-{0}.amazonaws.com/{1}".format(
|
"Location": "s3://awslambda-{0}-tasks.s3-{0}.amazonaws.com/{1}".format(
|
||||||
self.region, self.code["S3Key"]
|
self.region, self.code["S3Key"]
|
||||||
@ -294,6 +295,15 @@ class LambdaFunction(CloudFormationModel):
|
|||||||
},
|
},
|
||||||
"Configuration": self.get_configuration(),
|
"Configuration": self.get_configuration(),
|
||||||
}
|
}
|
||||||
|
if self.reserved_concurrency:
|
||||||
|
code.update(
|
||||||
|
{
|
||||||
|
"Concurrency": {
|
||||||
|
"ReservedConcurrentExecutions": self.reserved_concurrency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return code
|
||||||
|
|
||||||
def update_configuration(self, config_updates):
|
def update_configuration(self, config_updates):
|
||||||
for key, value in config_updates.items():
|
for key, value in config_updates.items():
|
||||||
@ -511,6 +521,15 @@ class LambdaFunction(CloudFormationModel):
|
|||||||
cls, resource_name, cloudformation_json, region_name
|
cls, resource_name, cloudformation_json, region_name
|
||||||
):
|
):
|
||||||
properties = cloudformation_json["Properties"]
|
properties = cloudformation_json["Properties"]
|
||||||
|
optional_properties = (
|
||||||
|
"Description",
|
||||||
|
"MemorySize",
|
||||||
|
"Publish",
|
||||||
|
"Timeout",
|
||||||
|
"VpcConfig",
|
||||||
|
"Environment",
|
||||||
|
"ReservedConcurrentExecutions",
|
||||||
|
)
|
||||||
|
|
||||||
# required
|
# required
|
||||||
spec = {
|
spec = {
|
||||||
@ -520,9 +539,7 @@ class LambdaFunction(CloudFormationModel):
|
|||||||
"Role": properties["Role"],
|
"Role": properties["Role"],
|
||||||
"Runtime": properties["Runtime"],
|
"Runtime": properties["Runtime"],
|
||||||
}
|
}
|
||||||
optional_properties = (
|
|
||||||
"Description MemorySize Publish Timeout VpcConfig Environment".split()
|
|
||||||
)
|
|
||||||
# NOTE: Not doing `properties.get(k, DEFAULT)` to avoid duplicating the
|
# NOTE: Not doing `properties.get(k, DEFAULT)` to avoid duplicating the
|
||||||
# default logic
|
# default logic
|
||||||
for prop in optional_properties:
|
for prop in optional_properties:
|
||||||
@ -1157,6 +1174,20 @@ class LambdaBackend(BaseBackend):
|
|||||||
else:
|
else:
|
||||||
return None
|
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():
|
def do_validate_s3():
|
||||||
return os.environ.get("VALIDATE_LAMBDA_S3", "") in ["", "1", "true"]
|
return os.environ.get("VALIDATE_LAMBDA_S3", "") in ["", "1", "true"]
|
||||||
|
@ -141,6 +141,19 @@ class LambdaResponse(BaseResponse):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Cannot handle request")
|
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):
|
def _add_policy(self, request, full_url, headers):
|
||||||
path = request.path if hasattr(request, "path") else path_url(request.url)
|
path = request.path if hasattr(request, "path") else path_url(request.url)
|
||||||
function_name = path.split("/")[-2]
|
function_name = path.split("/")[-2]
|
||||||
@ -359,3 +372,38 @@ class LambdaResponse(BaseResponse):
|
|||||||
return 200, {}, json.dumps(resp)
|
return 200, {}, json.dumps(resp)
|
||||||
else:
|
else:
|
||||||
return 404, {}, "{}"
|
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})
|
||||||
|
@ -19,4 +19,5 @@ url_paths = {
|
|||||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/policy/?$": response.policy,
|
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/policy/?$": response.policy,
|
||||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/configuration/?$": response.configuration,
|
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/configuration/?$": response.configuration,
|
||||||
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/code/?$": response.code,
|
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/code/?$": response.code,
|
||||||
|
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_-]+)/concurrency/?$": response.function_concurrency,
|
||||||
}
|
}
|
||||||
|
@ -489,7 +489,7 @@ def test_get_function():
|
|||||||
{"test_variable": "test_value"}
|
{"test_variable": "test_value"}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test get function with
|
# Test get function with qualifier
|
||||||
result = conn.get_function(FunctionName="testFunction", Qualifier="$LATEST")
|
result = conn.get_function(FunctionName="testFunction", Qualifier="$LATEST")
|
||||||
result["Configuration"]["Version"].should.equal("$LATEST")
|
result["Configuration"]["Version"].should.equal("$LATEST")
|
||||||
result["Configuration"]["FunctionArn"].should.equal(
|
result["Configuration"]["FunctionArn"].should.equal(
|
||||||
@ -1721,6 +1721,82 @@ def test_remove_function_permission():
|
|||||||
policy["Statement"].should.equal([])
|
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):
|
def create_invalid_lambda(role):
|
||||||
conn = boto3.client("lambda", _lambda_region)
|
conn = boto3.client("lambda", _lambda_region)
|
||||||
zip_content = get_test_zip_file1()
|
zip_content = get_test_zip_file1()
|
||||||
|
@ -1777,6 +1777,7 @@ def lambda_handler(event, context):
|
|||||||
"Role": {"Fn::GetAtt": ["MyRole", "Arn"]},
|
"Role": {"Fn::GetAtt": ["MyRole", "Arn"]},
|
||||||
"Runtime": "python2.7",
|
"Runtime": "python2.7",
|
||||||
"Environment": {"Variables": {"TEST_ENV_KEY": "test-env-val"}},
|
"Environment": {"Variables": {"TEST_ENV_KEY": "test-env-val"}},
|
||||||
|
"ReservedConcurrentExecutions": 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"MyRole": {
|
"MyRole": {
|
||||||
@ -1811,6 +1812,11 @@ def lambda_handler(event, context):
|
|||||||
{"Variables": {"TEST_ENV_KEY": "test-env-val"}}
|
{"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_cloudformation
|
||||||
@mock_ec2
|
@mock_ec2
|
||||||
|
Loading…
Reference in New Issue
Block a user