Feature: AWSLambda Function URL Configs (#5420)

This commit is contained in:
Bert Blommers 2022-08-24 21:19:17 +00:00 committed by GitHub
parent 2e0022cb1d
commit 1ea0c74028
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 273 additions and 10 deletions

View File

@ -3725,7 +3725,7 @@
## lambda ## lambda
<details> <details>
<summary>49% implemented</summary> <summary>55% implemented</summary>
- [ ] add_layer_version_permission - [ ] add_layer_version_permission
- [X] add_permission - [X] add_permission
@ -3733,7 +3733,7 @@
- [ ] create_code_signing_config - [ ] create_code_signing_config
- [X] create_event_source_mapping - [X] create_event_source_mapping
- [X] create_function - [X] create_function
- [ ] create_function_url_config - [X] create_function_url_config
- [X] delete_alias - [X] delete_alias
- [ ] delete_code_signing_config - [ ] delete_code_signing_config
- [X] delete_event_source_mapping - [X] delete_event_source_mapping
@ -3741,7 +3741,7 @@
- [ ] delete_function_code_signing_config - [ ] delete_function_code_signing_config
- [X] delete_function_concurrency - [X] delete_function_concurrency
- [ ] delete_function_event_invoke_config - [ ] delete_function_event_invoke_config
- [ ] delete_function_url_config - [X] delete_function_url_config
- [X] delete_layer_version - [X] delete_layer_version
- [ ] delete_provisioned_concurrency_config - [ ] delete_provisioned_concurrency_config
- [ ] get_account_settings - [ ] get_account_settings
@ -3753,7 +3753,7 @@
- [X] get_function_concurrency - [X] get_function_concurrency
- [ ] get_function_configuration - [ ] get_function_configuration
- [ ] get_function_event_invoke_config - [ ] get_function_event_invoke_config
- [ ] get_function_url_config - [X] get_function_url_config
- [X] get_layer_version - [X] get_layer_version
- [ ] get_layer_version_by_arn - [ ] get_layer_version_by_arn
- [ ] get_layer_version_policy - [ ] get_layer_version_policy
@ -3789,7 +3789,7 @@
- [X] update_function_code - [X] update_function_code
- [X] update_function_configuration - [X] update_function_configuration
- [ ] update_function_event_invoke_config - [ ] update_function_event_invoke_config
- [ ] update_function_url_config - [X] update_function_url_config
</details> </details>
## logs ## logs

View File

@ -33,7 +33,12 @@ lambda
- [ ] create_code_signing_config - [ ] create_code_signing_config
- [X] create_event_source_mapping - [X] create_event_source_mapping
- [X] create_function - [X] create_function
- [ ] create_function_url_config - [X] create_function_url_config
The Qualifier-parameter is not yet implemented.
Function URLs are not yet mocked, so invoking them will fail
- [X] delete_alias - [X] delete_alias
- [ ] delete_code_signing_config - [ ] delete_code_signing_config
- [X] delete_event_source_mapping - [X] delete_event_source_mapping
@ -41,7 +46,11 @@ lambda
- [ ] delete_function_code_signing_config - [ ] delete_function_code_signing_config
- [X] delete_function_concurrency - [X] delete_function_concurrency
- [ ] delete_function_event_invoke_config - [ ] delete_function_event_invoke_config
- [ ] delete_function_url_config - [X] delete_function_url_config
The Qualifier-parameter is not yet implemented
- [X] delete_layer_version - [X] delete_layer_version
- [ ] delete_provisioned_concurrency_config - [ ] delete_provisioned_concurrency_config
- [ ] get_account_settings - [ ] get_account_settings
@ -53,7 +62,11 @@ lambda
- [X] get_function_concurrency - [X] get_function_concurrency
- [ ] get_function_configuration - [ ] get_function_configuration
- [ ] get_function_event_invoke_config - [ ] get_function_event_invoke_config
- [ ] get_function_url_config - [X] get_function_url_config
The Qualifier-parameter is not yet implemented
- [X] get_layer_version - [X] get_layer_version
- [ ] get_layer_version_by_arn - [ ] get_layer_version_by_arn
- [ ] get_layer_version_policy - [ ] get_layer_version_policy
@ -97,5 +110,9 @@ lambda
- [X] update_function_code - [X] update_function_code
- [X] update_function_configuration - [X] update_function_configuration
- [ ] update_function_event_invoke_config - [ ] update_function_event_invoke_config
- [ ] update_function_url_config - [X] update_function_url_config
The Qualifier-parameter is not yet implemented

View File

@ -49,6 +49,15 @@ class UnknownFunctionException(LambdaClientError):
super().__init__("ResourceNotFoundException", f"Function not found: {arn}") super().__init__("ResourceNotFoundException", f"Function not found: {arn}")
class FunctionUrlConfigNotFound(LambdaClientError):
code = 404
def __init__(self):
super().__init__(
"ResourceNotFoundException", "The resource you requested does not exist."
)
class UnknownLayerException(LambdaClientError): class UnknownLayerException(LambdaClientError):
code = 404 code = 404

View File

@ -35,6 +35,7 @@ from moto.s3.exceptions import MissingBucket, MissingKey
from moto import settings from moto import settings
from .exceptions import ( from .exceptions import (
CrossAccountNotAllowed, CrossAccountNotAllowed,
FunctionUrlConfigNotFound,
InvalidRoleFormat, InvalidRoleFormat,
InvalidParameterValueException, InvalidParameterValueException,
UnknownLayerException, UnknownLayerException,
@ -404,6 +405,7 @@ class LambdaFunction(CloudFormationModel, DockerModel):
self.logs_backend = logs_backends[account_id][self.region] self.logs_backend = logs_backends[account_id][self.region]
self.environment_vars = spec.get("Environment", {}).get("Variables", {}) self.environment_vars = spec.get("Environment", {}).get("Variables", {})
self.policy = None self.policy = None
self.url_config = None
self.state = "Active" self.state = "Active"
self.reserved_concurrency = spec.get("ReservedConcurrentExecutions", None) self.reserved_concurrency = spec.get("ReservedConcurrentExecutions", None)
@ -914,6 +916,48 @@ class LambdaFunction(CloudFormationModel, DockerModel):
alias.update(description, function_version, routing_config) alias.update(description, function_version, routing_config)
return alias return alias
def create_url_config(self, config):
self.url_config = FunctionUrlConfig(function=self, config=config)
return self.url_config
def delete_url_config(self):
self.url_config = None
def get_url_config(self):
if not self.url_config:
raise FunctionUrlConfigNotFound()
return self.url_config
def update_url_config(self, config):
self.url_config.update(config)
return self.url_config
class FunctionUrlConfig:
def __init__(self, function: LambdaFunction, config):
self.function = function
self.config = config
self.url = f"https://{uuid.uuid4().hex}.lambda-url.{function.region}.on.aws"
self.created = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
self.last_modified = self.created
def to_dict(self):
return {
"FunctionUrl": self.url,
"FunctionArn": self.function.function_arn,
"AuthType": self.config.get("AuthType"),
"Cors": self.config.get("Cors"),
"CreationTime": self.created,
"LastModifiedTime": self.last_modified,
}
def update(self, new_config):
if new_config.get("Cors"):
self.config["Cors"] = new_config["Cors"]
if new_config.get("AuthType"):
self.config["AuthType"] = new_config["AuthType"]
self.last_modified = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
class EventSourceMapping(CloudFormationModel): class EventSourceMapping(CloudFormationModel):
def __init__(self, spec): def __init__(self, spec):
@ -1138,7 +1182,9 @@ class LambdaStorage(object):
arn = ":".join(arn.split(":")[0:-1]) arn = ":".join(arn.split(":")[0:-1])
return self._arns.get(arn, None) return self._arns.get(arn, None)
def get_function_by_name_or_arn(self, name_or_arn, qualifier=None): def get_function_by_name_or_arn(
self, name_or_arn, qualifier=None
) -> LambdaFunction:
fn = self.get_function_by_name(name_or_arn, qualifier) or self.get_arn( fn = self.get_function_by_name(name_or_arn, qualifier) or self.get_arn(
name_or_arn name_or_arn
) )
@ -1409,6 +1455,37 @@ class LambdaBackend(BaseBackend):
fn.version = ver.version fn.version = ver.version
return fn return fn
def create_function_url_config(self, name_or_arn, config):
"""
The Qualifier-parameter is not yet implemented.
Function URLs are not yet mocked, so invoking them will fail
"""
function = self._lambdas.get_function_by_name_or_arn(name_or_arn)
return function.create_url_config(config)
def delete_function_url_config(self, name_or_arn):
"""
The Qualifier-parameter is not yet implemented
"""
function = self._lambdas.get_function_by_name_or_arn(name_or_arn)
function.delete_url_config()
def get_function_url_config(self, name_or_arn):
"""
The Qualifier-parameter is not yet implemented
"""
function = self._lambdas.get_function_by_name_or_arn(name_or_arn)
if not function:
raise UnknownFunctionException(arn=name_or_arn)
return function.get_url_config()
def update_function_url_config(self, name_or_arn, config):
"""
The Qualifier-parameter is not yet implemented
"""
function = self._lambdas.get_function_by_name_or_arn(name_or_arn)
return function.update_url_config(config)
def create_event_source_mapping(self, spec): def create_event_source_mapping(self, spec):
required = ["EventSourceArn", "FunctionName"] required = ["EventSourceArn", "FunctionName"]
for param in required: for param in required:

View File

@ -188,6 +188,19 @@ class LambdaResponse(BaseResponse):
else: else:
raise ValueError("Cannot handle request") raise ValueError("Cannot handle request")
def function_url_config(self, request, full_url, headers):
http_method = request.method
self.setup_class(request, full_url, headers)
if http_method == "DELETE":
return self._delete_function_url_config()
elif http_method == "GET":
return self._get_function_url_config()
elif http_method == "POST":
return self._create_function_url_config()
elif http_method == "PUT":
return self._update_function_url_config()
def _add_policy(self, request): def _add_policy(self, request):
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 = unquote(path.split("/")[-2]) function_name = unquote(path.split("/")[-2])
@ -284,6 +297,26 @@ class LambdaResponse(BaseResponse):
config = fn.get_configuration(on_create=True) config = fn.get_configuration(on_create=True)
return 201, {}, json.dumps(config) return 201, {}, json.dumps(config)
def _create_function_url_config(self):
function_name = unquote(self.path.split("/")[-2])
config = self.backend.create_function_url_config(function_name, self.json_body)
return 201, {}, json.dumps(config.to_dict())
def _delete_function_url_config(self):
function_name = unquote(self.path.split("/")[-2])
self.backend.delete_function_url_config(function_name)
return 204, {}, "{}"
def _get_function_url_config(self):
function_name = unquote(self.path.split("/")[-2])
config = self.backend.get_function_url_config(function_name)
return 201, {}, json.dumps(config.to_dict())
def _update_function_url_config(self):
function_name = unquote(self.path.split("/")[-2])
config = self.backend.update_function_url_config(function_name, self.json_body)
return 200, {}, json.dumps(config.to_dict())
def _create_event_source_mapping(self): def _create_event_source_mapping(self):
fn = self.backend.create_event_source_mapping(self.json_body) fn = self.backend.create_event_source_mapping(self.json_body)
config = fn.get_configuration() config = fn.get_configuration()

View File

@ -22,6 +22,7 @@ url_paths = {
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_:%-]+)/code-signing-config$": response.code_signing_config, r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_:%-]+)/code-signing-config$": response.code_signing_config,
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_:%-]+)/concurrency/?$": response.function_concurrency, r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_:%-]+)/concurrency/?$": response.function_concurrency,
r"{0}/(?P<api_version>[^/]+)/functions/(?P<function_name>[\w_:%-]+)/url/?$": response.function_url_config,
r"{0}/(?P<api_version>[^/]+)/layers/?$": response.list_layers, r"{0}/(?P<api_version>[^/]+)/layers/?$": response.list_layers,
r"{0}/(?P<api_version>[^/]+)/layers/(?P<layer_name>[\w_-]+)/versions/?$": response.layers_versions, r"{0}/(?P<api_version>[^/]+)/layers/(?P<layer_name>[\w_-]+)/versions/?$": response.layers_versions,
r"{0}/(?P<api_version>[^/]+)/layers/(?P<layer_name>[\w_-]+)/versions/(?P<layer_version>[\w_-]+)$": response.layers_version, r"{0}/(?P<api_version>[^/]+)/layers/(?P<layer_name>[\w_-]+)/versions/(?P<layer_version>[\w_-]+)$": response.layers_version,

View File

@ -140,6 +140,7 @@ kms:
lambda: lambda:
- TestAccLambdaAlias_ - TestAccLambdaAlias_
- TestAccLambdaLayerVersion_ - TestAccLambdaLayerVersion_
- TestAccLambdaFunctionURL
meta: meta:
- TestAccMetaBillingServiceAccountDataSource - TestAccMetaBillingServiceAccountDataSource
mq: mq:

View File

@ -0,0 +1,125 @@
import boto3
import pytest
from botocore.exceptions import ClientError
from moto import mock_lambda
from uuid import uuid4
from .utilities import get_test_zip_file1, get_role_name
@mock_lambda
@pytest.mark.parametrize("key", ["FunctionName", "FunctionArn"])
def test_create_function_url_config(key):
client = boto3.client("lambda", "us-east-2")
function_name = str(uuid4())[0:6]
fxn = client.create_function(
FunctionName=function_name,
Runtime="python3.7",
Role=get_role_name(),
Handler="lambda_function.lambda_handler",
Code={"ZipFile": get_test_zip_file1()},
)
name_or_arn = fxn[key]
resp = client.create_function_url_config(
AuthType="AWS_IAM", FunctionName=name_or_arn
)
resp.should.have.key("FunctionArn").equals(fxn["FunctionArn"])
resp.should.have.key("AuthType").equals("AWS_IAM")
resp.should.have.key("FunctionUrl")
resp = client.get_function_url_config(FunctionName=name_or_arn)
resp.should.have.key("FunctionArn").equals(fxn["FunctionArn"])
resp.should.have.key("AuthType").equals("AWS_IAM")
resp.should.have.key("FunctionUrl")
@mock_lambda
def test_create_function_url_config_with_cors():
client = boto3.client("lambda", "us-east-2")
function_name = str(uuid4())[0:6]
fxn = client.create_function(
FunctionName=function_name,
Runtime="python3.7",
Role=get_role_name(),
Handler="lambda_function.lambda_handler",
Code={"ZipFile": get_test_zip_file1()},
)
name_or_arn = fxn["FunctionName"]
resp = client.create_function_url_config(
AuthType="AWS_IAM",
FunctionName=name_or_arn,
Cors={
"AllowCredentials": True,
"AllowHeaders": ["date", "keep-alive"],
"AllowMethods": ["*"],
"AllowOrigins": ["*"],
"ExposeHeaders": ["date", "keep-alive"],
"MaxAge": 86400,
},
)
resp.should.have.key("Cors").equals(
{
"AllowCredentials": True,
"AllowHeaders": ["date", "keep-alive"],
"AllowMethods": ["*"],
"AllowOrigins": ["*"],
"ExposeHeaders": ["date", "keep-alive"],
"MaxAge": 86400,
}
)
@mock_lambda
def test_update_function_url_config_with_cors():
client = boto3.client("lambda", "us-east-2")
function_name = str(uuid4())[0:6]
fxn = client.create_function(
FunctionName=function_name,
Runtime="python3.7",
Role=get_role_name(),
Handler="lambda_function.lambda_handler",
Code={"ZipFile": get_test_zip_file1()},
)
name_or_arn = fxn["FunctionName"]
resp = client.create_function_url_config(
AuthType="AWS_IAM",
FunctionName=name_or_arn,
Cors={
"AllowCredentials": True,
"AllowHeaders": ["date", "keep-alive"],
"AllowMethods": ["*"],
"AllowOrigins": ["*"],
"ExposeHeaders": ["date", "keep-alive"],
"MaxAge": 86400,
},
)
resp = client.update_function_url_config(
FunctionName=name_or_arn, AuthType="NONE", Cors={"AllowCredentials": False}
)
resp.should.have.key("Cors").equals({"AllowCredentials": False})
@mock_lambda
@pytest.mark.parametrize("key", ["FunctionName", "FunctionArn"])
def test_delete_function_url_config(key):
client = boto3.client("lambda", "us-east-2")
function_name = str(uuid4())[0:6]
fxn = client.create_function(
FunctionName=function_name,
Runtime="python3.7",
Role=get_role_name(),
Handler="lambda_function.lambda_handler",
Code={"ZipFile": get_test_zip_file1()},
)
name_or_arn = fxn[key]
client.create_function_url_config(AuthType="AWS_IAM", FunctionName=name_or_arn)
client.delete_function_url_config(FunctionName=name_or_arn)
# It no longer exists
with pytest.raises(ClientError):
client.get_function_url_config(FunctionName=name_or_arn)