Skip to content

Commit a3c2cae

Browse files
mndevecijonifevalerena
authored
feat: Lambda URLs (#2229)
Co-authored-by: jonife <[email protected]> Co-authored-by: Renato Valenzuela <[email protected]>
1 parent 9bac5ee commit a3c2cae

File tree

35 files changed

+1679
-5
lines changed

35 files changed

+1679
-5
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"LogicalResourceId": "MyLambdaFunction",
4+
"ResourceType": "AWS::Lambda::Function"
5+
},
6+
{
7+
"LogicalResourceId": "MyLambdaFunctionUrl",
8+
"ResourceType": "AWS::Lambda::Url"
9+
},
10+
{
11+
"LogicalResourceId": "MyLambdaFunctionRole",
12+
"ResourceType": "AWS::IAM::Role"
13+
}
14+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[
2+
{
3+
"LogicalResourceId": "MyLambdaFunction",
4+
"ResourceType": "AWS::Lambda::Function"
5+
},
6+
{
7+
"LogicalResourceId": "MyLambdaFunctionUrl",
8+
"ResourceType": "AWS::Lambda::Url"
9+
},
10+
{
11+
"LogicalResourceId": "MyLambdaFunctionRole",
12+
"ResourceType": "AWS::IAM::Role"
13+
},
14+
{
15+
"LogicalResourceId": "MyLambdaFunctionAliaslive",
16+
"ResourceType": "AWS::Lambda::Alias"
17+
},
18+
{
19+
"LogicalResourceId": "MyLambdaFunctionVersion",
20+
"ResourceType": "AWS::Lambda::Version"
21+
}
22+
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Resources:
2+
MyLambdaFunction:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
Handler: index.handler
6+
Runtime: nodejs12.x
7+
CodeUri: ${codeuri}
8+
MemorySize: 128
9+
FunctionUrlConfig:
10+
AuthorizationType: NONE
11+
Cors:
12+
AllowOrigins:
13+
- "https://foo.com"
14+
AllowMethods:
15+
- "POST"
16+
AllowCredentials: true
17+
AllowHeaders:
18+
- "x-Custom-Header"
19+
ExposeHeaders:
20+
- "x-amzn-header"
21+
MaxAge: 10
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Resources:
2+
MyLambdaFunction:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
Handler: index.handler
6+
Runtime: nodejs12.x
7+
CodeUri: ${codeuri}
8+
MemorySize: 128
9+
AutoPublishAlias: live
10+
FunctionUrlConfig:
11+
AuthorizationType: NONE
12+
Cors:
13+
AllowOrigins:
14+
- "https://foo.com"
15+
AllowMethods:
16+
- "POST"
17+
AllowCredentials: true
18+
AllowHeaders:
19+
- "x-Custom-Header"
20+
ExposeHeaders:
21+
- "x-amzn-header"
22+
MaxAge: 10

integration/single/test_basic_function.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,39 @@ def test_basic_function_with_architecture(self, file_name, architecture):
6666

6767
self.assertEqual(function_architecture, architecture)
6868

69+
@parameterized.expand(
70+
[
71+
("single/basic_function_with_function_url_config", None),
72+
("single/basic_function_with_function_url_with_autopuplishalias", "live"),
73+
]
74+
)
75+
@skipIf(current_region_does_not_support(["Url"]), "Url is not supported in this testing region")
76+
def test_basic_function_with_url_config(self, file_name, qualifier):
77+
"""
78+
Creates a basic lambda function with Function Url enabled
79+
"""
80+
self.create_and_verify_stack(file_name)
81+
82+
lambda_client = self.client_provider.lambda_client
83+
84+
function_name = self.get_physical_id_by_type("AWS::Lambda::Function")
85+
function_url_config = (
86+
lambda_client.get_function_url_config(FunctionName=function_name, Qualifier=qualifier)
87+
if qualifier
88+
else lambda_client.get_function_url_config(FunctionName=function_name)
89+
)
90+
cors_config = {
91+
"AllowOrigins": ["https://foo.com"],
92+
"AllowMethods": ["POST"],
93+
"AllowCredentials": True,
94+
"AllowHeaders": ["x-Custom-Header"],
95+
"ExposeHeaders": ["x-amzn-header"],
96+
"MaxAge": 10,
97+
}
98+
99+
self.assertEqual(function_url_config["AuthorizationType"], "NONE")
100+
self.assertEqual(function_url_config["Cors"], cors_config)
101+
69102
def test_function_with_deployment_preference_alarms_intrinsic_if(self):
70103
self.create_and_verify_stack("single/function_with_deployment_preference_alarms_intrinsic_if")
71104

samtranslator/model/lambda_.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,12 @@ class LambdaLayerVersion(Resource):
121121
}
122122

123123
runtime_attrs = {"name": lambda self: ref(self.logical_id), "arn": lambda self: fnGetAtt(self.logical_id, "Arn")}
124+
125+
126+
class LambdaUrl(Resource):
127+
resource_type = "AWS::Lambda::Url"
128+
property_types = {
129+
"TargetFunctionArn": PropertyType(True, one_of(is_str(), is_type(dict))),
130+
"AuthorizationType": PropertyType(True, is_str()),
131+
"Cors": PropertyType(False, is_type(dict)),
132+
}

samtranslator/model/sam_resources.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
LambdaAlias,
3535
LambdaLayerVersion,
3636
LambdaEventInvokeConfig,
37+
LambdaUrl,
3738
)
3839
from samtranslator.model.types import dict_of, is_str, is_type, list_of, one_of, any_type
3940
from samtranslator.translator import logical_id_generator
@@ -93,6 +94,7 @@ class SamFunction(SamResourceMacro):
9394
"ImageConfig": PropertyType(False, is_type(dict)),
9495
"CodeSigningConfigArn": PropertyType(False, is_str()),
9596
"Architectures": PropertyType(False, list_of(one_of(is_str(), is_type(dict)))),
97+
"FunctionUrlConfig": PropertyType(False, is_type(dict)),
9698
}
9799
event_resolver = ResourceTypeResolver(
98100
samtranslator.model.eventsources,
@@ -169,6 +171,10 @@ def to_cloudformation(self, **kwargs):
169171
resources.append(lambda_version)
170172
resources.append(lambda_alias)
171173

174+
if self.FunctionUrlConfig:
175+
lambda_url = self._construct_lambda_url(lambda_function, lambda_alias)
176+
resources.append(lambda_url)
177+
172178
if self.DeploymentPreference:
173179
self._validate_deployment_preference_and_add_update_policy(
174180
kwargs.get("deployment_preference_collection", None),
@@ -843,6 +849,84 @@ def _validate_deployment_preference_and_add_update_policy(
843849
"UpdatePolicy", deployment_preference_collection.update_policy(self.logical_id).to_dict()
844850
)
845851

852+
def _construct_lambda_url(self, lambda_function, lambda_alias):
853+
"""
854+
This method is used to construct a lambda url resource
855+
856+
Parameters
857+
----------
858+
lambda_function : LambdaFunction
859+
Lambda Function resource
860+
lambda_alias : LambdaAlias
861+
Lambda Alias resource
862+
863+
Returns
864+
-------
865+
LambdaUrl
866+
Lambda Url resource
867+
"""
868+
self._validate_function_url_params(lambda_function)
869+
870+
logical_id = "{id}Url".format(id=lambda_function.logical_id)
871+
lambda_url = LambdaUrl(logical_id=logical_id)
872+
873+
cors = self.FunctionUrlConfig.get("Cors")
874+
if cors:
875+
lambda_url.Cors = cors
876+
lambda_url.AuthorizationType = self.FunctionUrlConfig.get("AuthorizationType")
877+
lambda_url.TargetFunctionArn = (
878+
lambda_alias.get_runtime_attr("arn") if lambda_alias else lambda_function.get_runtime_attr("name")
879+
)
880+
return lambda_url
881+
882+
def _validate_function_url_params(self, lambda_function):
883+
"""
884+
Validate parameters provided to configure Lambda Urls
885+
"""
886+
self._validate_url_auth_type(lambda_function)
887+
self._validate_cors_config_parameter(lambda_function)
888+
889+
def _validate_url_auth_type(self, lambda_function):
890+
if is_intrinsic(self.FunctionUrlConfig):
891+
return
892+
893+
if not self.FunctionUrlConfig.get("AuthorizationType"):
894+
raise InvalidResourceException(
895+
lambda_function.logical_id,
896+
"AuthorizationType is required to configure function property `FunctionUrlConfig`. Please provide either an IAM role name or NONE.",
897+
)
898+
899+
def _validate_cors_config_parameter(self, lambda_function):
900+
if is_intrinsic(self.FunctionUrlConfig):
901+
return
902+
903+
cors_property_data_type = {
904+
"AllowOrigins": list,
905+
"AllowMethods": list,
906+
"AllowCredentials": bool,
907+
"AllowHeaders": list,
908+
"ExposeHeaders": list,
909+
"MaxAge": int,
910+
}
911+
912+
cors = self.FunctionUrlConfig.get("Cors")
913+
914+
if not cors or is_intrinsic(cors):
915+
return
916+
917+
for prop_name, prop_value in cors.items():
918+
if prop_name not in cors_property_data_type:
919+
raise InvalidResourceException(
920+
lambda_function.logical_id,
921+
"{} is not a valid property for configuring Cors.".format(prop_name),
922+
)
923+
prop_type = cors_property_data_type.get(prop_name)
924+
if not is_intrinsic(prop_value) and not isinstance(prop_value, prop_type):
925+
raise InvalidResourceException(
926+
lambda_function.logical_id,
927+
"{} must be of type {}.".format(prop_name, str(prop_type).split("'")[1]),
928+
)
929+
846930

847931
class SamApi(SamResourceMacro):
848932
"""SAM rest API macro."""

samtranslator/plugins/globals/globals.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class Globals(object):
4343
"FileSystemConfigs",
4444
"CodeSigningConfigArn",
4545
"Architectures",
46+
"FunctionUrlConfig",
4647
],
4748
# Everything except
4849
# DefinitionBody: because its hard to reason about merge of Swagger dictionaries
@@ -80,6 +81,8 @@ class Globals(object):
8081
],
8182
SamResourceType.SimpleTable.value: ["SSESpecification"],
8283
}
84+
# unreleased_properties *must be* part of supported_properties too
85+
unreleased_properties = {}
8386

8487
def __init__(self, template):
8588
"""
@@ -195,14 +198,17 @@ def _parse(self, globals_dict):
195198
if not isinstance(properties, dict):
196199
raise InvalidGlobalsSectionException(self._KEYWORD, "Value of ${section} must be a dictionary")
197200

201+
supported = self.supported_properties[resource_type]
202+
supported_displayed = [
203+
prop for prop in supported if prop not in self.unreleased_properties.get(resource_type, [])
204+
]
198205
for key, value in properties.items():
199-
supported = self.supported_properties[resource_type]
200206
if key not in supported:
201207
raise InvalidGlobalsSectionException(
202208
self._KEYWORD,
203209
"'{key}' is not a supported property of '{section}'. "
204210
"Must be one of the following values - {supported}".format(
205-
key=key, section=section_name, supported=supported
211+
key=key, section=section_name, supported=supported_displayed
206212
),
207213
)
208214

0 commit comments

Comments
 (0)