Skip to content

Commit 2fa9718

Browse files
authored
feat: Add a new property SeparateRecordSetGroup to disable merging into record set group (#2993)
1 parent 7aa03ec commit 2fa9718

11 files changed

+1330
-36
lines changed

samtranslator/internal/schema_source/aws_serverless_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ class Route53(BaseModel):
134134
IpV6: Optional[bool] = route53("IpV6")
135135
SetIdentifier: Optional[PassThroughProp] # TODO: add docs
136136
Region: Optional[PassThroughProp] # TODO: add docs
137+
SeparateRecordSetGroup: Optional[bool] # TODO: add docs
137138

138139

139140
class Domain(BaseModel):

samtranslator/model/api/api_generator.py

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import logging
22
from collections import namedtuple
3+
from dataclasses import dataclass
34
from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast
45

56
from samtranslator.metrics.method_decorator import cw_timer
7+
from samtranslator.model import Resource
68
from samtranslator.model.apigateway import (
79
ApiGatewayApiKey,
810
ApiGatewayAuthorizer,
@@ -67,6 +69,13 @@
6769
GatewayResponseProperties = ["ResponseParameters", "ResponseTemplates", "StatusCode"]
6870

6971

72+
@dataclass
73+
class ApiDomainResponse:
74+
domain: Optional[ApiGatewayDomainName]
75+
apigw_basepath_mapping_list: Optional[List[ApiGatewayBasePathMapping]]
76+
recordset_group: Any
77+
78+
7079
class SharedApiUsagePlan:
7180
"""
7281
Collects API information from different API resources in the same template,
@@ -443,12 +452,12 @@ def _construct_stage(
443452

444453
def _construct_api_domain( # noqa: too-many-branches
445454
self, rest_api: ApiGatewayRestApi, route53_record_set_groups: Any
446-
) -> Tuple[Optional[ApiGatewayDomainName], Optional[List[ApiGatewayBasePathMapping]], Any]:
455+
) -> ApiDomainResponse:
447456
"""
448457
Constructs and returns the ApiGateway Domain and BasepathMapping
449458
"""
450459
if self.domain is None:
451-
return None, None, None
460+
return ApiDomainResponse(None, None, None)
452461

453462
sam_expect(self.domain, self.logical_id, "Domain").to_be_a_map()
454463
domain_name: PassThrough = sam_expect(
@@ -565,6 +574,17 @@ def _construct_api_domain( # noqa: too-many-branches
565574
logical_id = "RecordSetGroup" + logical_id_suffix
566575

567576
record_set_group = route53_record_set_groups.get(logical_id)
577+
578+
if route53.get("SeparateRecordSetGroup"):
579+
sam_expect(
580+
route53.get("SeparateRecordSetGroup"), self.logical_id, "Domain.Route53.SeparateRecordSetGroup"
581+
).to_be_a_bool()
582+
return ApiDomainResponse(
583+
domain,
584+
basepath_resource_list,
585+
self._construct_single_record_set_group(self.domain, api_domain_name, route53),
586+
)
587+
568588
if not record_set_group:
569589
record_set_group = Route53RecordSetGroup(logical_id, attributes=self.passthrough_resource_attributes)
570590
if "HostedZoneId" in route53:
@@ -576,27 +596,46 @@ def _construct_api_domain( # noqa: too-many-branches
576596

577597
record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain, api_domain_name, route53)
578598

579-
return domain, basepath_resource_list, record_set_group
599+
return ApiDomainResponse(domain, basepath_resource_list, record_set_group)
600+
601+
def _construct_single_record_set_group(
602+
self, domain: Dict[str, Any], api_domain_name: str, route53: Any
603+
) -> Route53RecordSetGroup:
604+
hostedZoneId = route53.get("HostedZoneId")
605+
hostedZoneName = route53.get("HostedZoneName")
606+
domainName = domain.get("DomainName")
607+
logical_id = logical_id = LogicalIdGenerator(
608+
"RecordSetGroup", [hostedZoneId or hostedZoneName, domainName]
609+
).gen()
610+
611+
record_set_group = Route53RecordSetGroup(logical_id, attributes=self.passthrough_resource_attributes)
612+
if hostedZoneId:
613+
record_set_group.HostedZoneId = hostedZoneId
614+
if hostedZoneName:
615+
record_set_group.HostedZoneName = hostedZoneName
616+
617+
record_set_group.RecordSets = []
618+
record_set_group.RecordSets += self._construct_record_sets_for_domain(domain, api_domain_name, route53)
619+
620+
return record_set_group
580621

581622
def _construct_record_sets_for_domain(
582623
self, custom_domain_config: Dict[str, Any], api_domain_name: str, route53_config: Dict[str, Any]
583624
) -> List[Dict[str, Any]]:
584625
recordset_list = []
585-
626+
alias_target = self._construct_alias_target(custom_domain_config, api_domain_name, route53_config)
586627
recordset = {}
587628
recordset["Name"] = custom_domain_config.get("DomainName")
588629
recordset["Type"] = "A"
589-
recordset["AliasTarget"] = self._construct_alias_target(custom_domain_config, api_domain_name, route53_config)
630+
recordset["AliasTarget"] = alias_target
590631
self._update_route53_routing_policy_properties(route53_config, recordset)
591632
recordset_list.append(recordset)
592633

593634
if route53_config.get("IpV6") is not None and route53_config.get("IpV6") is True:
594635
recordset_ipv6 = {}
595636
recordset_ipv6["Name"] = custom_domain_config.get("DomainName")
596637
recordset_ipv6["Type"] = "AAAA"
597-
recordset_ipv6["AliasTarget"] = self._construct_alias_target(
598-
custom_domain_config, api_domain_name, route53_config
599-
)
638+
recordset_ipv6["AliasTarget"] = alias_target
600639
self._update_route53_routing_policy_properties(route53_config, recordset_ipv6)
601640
recordset_list.append(recordset_ipv6)
602641

@@ -626,14 +665,20 @@ def _construct_alias_target(self, domain: Dict[str, Any], api_domain_name: str,
626665
return alias_target
627666

628667
@cw_timer(prefix="Generator", name="Api")
629-
def to_cloudformation(self, redeploy_restapi_parameters, route53_record_set_groups): # type: ignore[no-untyped-def]
668+
def to_cloudformation(
669+
self, redeploy_restapi_parameters: Optional[Any], route53_record_set_groups: Dict[str, Route53RecordSetGroup]
670+
) -> List[Resource]:
630671
"""Generates CloudFormation resources from a SAM API resource
631672
632673
:returns: a tuple containing the RestApi, Deployment, and Stage for an empty Api.
633674
:rtype: tuple
634675
"""
635676
rest_api = self._construct_rest_api()
636-
domain, basepath_mapping, route53 = self._construct_api_domain(rest_api, route53_record_set_groups)
677+
api_domain_response = self._construct_api_domain(rest_api, route53_record_set_groups)
678+
domain = api_domain_response.domain
679+
basepath_mapping = api_domain_response.apigw_basepath_mapping_list
680+
route53_recordsetGroup = api_domain_response.recordset_group
681+
637682
deployment = self._construct_deployment(rest_api)
638683

639684
swagger = None
@@ -646,7 +691,41 @@ def to_cloudformation(self, redeploy_restapi_parameters, route53_record_set_grou
646691
permissions = self._construct_authorizer_lambda_permission()
647692
usage_plan = self._construct_usage_plan(rest_api_stage=stage)
648693

649-
return rest_api, deployment, stage, permissions, domain, basepath_mapping, route53, usage_plan
694+
# mypy complains if the type in List doesn't match exactly
695+
# TODO: refactor to have a list of single resource
696+
generated_resources: List[
697+
Union[
698+
Optional[Resource],
699+
List[Resource],
700+
Tuple[Resource],
701+
List[LambdaPermission],
702+
List[ApiGatewayBasePathMapping],
703+
],
704+
] = []
705+
706+
generated_resources.extend(
707+
[
708+
rest_api,
709+
deployment,
710+
stage,
711+
permissions,
712+
domain,
713+
basepath_mapping,
714+
route53_recordsetGroup,
715+
usage_plan,
716+
]
717+
)
718+
719+
# Make a list of single resources
720+
generated_resources_list: List[Resource] = []
721+
for resource in generated_resources:
722+
if resource:
723+
if isinstance(resource, (list, tuple)):
724+
generated_resources_list.extend(resource)
725+
else:
726+
generated_resources_list.extend([resource])
727+
728+
return generated_resources_list
650729

651730
def _add_cors(self) -> None:
652731
"""

samtranslator/model/sam_resources.py

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,15 +1220,14 @@ class SamApi(SamResourceMacro):
12201220
}
12211221

12221222
@cw_timer
1223-
def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]
1223+
def to_cloudformation(self, **kwargs) -> List[Resource]: # type: ignore[no-untyped-def]
12241224
"""Returns the API Gateway RestApi, Deployment, and Stage to which this SAM Api corresponds.
12251225
12261226
:param dict kwargs: already-converted resources that may need to be modified when converting this \
12271227
macro to pure CloudFormation
12281228
:returns: a list of vanilla CloudFormation Resources, to which this Function expands
12291229
:rtype: list
12301230
"""
1231-
resources = []
12321231

12331232
intrinsics_resolver = kwargs["intrinsics_resolver"]
12341233
self.BinaryMediaTypes = intrinsics_resolver.resolve_parameter_refs(self.BinaryMediaTypes)
@@ -1276,29 +1275,7 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]
12761275
always_deploy=self.AlwaysDeploy,
12771276
)
12781277

1279-
(
1280-
rest_api,
1281-
deployment,
1282-
stage,
1283-
permissions,
1284-
domain,
1285-
basepath_mapping,
1286-
route53,
1287-
usage_plan_resources,
1288-
) = api_generator.to_cloudformation(redeploy_restapi_parameters, route53_record_set_groups)
1289-
1290-
resources.extend([rest_api, deployment, stage])
1291-
resources.extend(permissions)
1292-
if domain:
1293-
resources.extend([domain])
1294-
if basepath_mapping:
1295-
resources.extend(basepath_mapping)
1296-
if route53:
1297-
resources.extend([route53])
1298-
# contains usage plan, api key and usageplan key resources
1299-
if usage_plan_resources:
1300-
resources.extend(usage_plan_resources)
1301-
return resources
1278+
return api_generator.to_cloudformation(redeploy_restapi_parameters, route53_record_set_groups)
13021279

13031280

13041281
class SamHttpApi(SamResourceMacro):

samtranslator/schema/schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198371,6 +198371,10 @@
198371198371
"Region": {
198372198372
"$ref": "#/definitions/PassThroughProp"
198373198373
},
198374+
"SeparateRecordSetGroup": {
198375+
"title": "Separaterecordsetgroup",
198376+
"type": "boolean"
198377+
},
198374198378
"SetIdentifier": {
198375198379
"$ref": "#/definitions/PassThroughProp"
198376198380
}

schema_source/sam.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4094,6 +4094,10 @@
40944094
"Region": {
40954095
"$ref": "#/definitions/PassThroughProp"
40964096
},
4097+
"SeparateRecordSetGroup": {
4098+
"title": "Separaterecordsetgroup",
4099+
"type": "boolean"
4100+
},
40974101
"SetIdentifier": {
40984102
"$ref": "#/definitions/PassThroughProp"
40994103
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: >
4+
apigateway-2402
5+
6+
Sample SAM Template for apigateway-2402
7+
8+
Parameters:
9+
EnvType:
10+
Description: Environment type.
11+
Default: test
12+
Type: String
13+
AllowedValues:
14+
- prod
15+
- test
16+
ConstraintDescription: must specify prod or test.
17+
Conditions:
18+
CreateProdResources: !Equals
19+
- !Ref EnvType
20+
- prod
21+
Resources:
22+
ApiGatewayAdminOne:
23+
Type: AWS::Serverless::Api
24+
Properties:
25+
Name: App-Prod-Web
26+
StageName: Prod
27+
TracingEnabled: true
28+
MethodSettings:
29+
- LoggingLevel: Info
30+
ResourcePath: /*
31+
HttpMethod: '*'
32+
Domain:
33+
DomainName: admin.one.amazon.com
34+
CertificateArn: arn::cert::abc
35+
EndpointConfiguration: REGIONAL
36+
Route53:
37+
HostedZoneId: abc123456
38+
EndpointConfiguration:
39+
Type: REGIONAL
40+
41+
42+
ApiGatewayAdminTwo:
43+
Type: AWS::Serverless::Api
44+
Condition: CreateProdResources
45+
Properties:
46+
Name: App-Prod-Web
47+
StageName: Prod
48+
TracingEnabled: true
49+
MethodSettings:
50+
- LoggingLevel: Info
51+
ResourcePath: /*
52+
HttpMethod: '*'
53+
Domain:
54+
DomainName: admin.two.amazon.com
55+
CertificateArn: arn::cert::abc
56+
EndpointConfiguration: REGIONAL
57+
Route53:
58+
HostedZoneId: abc123456
59+
SeparateRecordSetGroup: [true]
60+
EndpointConfiguration:
61+
Type: REGIONAL
62+
63+
64+
ApiGatewayAdminThree:
65+
Type: AWS::Serverless::Api
66+
Properties:
67+
Name: App-Prod-Web
68+
StageName: Prod
69+
TracingEnabled: true
70+
MethodSettings:
71+
- LoggingLevel: Info
72+
ResourcePath: /*
73+
HttpMethod: '*'
74+
Domain:
75+
DomainName: admin.three.amazon.com
76+
CertificateArn: arn::cert::abc
77+
EndpointConfiguration: REGIONAL
78+
Route53:
79+
HostedZoneId: abc123456
80+
SeparateRecordSetGroup: true
81+
EndpointConfiguration:
82+
Type: REGIONAL

0 commit comments

Comments
 (0)