diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 8ba12654b..d6bd15a10 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -3725,7 +3725,7 @@
## lambda
-49% implemented
+55% implemented
- [ ] 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
## logs
diff --git a/docs/docs/services/lambda.rst b/docs/docs/services/lambda.rst
index 4ea379fcc..b51c4e10c 100644
--- a/docs/docs/services/lambda.rst
+++ b/docs/docs/services/lambda.rst
@@ -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
+
+
diff --git a/moto/awslambda/exceptions.py b/moto/awslambda/exceptions.py
index 364bccd9d..1f4808cf1 100644
--- a/moto/awslambda/exceptions.py
+++ b/moto/awslambda/exceptions.py
@@ -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
diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py
index 819a5cd44..cf3e7554b 100644
--- a/moto/awslambda/models.py
+++ b/moto/awslambda/models.py
@@ -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:
diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py
index ca355d583..e64c1c9dd 100644
--- a/moto/awslambda/responses.py
+++ b/moto/awslambda/responses.py
@@ -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()
diff --git a/moto/awslambda/urls.py b/moto/awslambda/urls.py
index a567197ed..348014251 100644
--- a/moto/awslambda/urls.py
+++ b/moto/awslambda/urls.py
@@ -22,6 +22,7 @@ url_paths = {
r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/code/?$": response.code,
r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/code-signing-config$": response.code_signing_config,
r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/concurrency/?$": response.function_concurrency,
+ r"{0}/(?P[^/]+)/functions/(?P[\w_:%-]+)/url/?$": response.function_url_config,
r"{0}/(?P[^/]+)/layers/?$": response.list_layers,
r"{0}/(?P[^/]+)/layers/(?P[\w_-]+)/versions/?$": response.layers_versions,
r"{0}/(?P[^/]+)/layers/(?P[\w_-]+)/versions/(?P[\w_-]+)$": response.layers_version,
diff --git a/tests/terraformtests/terraform-tests.success.txt b/tests/terraformtests/terraform-tests.success.txt
index 3a577cd5d..87f270543 100644
--- a/tests/terraformtests/terraform-tests.success.txt
+++ b/tests/terraformtests/terraform-tests.success.txt
@@ -140,6 +140,7 @@ kms:
lambda:
- TestAccLambdaAlias_
- TestAccLambdaLayerVersion_
+ - TestAccLambdaFunctionURL
meta:
- TestAccMetaBillingServiceAccountDataSource
mq:
diff --git a/tests/test_awslambda/test_lambda_function_urls.py b/tests/test_awslambda/test_lambda_function_urls.py
new file mode 100644
index 000000000..3353b3018
--- /dev/null
+++ b/tests/test_awslambda/test_lambda_function_urls.py
@@ -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)