Skip to content

Commit 38a85f7

Browse files
authored
Merge branch 'develop' into fix-pydantic1-usage
2 parents f9cd4a8 + 857db28 commit 38a85f7

File tree

60 files changed

+7808
-1254
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+7808
-1254
lines changed

.cfnlintrc.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
templates:
22
- tests/translator/output/**/*.json
33
ignore_templates:
4+
- tests/translator/output/**/function_with_function_url_config.json
5+
- tests/translator/output/**/function_with_function_url_config_and_autopublishalias.json
6+
- tests/translator/output/**/function_with_function_url_config_without_cors_config.json
47
- tests/translator/output/**/error_*.json # Fail by design
58
- tests/translator/output/**/api_http_paths_with_if_condition.json
69
- tests/translator/output/**/api_http_paths_with_if_condition_no_value_else_case.json
@@ -139,7 +142,6 @@ ignore_templates:
139142
- tests/translator/output/**/managed_policies_everything.json # intentionally contains wrong arns
140143
- tests/translator/output/**/function_with_provisioned_poller_config.json
141144
- tests/translator/output/**/function_with_metrics_config.json
142-
- tests/translator/output/**/api_with_custom_domains_private.json
143145

144146
ignore_checks:
145147
- E2531 # Deprecated runtime; not relevant for transform tests
@@ -148,4 +150,4 @@ ignore_checks:
148150
- E3001 # Invalid or unsupported Type; common in transform tests since they focus on SAM resources
149151
- W2001 # Parameter not used
150152
- E3006 # Resource type check; we have some Foo Bar resources
151-
- W3037 # Ignore cfn-lint check for non existing IAM permissions
153+
- W3037 # Ignore cfn-lint check for non existing IAM permissions
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": "MyLambdaFunctionUrlPublicPermissions",
12+
"ResourceType": "AWS::Lambda::Permission"
13+
},
14+
{
15+
"LogicalResourceId": "MyLambdaFunctionURLInvokeAllowPublicAccess",
16+
"ResourceType": "AWS::Lambda::Permission"
17+
},
18+
{
19+
"LogicalResourceId": "MyLambdaFunctionRole",
20+
"ResourceType": "AWS::IAM::Role"
21+
}
22+
]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[
2+
{
3+
"LogicalResourceId": "MyLambdaFunction",
4+
"ResourceType": "AWS::Lambda::Function"
5+
},
6+
{
7+
"LogicalResourceId": "MyLambdaFunctionRole",
8+
"ResourceType": "AWS::IAM::Role"
9+
},
10+
{
11+
"LogicalResourceId": "MyLambdaFunctionVersion",
12+
"ResourceType": "AWS::Lambda::Version"
13+
},
14+
{
15+
"LogicalResourceId": "MyLambdaFunctionAliaslive",
16+
"ResourceType": "AWS::Lambda::Alias"
17+
},
18+
{
19+
"LogicalResourceId": "MyLambdaFunctionUrlPublicPermissions",
20+
"ResourceType": "AWS::Lambda::Permission"
21+
},
22+
{
23+
"LogicalResourceId": "MyLambdaFunctionURLInvokeAllowPublicAccess",
24+
"ResourceType": "AWS::Lambda::Permission"
25+
},
26+
{
27+
"LogicalResourceId": "MyLambdaFunctionUrl",
28+
"ResourceType": "AWS::Lambda::Url"
29+
}
30+
]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Resources:
2+
MyLambdaFunction:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
Handler: index.handler
6+
Runtime: nodejs18.x
7+
CodeUri: ${codeuri}
8+
MemorySize: 128
9+
FunctionUrlConfig:
10+
AuthType: 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
22+
Outputs:
23+
FunctionUrl:
24+
Description: URL of the Lambda function
25+
Value: !GetAtt MyLambdaFunctionUrl.FunctionUrl
26+
Metadata:
27+
SamTransformTest: true
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Resources:
2+
MyLambdaFunction:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
Handler: index.handler
6+
Runtime: nodejs18.x
7+
CodeUri: ${codeuri}
8+
MemorySize: 128
9+
AutoPublishAlias: live
10+
FunctionUrlConfig:
11+
AuthType: 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
23+
Outputs:
24+
FunctionUrl:
25+
Description: URL of the Lambda function alias
26+
Value: !GetAtt MyLambdaFunctionUrl.FunctionUrl
27+
Metadata:
28+
SamTransformTest: true

integration/single/test_basic_function.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,73 @@ def test_basic_function_with_url_config(self, file_name, qualifier):
130130
self.assertEqual(function_url_config["Cors"], cors_config)
131131
self._assert_invoke(lambda_client, function_name, qualifier, 200)
132132

133+
@parameterized.expand(
134+
[
135+
("single/basic_function_with_function_url_dual_auth", None),
136+
("single/basic_function_with_function_url_with_autopuplishalias_dual_auth", "live"),
137+
]
138+
)
139+
@skipIf(current_region_does_not_support([LAMBDA_URL]), "Lambda Url is not supported in this testing region")
140+
def test_basic_function_with_url_dual_auth(self, file_name, qualifier):
141+
"""
142+
Creates a basic lambda function with Function Url with authtype: None
143+
Verifies that 2 AWS::Lambda::Permission resources are created:
144+
- lambda:InvokeFunctionUrl
145+
- lambda:InvokeFunction with InvokedViaFunctionUrl: True
146+
"""
147+
self.create_and_verify_stack(file_name)
148+
149+
# Get Lambda permissions
150+
lambda_permissions = self.get_stack_resources("AWS::Lambda::Permission")
151+
152+
# Verify we have exactly 2 permissions
153+
self.assertEqual(len(lambda_permissions), 2, "Expected exactly 2 Lambda permissions")
154+
155+
# Check for the expected permission logical IDs
156+
invoke_function_url_permission = None
157+
invoke_permission = None
158+
159+
for permission in lambda_permissions:
160+
logical_id = permission["LogicalResourceId"]
161+
if "MyLambdaFunctionUrlPublicPermissions" in logical_id:
162+
invoke_function_url_permission = permission
163+
elif "MyLambdaFunctionURLInvokeAllowPublicAccess" in logical_id:
164+
invoke_permission = permission
165+
166+
# Verify both permissions exist
167+
self.assertIsNotNone(invoke_function_url_permission, "Expected MyLambdaFunctionUrlPublicPermissions to exist")
168+
self.assertIsNotNone(invoke_permission, "Expected MyLambdaFunctionURLInvokeAllowPublicAccess to exist")
169+
170+
# Get the function name and URL
171+
function_name = self.get_physical_id_by_type("AWS::Lambda::Function")
172+
lambda_client = self.client_provider.lambda_client
173+
174+
# Get the function URL configuration to verify auth type
175+
function_url_config = (
176+
lambda_client.get_function_url_config(FunctionName=function_name, Qualifier=qualifier)
177+
if qualifier
178+
else lambda_client.get_function_url_config(FunctionName=function_name)
179+
)
180+
181+
# Verify the auth type is NONE
182+
self.assertEqual(function_url_config["AuthType"], "NONE", "Expected AuthType to be NONE")
183+
184+
# Get the template to check for InvokedViaFunctionUrl property
185+
cfn_client = self.client_provider.cfn_client
186+
template = cfn_client.get_template(StackName=self.stack_name, TemplateStage="Processed")
187+
template_body = template["TemplateBody"]
188+
189+
# Check if the InvokePermission has InvokedViaFunctionUrl: True
190+
# This is a bit hacky but we don't have direct access to the resource properties
191+
# We're checking if the string representation of the template contains this property
192+
template_str = str(template_body)
193+
self.assertIn("InvokedViaFunctionUrl", template_str, "Expected InvokedViaFunctionUrl property in the template")
194+
195+
# Get the function URL from stack outputs
196+
function_url = self.get_stack_output("FunctionUrl")["OutputValue"]
197+
# Invoke the function URL and verify the response
198+
self._verify_get_request(function_url, self.FUNCTION_OUTPUT)
199+
133200
@skipIf(current_region_does_not_support([CODE_DEPLOY]), "CodeDeploy is not supported in this testing region")
134201
def test_function_with_deployment_preference_alarms_intrinsic_if(self):
135202
self.create_and_verify_stack("single/function_with_deployment_preference_alarms_intrinsic_if")

samtranslator/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.95.0"
1+
__version__ = "1.97.0"

samtranslator/internal/schema_source/aws_serverless_api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ class Route53(BaseModel):
154154
SetIdentifier: Optional[PassThroughProp] # TODO: add docs
155155
Region: Optional[PassThroughProp] # TODO: add docs
156156
SeparateRecordSetGroup: Optional[bool] # TODO: add docs
157+
VpcEndpointDomainName: Optional[PassThroughProp] # TODO: add docs
158+
VpcEndpointHostedZoneId: Optional[PassThroughProp] # TODO: add docs
159+
160+
161+
class AccessAssociation(BaseModel):
162+
VpcEndpointId: PassThroughProp # TODO: add docs
157163

158164

159165
class Domain(BaseModel):
@@ -185,6 +191,7 @@ class Domain(BaseModel):
185191
"SecurityPolicy",
186192
["AWS::ApiGateway::DomainName", "Properties", "SecurityPolicy"],
187193
)
194+
AccessAssociation: Optional[AccessAssociation]
188195

189196

190197
class DefinitionUri(BaseModel):
@@ -307,6 +314,7 @@ class Properties(BaseModel):
307314
OpenApiVersion: Optional[OpenApiVersion] = properties("OpenApiVersion")
308315
StageName: SamIntrinsicable[str] = properties("StageName")
309316
Tags: Optional[DictStrAny] = properties("Tags")
317+
Policy: Optional[PassThroughProp] # TODO: add docs
310318
PropagateTags: Optional[bool] # TODO: add docs
311319
TracingEnabled: Optional[TracingEnabled] = passthrough_prop(
312320
PROPERTIES_STEM,

samtranslator/model/api/api_generator.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
ApiGatewayBasePathMappingV2,
1414
ApiGatewayDeployment,
1515
ApiGatewayDomainName,
16+
ApiGatewayDomainNameAccessAssociation,
1617
ApiGatewayDomainNameV2,
1718
ApiGatewayResponse,
1819
ApiGatewayRestApi,
@@ -86,6 +87,7 @@ class ApiDomainResponseV2:
8687
domain: Optional[ApiGatewayDomainNameV2]
8788
apigw_basepath_mapping_list: Optional[List[ApiGatewayBasePathMappingV2]]
8889
recordset_group: Any
90+
domain_access_association: Any
8991

9092

9193
class SharedApiUsagePlan:
@@ -218,6 +220,7 @@ def __init__( # noqa: PLR0913
218220
api_key_source_type: Optional[Intrinsicable[str]] = None,
219221
always_deploy: Optional[bool] = False,
220222
feature_toggle: Optional[FeatureToggle] = None,
223+
policy: Optional[Union[Dict[str, Any], Intrinsicable[str]]] = None,
221224
):
222225
"""Constructs an API Generator class that generates API Gateway resources
223226
@@ -275,6 +278,7 @@ def __init__( # noqa: PLR0913
275278
self.api_key_source_type = api_key_source_type
276279
self.always_deploy = always_deploy
277280
self.feature_toggle = feature_toggle
281+
self.policy = policy
278282

279283
def _construct_rest_api(self) -> ApiGatewayRestApi:
280284
"""Constructs and returns the ApiGateway RestApi.
@@ -328,6 +332,9 @@ def _construct_rest_api(self) -> ApiGatewayRestApi:
328332
if self.api_key_source_type:
329333
rest_api.ApiKeySourceType = self.api_key_source_type
330334

335+
if self.policy:
336+
rest_api.Policy = self.policy
337+
331338
return rest_api
332339

333340
def _validate_properties(self) -> None:
@@ -602,7 +609,7 @@ def _construct_api_domain_v2(
602609
Constructs and returns the ApiGateway Domain V2 and BasepathMapping V2
603610
"""
604611
if self.domain is None:
605-
return ApiDomainResponseV2(None, None, None)
612+
return ApiDomainResponseV2(None, None, None, None)
606613

607614
sam_expect(self.domain, self.logical_id, "Domain").to_be_a_map()
608615
domain_name: PassThrough = sam_expect(
@@ -657,6 +664,14 @@ def _construct_api_domain_v2(
657664
basepath_mapping.BasePath = path if normalize_basepath else basepath
658665
basepath_resource_list.extend([basepath_mapping])
659666

667+
# Create the DomainNameAccessAssociation
668+
domain_access_association = self.domain.get("AccessAssociation")
669+
domain_access_association_resource = None
670+
if domain_access_association is not None:
671+
domain_access_association_resource = self._generate_domain_access_association(
672+
domain_access_association, domain_name_arn, api_domain_name
673+
)
674+
660675
# Create the Route53 RecordSetGroup resource
661676
record_set_group = None
662677
route53 = self.domain.get("Route53")
@@ -683,6 +698,7 @@ def _construct_api_domain_v2(
683698
domain,
684699
basepath_resource_list,
685700
self._construct_single_record_set_group(self.domain, domain_name, route53),
701+
domain_access_association_resource,
686702
)
687703

688704
if not record_set_group:
@@ -691,7 +707,7 @@ def _construct_api_domain_v2(
691707

692708
record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain, domain_name, route53)
693709

694-
return ApiDomainResponseV2(domain, basepath_resource_list, record_set_group)
710+
return ApiDomainResponseV2(domain, basepath_resource_list, record_set_group, domain_access_association_resource)
695711

696712
def _get_basepaths(self) -> Optional[List[str]]:
697713
if self.domain is None:
@@ -779,11 +795,14 @@ def _construct_alias_target(self, domain: Dict[str, Any], api_domain_name: str,
779795
if domain.get("EndpointConfiguration") == "REGIONAL":
780796
alias_target["HostedZoneId"] = fnGetAtt(api_domain_name, "RegionalHostedZoneId")
781797
alias_target["DNSName"] = fnGetAtt(api_domain_name, "RegionalDomainName")
782-
else:
798+
elif domain.get("EndpointConfiguration") == "EDGE":
783799
if route53.get("DistributionDomainName") is None:
784800
route53["DistributionDomainName"] = fnGetAtt(api_domain_name, "DistributionDomainName")
785801
alias_target["HostedZoneId"] = "Z2FDTNDATAQYW2"
786802
alias_target["DNSName"] = route53.get("DistributionDomainName")
803+
else:
804+
alias_target["HostedZoneId"] = route53.get("VpcEndpointHostedZoneId")
805+
alias_target["DNSName"] = route53.get("VpcEndpointDomainName")
787806
return alias_target
788807

789808
def _create_basepath_mapping(
@@ -833,12 +852,17 @@ def to_cloudformation(
833852
domain: Union[Resource, None]
834853
basepath_mapping: Union[List[ApiGatewayBasePathMapping], List[ApiGatewayBasePathMappingV2], None]
835854
rest_api = self._construct_rest_api()
855+
is_private_domain = isinstance(self.domain, dict) and self.domain.get("EndpointConfiguration") == "PRIVATE"
836856
api_domain_response = (
837857
self._construct_api_domain_v2(rest_api, route53_record_set_groups)
838-
if isinstance(self.domain, dict) and self.domain.get("EndpointConfiguration") == "PRIVATE"
858+
if is_private_domain
839859
else self._construct_api_domain(rest_api, route53_record_set_groups)
840860
)
841861

862+
domain_access_association = None
863+
if is_private_domain:
864+
domain_access_association = cast(ApiDomainResponseV2, api_domain_response).domain_access_association
865+
842866
domain = api_domain_response.domain
843867
basepath_mapping = api_domain_response.apigw_basepath_mapping_list
844868

@@ -882,6 +906,9 @@ def to_cloudformation(
882906
]
883907
)
884908

909+
if domain_access_association is not None:
910+
generated_resources.append(domain_access_association)
911+
885912
# Make a list of single resources
886913
generated_resources_list: List[Resource] = []
887914
for resource in generated_resources:
@@ -1513,3 +1540,24 @@ def _set_endpoint_configuration(self, rest_api: ApiGatewayRestApi, value: Union[
15131540
else:
15141541
rest_api.EndpointConfiguration = {"Types": [value]}
15151542
rest_api.Parameters = {"endpointConfigurationTypes": value}
1543+
1544+
def _generate_domain_access_association(
1545+
self,
1546+
domain_access_association: Dict[str, Any],
1547+
domain_name_arn: Dict[str, str],
1548+
domain_logical_id: str,
1549+
) -> ApiGatewayDomainNameAccessAssociation:
1550+
"""
1551+
Generate domain access association resource
1552+
"""
1553+
vpcEndpointId = domain_access_association.get("VpcEndpointId")
1554+
logical_id = LogicalIdGenerator("DomainNameAccessAssociation", [vpcEndpointId, domain_logical_id]).gen()
1555+
1556+
domain_access_association_resource = ApiGatewayDomainNameAccessAssociation(
1557+
logical_id, attributes=self.passthrough_resource_attributes
1558+
)
1559+
domain_access_association_resource.DomainNameArn = domain_name_arn
1560+
domain_access_association_resource.AccessAssociationSourceType = "VPCE"
1561+
domain_access_association_resource.AccessAssociationSource = vpcEndpointId
1562+
1563+
return domain_access_association_resource

0 commit comments

Comments
 (0)