Feature: AWSLambda Function URL Configs (#5420)
This commit is contained in:
parent
2e0022cb1d
commit
1ea0c74028
@ -3725,7 +3725,7 @@
|
||||
|
||||
## lambda
|
||||
<details>
|
||||
<summary>49% implemented</summary>
|
||||
<summary>55% implemented</summary>
|
||||
|
||||
- [ ] add_layer_version_permission
|
||||
- [X] add_permission
|
||||
@ -3733,7 +3733,7 @@
|
||||
- [ ] create_code_signing_config
|
||||
- [X] create_event_source_mapping
|
||||
- [X] create_function
|
||||
- [ ] create_function_url_config
|
||||
- [X] create_function_url_config
|
||||
- [X] delete_alias
|
||||
- [ ] delete_code_signing_config
|
||||
- [X] delete_event_source_mapping
|
||||
@ -3741,7 +3741,7 @@
|
||||
- [ ] delete_function_code_signing_config
|
||||
- [X] delete_function_concurrency
|
||||
- [ ] delete_function_event_invoke_config
|
||||
- [ ] delete_function_url_config
|
||||
- [X] delete_function_url_config
|
||||
- [X] delete_layer_version
|
||||
- [ ] delete_provisioned_concurrency_config
|
||||
- [ ] get_account_settings
|
||||
@ -3753,7 +3753,7 @@
|
||||
- [X] get_function_concurrency
|
||||
- [ ] get_function_configuration
|
||||
- [ ] get_function_event_invoke_config
|
||||
- [ ] get_function_url_config
|
||||
- [X] get_function_url_config
|
||||
- [X] get_layer_version
|
||||
- [ ] get_layer_version_by_arn
|
||||
- [ ] get_layer_version_policy
|
||||
@ -3789,7 +3789,7 @@
|
||||
- [X] update_function_code
|
||||
- [X] update_function_configuration
|
||||
- [ ] update_function_event_invoke_config
|
||||
- [ ] update_function_url_config
|
||||
- [X] update_function_url_config
|
||||
</details>
|
||||
|
||||
## logs
|
||||
|
@ -33,7 +33,12 @@ lambda
|
||||
- [ ] create_code_signing_config
|
||||
- [X] create_event_source_mapping
|
||||
- [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
|
||||
- [ ] delete_code_signing_config
|
||||
- [X] delete_event_source_mapping
|
||||
@ -41,7 +46,11 @@ lambda
|
||||
- [ ] delete_function_code_signing_config
|
||||
- [X] delete_function_concurrency
|
||||
- [ ] 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
|
||||
- [ ] delete_provisioned_concurrency_config
|
||||
- [ ] get_account_settings
|
||||
@ -53,7 +62,11 @@ lambda
|
||||
- [X] get_function_concurrency
|
||||
- [ ] get_function_configuration
|
||||
- [ ] 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
|
||||
- [ ] get_layer_version_by_arn
|
||||
- [ ] get_layer_version_policy
|
||||
@ -97,5 +110,9 @@ lambda
|
||||
- [X] update_function_code
|
||||
- [X] update_function_configuration
|
||||
- [ ] update_function_event_invoke_config
|
||||
- [ ] update_function_url_config
|
||||
- [X] update_function_url_config
|
||||
|
||||
The Qualifier-parameter is not yet implemented
|
||||
|
||||
|
||||
|
||||
|
@ -49,6 +49,15 @@ class UnknownFunctionException(LambdaClientError):
|
||||
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):
|
||||
code = 404
|
||||
|
||||
|
@ -35,6 +35,7 @@ from moto.s3.exceptions import MissingBucket, MissingKey
|
||||
from moto import settings
|
||||
from .exceptions import (
|
||||
CrossAccountNotAllowed,
|
||||
FunctionUrlConfigNotFound,
|
||||
InvalidRoleFormat,
|
||||
InvalidParameterValueException,
|
||||
UnknownLayerException,
|
||||
@ -404,6 +405,7 @@ class LambdaFunction(CloudFormationModel, DockerModel):
|
||||
self.logs_backend = logs_backends[account_id][self.region]
|
||||
self.environment_vars = spec.get("Environment", {}).get("Variables", {})
|
||||
self.policy = None
|
||||
self.url_config = None
|
||||
self.state = "Active"
|
||||
self.reserved_concurrency = spec.get("ReservedConcurrentExecutions", None)
|
||||
|
||||
@ -914,6 +916,48 @@ class LambdaFunction(CloudFormationModel, DockerModel):
|
||||
alias.update(description, function_version, routing_config)
|
||||
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):
|
||||
def __init__(self, spec):
|
||||
@ -1138,7 +1182,9 @@ class LambdaStorage(object):
|
||||
arn = ":".join(arn.split(":")[0:-1])
|
||||
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(
|
||||
name_or_arn
|
||||
)
|
||||
@ -1409,6 +1455,37 @@ class LambdaBackend(BaseBackend):
|
||||
fn.version = ver.version
|
||||
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):
|
||||
required = ["EventSourceArn", "FunctionName"]
|
||||
for param in required:
|
||||
|
@ -188,6 +188,19 @@ class LambdaResponse(BaseResponse):
|
||||
else:
|
||||
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):
|
||||
path = request.path if hasattr(request, "path") else path_url(request.url)
|
||||
function_name = unquote(path.split("/")[-2])
|
||||
@ -284,6 +297,26 @@ class LambdaResponse(BaseResponse):
|
||||
config = fn.get_configuration(on_create=True)
|
||||
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):
|
||||
fn = self.backend.create_event_source_mapping(self.json_body)
|
||||
config = fn.get_configuration()
|
||||
|
@ -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-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_:%-]+)/url/?$": response.function_url_config,
|
||||
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/(?P<layer_version>[\w_-]+)$": response.layers_version,
|
||||
|
@ -140,6 +140,7 @@ kms:
|
||||
lambda:
|
||||
- TestAccLambdaAlias_
|
||||
- TestAccLambdaLayerVersion_
|
||||
- TestAccLambdaFunctionURL
|
||||
meta:
|
||||
- TestAccMetaBillingServiceAccountDataSource
|
||||
mq:
|
||||
|
125
tests/test_awslambda/test_lambda_function_urls.py
Normal file
125
tests/test_awslambda/test_lambda_function_urls.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user