Skip to content

Commit cf92b7b

Browse files
authored
Merge pull request #3745 from aws/release-v1.96.0
Release 1.96.0 (to main)
2 parents a7da20c + c5b1b2e commit cf92b7b

34 files changed

+4914
-949
lines changed

.cfnlintrc.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,4 @@ ignore_checks:
147147
- E3001 # Invalid or unsupported Type; common in transform tests since they focus on SAM resources
148148
- W2001 # Parameter not used
149149
- E3006 # Resource type check; we have some Foo Bar resources
150+
- W3037 # Ignore cfn-lint check for non existing IAM permissions

.github/workflows/close_issue_message.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
permissions:
1010
issues: write
1111
steps:
12-
- uses: aws-actions/closed-issue-message@v1
12+
- uses: aws-actions/closed-issue-message@v2
1313
with:
1414
# These inputs are both required
1515
repo-token: "${{ secrets.GITHUB_TOKEN }}"

integration/combination/test_function_with_alias.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from botocore.exceptions import ClientError
55

6-
from integration.config.service_names import REST_API
6+
from integration.config.service_names import LAYERS, REST_API
77
from integration.helpers.base_test import LOG, BaseTest
88
from integration.helpers.common_api import get_function_versions
99
from integration.helpers.resource import current_region_does_not_support
@@ -160,6 +160,7 @@ def test_alias_with_event_sources_get_correct_permissions(self):
160160
function_policy = json.loads(function_policy_str)
161161
self.assertEqual(len(function_policy["Statement"]), len(permission_resources))
162162

163+
@skipIf(current_region_does_not_support([LAYERS]), "Layers is not supported in this testing region")
163164
def test_function_with_alias_and_layer_version(self):
164165
self.create_and_verify_stack("combination/function_with_alias_all_properties_and_layer_version")
165166
alias_name = "Live"

samtranslator/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.95.0"
1+
__version__ = "1.96.0"

samtranslator/internal/schema_source/aws_serverless_api.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,16 @@ class Route53(BaseModel):
159159
class Domain(BaseModel):
160160
BasePath: Optional[PassThroughProp] = domain("BasePath")
161161
NormalizeBasePath: Optional[bool] = domain("NormalizeBasePath")
162+
Policy: Optional[PassThroughProp]
162163
CertificateArn: PassThroughProp = domain("CertificateArn")
163164
DomainName: PassThroughProp = passthrough_prop(
164165
DOMAIN_STEM,
165166
"DomainName",
166167
["AWS::ApiGateway::DomainName", "Properties", "DomainName"],
167168
)
168-
EndpointConfiguration: Optional[SamIntrinsicable[Literal["REGIONAL", "EDGE"]]] = domain("EndpointConfiguration")
169+
EndpointConfiguration: Optional[SamIntrinsicable[Literal["REGIONAL", "EDGE", "PRIVATE"]]] = domain(
170+
"EndpointConfiguration"
171+
)
169172
MutualTlsAuthentication: Optional[PassThroughProp] = passthrough_prop(
170173
DOMAIN_STEM,
171174
"MutualTlsAuthentication",

samtranslator/model/api/api_generator.py

+186-23
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
ApiGatewayApiKey,
1111
ApiGatewayAuthorizer,
1212
ApiGatewayBasePathMapping,
13+
ApiGatewayBasePathMappingV2,
1314
ApiGatewayDeployment,
1415
ApiGatewayDomainName,
16+
ApiGatewayDomainNameV2,
1517
ApiGatewayResponse,
1618
ApiGatewayRestApi,
1719
ApiGatewayStage,
@@ -79,6 +81,13 @@ class ApiDomainResponse:
7981
recordset_group: Any
8082

8183

84+
@dataclass
85+
class ApiDomainResponseV2:
86+
domain: Optional[ApiGatewayDomainNameV2]
87+
apigw_basepath_mapping_list: Optional[List[ApiGatewayBasePathMappingV2]]
88+
recordset_group: Any
89+
90+
8291
class SharedApiUsagePlan:
8392
"""
8493
Collects API information from different API resources in the same template,
@@ -517,11 +526,7 @@ def _construct_api_domain( # noqa: PLR0912, PLR0915
517526
if mutual_tls_auth.get("TruststoreVersion", None):
518527
domain.MutualTlsAuthentication["TruststoreVersion"] = mutual_tls_auth["TruststoreVersion"]
519528

520-
if self.domain.get("SecurityPolicy", None):
521-
domain.SecurityPolicy = self.domain["SecurityPolicy"]
522-
523-
if self.domain.get("OwnershipVerificationCertificateArn", None):
524-
domain.OwnershipVerificationCertificateArn = self.domain["OwnershipVerificationCertificateArn"]
529+
self._set_optional_domain_properties(domain)
525530

526531
basepaths: Optional[List[str]]
527532
basepath_value = self.domain.get("BasePath")
@@ -539,12 +544,102 @@ def _construct_api_domain( # noqa: PLR0912, PLR0915
539544
basepath_resource_list: List[ApiGatewayBasePathMapping] = []
540545

541546
if basepaths is None:
542-
basepath_mapping = ApiGatewayBasePathMapping(
543-
self.logical_id + "BasePathMapping", attributes=self.passthrough_resource_attributes
547+
basepath_mapping = self._create_basepath_mapping(api_domain_name, rest_api, None, None)
548+
basepath_resource_list.extend([basepath_mapping])
549+
else:
550+
sam_expect(basepaths, self.logical_id, "Domain.BasePath").to_be_a_list_of(ExpectedType.STRING)
551+
for basepath in basepaths:
552+
# Remove possible leading and trailing '/' because a base path may only
553+
# contain letters, numbers, and one of "$-_.+!*'()"
554+
path = "".join(e for e in basepath if e.isalnum())
555+
mapping_basepath = path if normalize_basepath else basepath
556+
logical_id = "{}{}{}".format(self.logical_id, path, "BasePathMapping")
557+
basepath_mapping = self._create_basepath_mapping(
558+
api_domain_name, rest_api, logical_id, mapping_basepath
559+
)
560+
basepath_resource_list.extend([basepath_mapping])
561+
562+
# Create the Route53 RecordSetGroup resource
563+
record_set_group = None
564+
route53 = self.domain.get("Route53")
565+
if route53 is not None:
566+
sam_expect(route53, self.logical_id, "Domain.Route53").to_be_a_map()
567+
if route53.get("HostedZoneId") is None and route53.get("HostedZoneName") is None:
568+
raise InvalidResourceException(
569+
self.logical_id,
570+
"HostedZoneId or HostedZoneName is required to enable Route53 support on Custom Domains.",
571+
)
572+
573+
logical_id_suffix = LogicalIdGenerator(
574+
"", route53.get("HostedZoneId") or route53.get("HostedZoneName")
575+
).gen()
576+
logical_id = "RecordSetGroup" + logical_id_suffix
577+
578+
record_set_group = route53_record_set_groups.get(logical_id)
579+
580+
if route53.get("SeparateRecordSetGroup"):
581+
sam_expect(
582+
route53.get("SeparateRecordSetGroup"), self.logical_id, "Domain.Route53.SeparateRecordSetGroup"
583+
).to_be_a_bool()
584+
return ApiDomainResponse(
585+
domain,
586+
basepath_resource_list,
587+
self._construct_single_record_set_group(self.domain, api_domain_name, route53),
588+
)
589+
590+
if not record_set_group:
591+
record_set_group = self._get_record_set_group(logical_id, route53)
592+
route53_record_set_groups[logical_id] = record_set_group
593+
594+
record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain, api_domain_name, route53)
595+
596+
return ApiDomainResponse(domain, basepath_resource_list, record_set_group)
597+
598+
def _construct_api_domain_v2(
599+
self, rest_api: ApiGatewayRestApi, route53_record_set_groups: Any
600+
) -> ApiDomainResponseV2:
601+
"""
602+
Constructs and returns the ApiGateway Domain V2 and BasepathMapping V2
603+
"""
604+
if self.domain is None:
605+
return ApiDomainResponseV2(None, None, None)
606+
607+
sam_expect(self.domain, self.logical_id, "Domain").to_be_a_map()
608+
domain_name: PassThrough = sam_expect(
609+
self.domain.get("DomainName"), self.logical_id, "Domain.DomainName"
610+
).to_not_be_none()
611+
certificate_arn: PassThrough = sam_expect(
612+
self.domain.get("CertificateArn"), self.logical_id, "Domain.CertificateArn"
613+
).to_not_be_none()
614+
615+
api_domain_name = "{}{}".format("ApiGatewayDomainNameV2", LogicalIdGenerator("", domain_name).gen())
616+
domain_name_arn = ref(api_domain_name)
617+
domain = ApiGatewayDomainNameV2(api_domain_name, attributes=self.passthrough_resource_attributes)
618+
619+
domain.DomainName = domain_name
620+
endpoint = self.domain.get("EndpointConfiguration")
621+
622+
if endpoint not in ["EDGE", "REGIONAL", "PRIVATE"]:
623+
raise InvalidResourceException(
624+
self.logical_id,
625+
"EndpointConfiguration for Custom Domains must be"
626+
" one of {}.".format(["EDGE", "REGIONAL", "PRIVATE"]),
544627
)
545-
basepath_mapping.DomainName = ref(api_domain_name)
546-
basepath_mapping.RestApiId = ref(rest_api.logical_id)
547-
basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage")
628+
629+
domain.CertificateArn = certificate_arn
630+
631+
domain.EndpointConfiguration = {"Types": [endpoint]}
632+
633+
self._set_optional_domain_properties(domain)
634+
635+
basepaths: Optional[List[str]] = self._get_basepaths()
636+
637+
# Boolean to allow/disallow symbols in BasePath property
638+
normalize_basepath = self.domain.get("NormalizeBasePath", True)
639+
640+
basepath_resource_list: List[ApiGatewayBasePathMappingV2] = []
641+
if basepaths is None:
642+
basepath_mapping = self._create_basepath_mapping_v2(domain_name_arn, rest_api)
548643
basepath_resource_list.extend([basepath_mapping])
549644
else:
550645
sam_expect(basepaths, self.logical_id, "Domain.BasePath").to_be_a_list_of(ExpectedType.STRING)
@@ -553,10 +648,10 @@ def _construct_api_domain( # noqa: PLR0912, PLR0915
553648
# contain letters, numbers, and one of "$-_.+!*'()"
554649
path = "".join(e for e in basepath if e.isalnum())
555650
logical_id = "{}{}{}".format(self.logical_id, path, "BasePathMapping")
556-
basepath_mapping = ApiGatewayBasePathMapping(
651+
basepath_mapping = ApiGatewayBasePathMappingV2(
557652
logical_id, attributes=self.passthrough_resource_attributes
558653
)
559-
basepath_mapping.DomainName = ref(api_domain_name)
654+
basepath_mapping.DomainNameArn = domain_name_arn
560655
basepath_mapping.RestApiId = ref(rest_api.logical_id)
561656
basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage")
562657
basepath_mapping.BasePath = path if normalize_basepath else basepath
@@ -584,24 +679,48 @@ def _construct_api_domain( # noqa: PLR0912, PLR0915
584679
sam_expect(
585680
route53.get("SeparateRecordSetGroup"), self.logical_id, "Domain.Route53.SeparateRecordSetGroup"
586681
).to_be_a_bool()
587-
return ApiDomainResponse(
682+
return ApiDomainResponseV2(
588683
domain,
589684
basepath_resource_list,
590-
self._construct_single_record_set_group(self.domain, api_domain_name, route53),
685+
self._construct_single_record_set_group(self.domain, domain_name, route53),
591686
)
592687

593688
if not record_set_group:
594-
record_set_group = Route53RecordSetGroup(logical_id, attributes=self.passthrough_resource_attributes)
595-
if "HostedZoneId" in route53:
596-
record_set_group.HostedZoneId = route53.get("HostedZoneId")
597-
if "HostedZoneName" in route53:
598-
record_set_group.HostedZoneName = route53.get("HostedZoneName")
599-
record_set_group.RecordSets = []
689+
record_set_group = self._get_record_set_group(logical_id, route53)
600690
route53_record_set_groups[logical_id] = record_set_group
601691

602-
record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain, api_domain_name, route53)
692+
record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain, domain_name, route53)
603693

604-
return ApiDomainResponse(domain, basepath_resource_list, record_set_group)
694+
return ApiDomainResponseV2(domain, basepath_resource_list, record_set_group)
695+
696+
def _get_basepaths(self) -> Optional[List[str]]:
697+
if self.domain is None:
698+
return None
699+
basepath_value = self.domain.get("BasePath")
700+
if self.domain.get("BasePath") and isinstance(basepath_value, str):
701+
return [basepath_value]
702+
if self.domain.get("BasePath") and isinstance(basepath_value, list):
703+
return cast(Optional[List[Any]], basepath_value)
704+
return None
705+
706+
def _set_optional_domain_properties(self, domain: Union[ApiGatewayDomainName, ApiGatewayDomainNameV2]) -> None:
707+
if self.domain is None:
708+
return
709+
if self.domain.get("SecurityPolicy", None):
710+
domain.SecurityPolicy = self.domain["SecurityPolicy"]
711+
if self.domain.get("Policy", None):
712+
domain.Policy = self.domain["Policy"]
713+
if self.domain.get("OwnershipVerificationCertificateArn", None):
714+
domain.OwnershipVerificationCertificateArn = self.domain["OwnershipVerificationCertificateArn"]
715+
716+
def _get_record_set_group(self, logical_id: str, route53: Dict[str, Any]) -> Route53RecordSetGroup:
717+
record_set_group = Route53RecordSetGroup(logical_id, attributes=self.passthrough_resource_attributes)
718+
if "HostedZoneId" in route53:
719+
record_set_group.HostedZoneId = route53.get("HostedZoneId")
720+
if "HostedZoneName" in route53:
721+
record_set_group.HostedZoneName = route53.get("HostedZoneName")
722+
record_set_group.RecordSets = []
723+
return record_set_group
605724

606725
def _construct_single_record_set_group(
607726
self, domain: Dict[str, Any], api_domain_name: str, route53: Any
@@ -667,6 +786,40 @@ def _construct_alias_target(self, domain: Dict[str, Any], api_domain_name: str,
667786
alias_target["DNSName"] = route53.get("DistributionDomainName")
668787
return alias_target
669788

789+
def _create_basepath_mapping(
790+
self,
791+
api_domain_name: PassThrough,
792+
rest_api: ApiGatewayRestApi,
793+
logical_id: Optional[str],
794+
basepath: Optional[str],
795+
) -> ApiGatewayBasePathMapping:
796+
797+
basepath_mapping: ApiGatewayBasePathMapping
798+
basepath_mapping = (
799+
ApiGatewayBasePathMapping(logical_id, attributes=self.passthrough_resource_attributes)
800+
if logical_id
801+
else ApiGatewayBasePathMapping(
802+
self.logical_id + "BasePathMapping", attributes=self.passthrough_resource_attributes
803+
)
804+
)
805+
basepath_mapping.DomainName = ref(api_domain_name)
806+
basepath_mapping.RestApiId = ref(rest_api.logical_id)
807+
basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage")
808+
if basepath is not None:
809+
basepath_mapping.BasePath = basepath
810+
return basepath_mapping
811+
812+
def _create_basepath_mapping_v2(
813+
self, domain_name_arn: PassThrough, rest_api: ApiGatewayRestApi
814+
) -> ApiGatewayBasePathMappingV2:
815+
basepath_mapping = ApiGatewayBasePathMappingV2(
816+
self.logical_id + "BasePathMapping", attributes=self.passthrough_resource_attributes
817+
)
818+
basepath_mapping.DomainNameArn = domain_name_arn
819+
basepath_mapping.RestApiId = ref(rest_api.logical_id)
820+
basepath_mapping.Stage = ref(rest_api.logical_id + ".Stage")
821+
return basepath_mapping
822+
670823
@cw_timer(prefix="Generator", name="Api")
671824
def to_cloudformation(
672825
self, redeploy_restapi_parameters: Optional[Any], route53_record_set_groups: Dict[str, Route53RecordSetGroup]
@@ -676,10 +829,19 @@ def to_cloudformation(
676829
:returns: a tuple containing the RestApi, Deployment, and Stage for an empty Api.
677830
:rtype: tuple
678831
"""
832+
api_domain_response: Union[ApiDomainResponseV2, ApiDomainResponse]
833+
domain: Union[Resource, None]
834+
basepath_mapping: Union[List[ApiGatewayBasePathMapping], List[ApiGatewayBasePathMappingV2], None]
679835
rest_api = self._construct_rest_api()
680-
api_domain_response = self._construct_api_domain(rest_api, route53_record_set_groups)
836+
api_domain_response = (
837+
self._construct_api_domain_v2(rest_api, route53_record_set_groups)
838+
if isinstance(self.domain, dict) and self.domain.get("EndpointConfiguration") == "PRIVATE"
839+
else self._construct_api_domain(rest_api, route53_record_set_groups)
840+
)
841+
681842
domain = api_domain_response.domain
682843
basepath_mapping = api_domain_response.apigw_basepath_mapping_list
844+
683845
route53_recordsetGroup = api_domain_response.recordset_group
684846

685847
deployment = self._construct_deployment(rest_api)
@@ -703,6 +865,7 @@ def to_cloudformation(
703865
Tuple[Resource],
704866
List[LambdaPermission],
705867
List[ApiGatewayBasePathMapping],
868+
List[ApiGatewayBasePathMappingV2],
706869
],
707870
] = []
708871

samtranslator/model/apigateway.py

+29
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,25 @@ class ApiGatewayDomainName(Resource):
230230
OwnershipVerificationCertificateArn: Optional[PassThrough]
231231

232232

233+
class ApiGatewayDomainNameV2(Resource):
234+
resource_type = "AWS::ApiGateway::DomainNameV2"
235+
property_types = {
236+
"DomainName": GeneratedProperty(),
237+
"EndpointConfiguration": GeneratedProperty(),
238+
"SecurityPolicy": GeneratedProperty(),
239+
"CertificateArn": GeneratedProperty(),
240+
"Tags": GeneratedProperty(),
241+
"Policy": GeneratedProperty(),
242+
}
243+
244+
DomainName: PassThrough
245+
EndpointConfiguration: Optional[PassThrough]
246+
SecurityPolicy: Optional[PassThrough]
247+
CertificateArn: Optional[PassThrough]
248+
Tags: Optional[PassThrough]
249+
Policy: Optional[PassThrough]
250+
251+
233252
class ApiGatewayBasePathMapping(Resource):
234253
resource_type = "AWS::ApiGateway::BasePathMapping"
235254
property_types = {
@@ -240,6 +259,16 @@ class ApiGatewayBasePathMapping(Resource):
240259
}
241260

242261

262+
class ApiGatewayBasePathMappingV2(Resource):
263+
resource_type = "AWS::ApiGateway::BasePathMappingV2"
264+
property_types = {
265+
"BasePath": GeneratedProperty(),
266+
"DomainNameArn": GeneratedProperty(),
267+
"RestApiId": GeneratedProperty(),
268+
"Stage": GeneratedProperty(),
269+
}
270+
271+
243272
class ApiGatewayUsagePlan(Resource):
244273
resource_type = "AWS::ApiGateway::UsagePlan"
245274
property_types = {

samtranslator/model/sam_resources.py

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
ApiGatewayApiKey,
5454
ApiGatewayDeployment,
5555
ApiGatewayDomainName,
56+
ApiGatewayDomainNameV2,
5657
ApiGatewayStage,
5758
ApiGatewayUsagePlan,
5859
ApiGatewayUsagePlanKey,
@@ -1310,6 +1311,7 @@ class SamApi(SamResourceMacro):
13101311
"Stage": ApiGatewayStage.resource_type,
13111312
"Deployment": ApiGatewayDeployment.resource_type,
13121313
"DomainName": ApiGatewayDomainName.resource_type,
1314+
"DomainNameV2": ApiGatewayDomainNameV2.resource_type,
13131315
"UsagePlan": ApiGatewayUsagePlan.resource_type,
13141316
"UsagePlanKey": ApiGatewayUsagePlanKey.resource_type,
13151317
"ApiKey": ApiGatewayApiKey.resource_type,

0 commit comments

Comments
 (0)