Skip to content

Commit 2833427

Browse files
jonifemgrandis
authored andcommitted
feat: Support for Lambda URL (#86)
1 parent 1f3a03f commit 2833427

File tree

29 files changed

+1544
-3
lines changed

29 files changed

+1544
-3
lines changed

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+
"CorsConfig": 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("CorsConfig")
874+
if cors:
875+
lambda_url.CorsConfig = 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("CorsConfig")
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 CorsConfig.".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: 1 addition & 0 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

tests/model/test_sam_resources.py

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from samtranslator.intrinsics.resolver import IntrinsicsResolver
66
from samtranslator.model import InvalidResourceException
77
from samtranslator.model.apigatewayv2 import ApiGatewayV2HttpApi
8-
from samtranslator.model.lambda_ import LambdaFunction, LambdaLayerVersion, LambdaVersion
8+
from samtranslator.model.lambda_ import LambdaFunction, LambdaLayerVersion, LambdaVersion, LambdaUrl
99
from samtranslator.model.apigateway import ApiGatewayDeployment, ApiGatewayRestApi
1010
from samtranslator.model.apigateway import ApiGatewayStage
1111
from samtranslator.model.iam import IAMRole
@@ -461,3 +461,162 @@ def test_invalid_compatible_architectures(self):
461461
layer.CompatibleArchitectures = architecturea
462462
with pytest.raises(InvalidResourceException):
463463
layer.to_cloudformation(**self.kwargs)
464+
465+
466+
class TestFunctionUrlConfig(TestCase):
467+
kwargs = {
468+
"intrinsics_resolver": IntrinsicsResolver({}),
469+
"event_resources": [],
470+
"managed_policy_map": {"foo": "bar"},
471+
}
472+
473+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
474+
def test_with_function_url_config_with_no_authorization_type(self):
475+
function = SamFunction("foo")
476+
function.CodeUri = "s3://foobar/foo.zip"
477+
function.Runtime = "foo"
478+
function.Handler = "bar"
479+
function.FunctionUrlConfig = {"CorsConfig": {"AllowOrigins": ["example1.com"]}}
480+
with pytest.raises(InvalidResourceException) as e:
481+
function.to_cloudformation(**self.kwargs)
482+
self.assertEqual(
483+
str(e.value.message),
484+
"Resource with id [foo] is invalid. AuthorizationType is required to configure"
485+
+ " function property `FunctionUrlConfig`. Please provide either an IAM role name or NONE.",
486+
)
487+
488+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
489+
def test_with_function_url_config_with_no_cors_config(self):
490+
function = SamFunction("foo")
491+
function.CodeUri = "s3://foobar/foo.zip"
492+
function.Runtime = "foo"
493+
function.Handler = "bar"
494+
function.FunctionUrlConfig = {"AuthorizationType": "sample_IAM_role"}
495+
cfnResources = function.to_cloudformation(**self.kwargs)
496+
generatedUrlList = [x for x in cfnResources if isinstance(x, LambdaUrl)]
497+
self.assertEqual(generatedUrlList.__len__(), 1)
498+
self.assertEqual(generatedUrlList[0].AuthorizationType, "sample_IAM_role")
499+
500+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
501+
def test_validate_function_url_config_properties_with_intrinsic(self):
502+
function = SamFunction("foo")
503+
function.CodeUri = "s3://foobar/foo.zip"
504+
function.Runtime = "foo"
505+
function.Handler = "bar"
506+
function.FunctionUrlConfig = {"AuthorizationType": {"Ref": "MyIAMRef"}, "CorsConfig": {"Ref": "MyCorConfigRef"}}
507+
508+
cfnResources = function.to_cloudformation(**self.kwargs)
509+
generatedUrlList = [x for x in cfnResources if isinstance(x, LambdaUrl)]
510+
self.assertEqual(generatedUrlList.__len__(), 1)
511+
generatedUrlList = [x for x in cfnResources if isinstance(x, LambdaUrl)]
512+
self.assertEqual(generatedUrlList.__len__(), 1)
513+
self.assertEqual(generatedUrlList[0].AuthorizationType, {"Ref": "MyIAMRef"})
514+
self.assertEqual(generatedUrlList[0].CorsConfig, {"Ref": "MyCorConfigRef"})
515+
516+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
517+
def test_with_valid_function_url_config(self):
518+
corsConfig = {
519+
"AllowOrigins": ["example1.com", "example2.com", "example2.com"],
520+
"AllowMethods": ["GET"],
521+
"AllowCredentials": True,
522+
"AllowHeaders": ["X-Custom-Header"],
523+
"ExposeHeaders": ["x-amzn-header"],
524+
"MaxAge": 10,
525+
}
526+
function = SamFunction("foo")
527+
function.CodeUri = "s3://foobar/foo.zip"
528+
function.Runtime = "foo"
529+
function.Handler = "bar"
530+
function.FunctionUrlConfig = {"AuthorizationType": "NONE", "CorsConfig": corsConfig}
531+
532+
cfnResources = function.to_cloudformation(**self.kwargs)
533+
generatedUrlList = [x for x in cfnResources if isinstance(x, LambdaUrl)]
534+
self.assertEqual(generatedUrlList.__len__(), 1)
535+
self.assertEqual(generatedUrlList[0].AuthorizationType, "NONE")
536+
self.assertEqual(generatedUrlList[0].CorsConfig, corsConfig)
537+
538+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
539+
def test_with_valid_function_url_config_with_Intrinsics(self):
540+
function = SamFunction("foo")
541+
function.CodeUri = "s3://foobar/foo.zip"
542+
function.Runtime = "foo"
543+
function.Handler = "bar"
544+
function.FunctionUrlConfig = {"Ref": "MyFunctionUrlConfig"}
545+
546+
cfnResources = function.to_cloudformation(**self.kwargs)
547+
generatedUrlList = [x for x in cfnResources if isinstance(x, LambdaUrl)]
548+
self.assertEqual(generatedUrlList.__len__(), 1)
549+
550+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
551+
def test_with_function_url_config_with_invalid_cors_parameter(self):
552+
function = SamFunction("foo")
553+
function.CodeUri = "s3://foobar/foo.zip"
554+
function.Runtime = "foo"
555+
function.Handler = "bar"
556+
function.FunctionUrlConfig = {"AuthorizationType": "NONE", "CorsConfig": {"AllowOrigin": ["example1.com"]}}
557+
with pytest.raises(InvalidResourceException) as e:
558+
function.to_cloudformation(**self.kwargs)
559+
self.assertEqual(
560+
str(e.value.message),
561+
"Resource with id [foo] is invalid. AllowOrigin is not a valid property for configuring CorsConfig.",
562+
)
563+
564+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
565+
def test_with_function_url_config_with_invalid_cors_parameter_data_type(self):
566+
function = SamFunction("foo")
567+
function.CodeUri = "s3://foobar/foo.zip"
568+
function.Runtime = "foo"
569+
function.Handler = "bar"
570+
function.FunctionUrlConfig = {"AuthorizationType": "NONE", "CorsConfig": {"AllowOrigins": "example1.com"}}
571+
with pytest.raises(InvalidResourceException) as e:
572+
function.to_cloudformation(**self.kwargs)
573+
self.assertEqual(
574+
str(e.value.message),
575+
"Resource with id [foo] is invalid. AllowOrigins must be of type list.",
576+
)
577+
578+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
579+
def test_with_valid_function_url_config_without_auto_publish_alias(self):
580+
function = SamFunction("foo")
581+
function.CodeUri = "s3://foobar/foo.zip"
582+
function.Runtime = "foo"
583+
function.Handler = "bar"
584+
function.FunctionUrlConfig = {"AuthorizationType": "NONE", "CorsConfig": {"AllowOrigins": ["example1.com"]}}
585+
586+
cfnResources = function.to_cloudformation(**self.kwargs)
587+
generatedUrlList = [x for x in cfnResources if isinstance(x, LambdaUrl)]
588+
self.assertEqual(generatedUrlList.__len__(), 1)
589+
expected_url_logicalid = {"Ref": "foo"}
590+
self.assertEqual(generatedUrlList[0].TargetFunctionArn, expected_url_logicalid)
591+
592+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
593+
def test_with_valid_function_url_config_with_auto_publish_alias(self):
594+
function = SamFunction("foo")
595+
function.CodeUri = "s3://foobar/foo.zip"
596+
function.Runtime = "foo"
597+
function.Handler = "bar"
598+
function.AutoPublishAlias = "live"
599+
function.FunctionUrlConfig = {"AuthorizationType": "NONE", "CorsConfig": {"AllowOrigins": ["example1.com"]}}
600+
601+
cfnResources = function.to_cloudformation(**self.kwargs)
602+
generatedUrlList = [x for x in cfnResources if isinstance(x, LambdaUrl)]
603+
self.assertEqual(generatedUrlList.__len__(), 1)
604+
expected_url_logicalid = {"Ref": "fooAliaslive"}
605+
self.assertEqual(generatedUrlList[0].TargetFunctionArn, expected_url_logicalid)
606+
607+
@patch("boto3.session.Session.region_name", "ap-southeast-1")
608+
def test_with_valid_function_url_config_with_authorization_type_value_as_None(self):
609+
610+
function = SamFunction("foo")
611+
function.CodeUri = "s3://foobar/foo.zip"
612+
function.Runtime = "foo"
613+
function.Handler = "bar"
614+
function.FunctionUrlConfig = {"AuthorizationType": None}
615+
616+
with pytest.raises(InvalidResourceException) as e:
617+
cfnResources = function.to_cloudformation(**self.kwargs)
618+
self.assertEqual(
619+
str(e.value.message),
620+
"Resource with id [foo] is invalid. AuthorizationType is required to configure function property "
621+
+ "`FunctionUrlConfig`. Please provide either an IAM role name or NONE.",
622+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Parameters: {}
3+
Resources:
4+
MyFunction:
5+
Type: AWS::Serverless::Function
6+
Properties:
7+
CodeUri: s3://sam-demo-bucket/hello.zip
8+
Description: Created by SAM
9+
Handler: index.handler
10+
MemorySize: 1024
11+
Runtime: nodejs12.x
12+
Timeout: 3
13+
FunctionUrlConfig:
14+
AuthorizationType: NONE
15+
CorsConfig:
16+
AllowOrigin:
17+
- "https://example.com"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Parameters: {}
3+
Resources:
4+
MyFunction:
5+
Type: AWS::Serverless::Function
6+
Properties:
7+
CodeUri: s3://sam-demo-bucket/hello.zip
8+
Description: Created by SAM
9+
Handler: index.handler
10+
MemorySize: 1024
11+
Runtime: nodejs12.x
12+
Timeout: 3
13+
FunctionUrlConfig:
14+
AuthorizationType: NONE
15+
CorsConfig:
16+
MaxAge: "10"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Parameters: {}
3+
Resources:
4+
MyFunction:
5+
Type: AWS::Serverless::Function
6+
Properties:
7+
CodeUri: s3://sam-demo-bucket/hello.zip
8+
Description: Created by SAM
9+
Handler: index.handler
10+
MemorySize: 1024
11+
Runtime: nodejs12.x
12+
Timeout: 3
13+
FunctionUrlConfig:
14+
CorsConfig:
15+
AllowOrigins:
16+
- "https://example.com"
17+
- "example1.com"
18+
- "example2.com"
19+
- "example2.com"
20+
AllowMethods:
21+
- "GET"
22+
AllowCredentials: true
23+
AllowHeaders:
24+
- "x-Custom-Header"
25+
ExposeHeaders:
26+
- "x-amzn-header"
27+
MaxAge: 10
28+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Parameters: {}
3+
Resources:
4+
MyFunction:
5+
Type: AWS::Serverless::Function
6+
Properties:
7+
CodeUri: s3://sam-demo-bucket/hello.zip
8+
Description: Created by SAM
9+
Handler: index.handler
10+
MemorySize: 1024
11+
Runtime: nodejs12.x
12+
Timeout: 3
13+
FunctionUrlConfig:
14+
AuthorizationType: NONE
15+
CorsConfig:
16+
AllowOrigins:
17+
- "https://example.com"
18+
- "example1.com"
19+
- "example2.com"
20+
- "example2.com"
21+
AllowMethods:
22+
- "GET"
23+
AllowCredentials: true
24+
AllowHeaders:
25+
- "x-Custom-Header"
26+
ExposeHeaders:
27+
- "x-amzn-header"
28+
MaxAge: 10
29+

0 commit comments

Comments
 (0)