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+
8291class 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 :
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
0 commit comments