Skip to content

Commit 400ca04

Browse files
authored
Fix route53 record set groups (#2408)
* Fix route53 record set groups * Fix record set group for http api * improve readability * add more tests * fix tests * Add test case for intrinsic hosted zone
1 parent 337871b commit 400ca04

22 files changed

+3532
-52
lines changed

samtranslator/model/api/api_generator.py

+21-15
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from samtranslator.swagger.swagger import SwaggerEditor
2323
from samtranslator.model.intrinsics import is_intrinsic, fnSub
2424
from samtranslator.model.lambda_ import LambdaPermission
25-
from samtranslator.translator import logical_id_generator
25+
from samtranslator.translator.logical_id_generator import LogicalIdGenerator
2626
from samtranslator.translator.arn_generator import ArnGenerator
2727
from samtranslator.model.tags.resource_tagging import get_tag_list
2828
from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr
@@ -393,7 +393,7 @@ def _construct_stage(self, deployment, swagger, redeploy_restapi_parameters):
393393
if stage_name_prefix.isalnum():
394394
stage_logical_id = self.logical_id + stage_name_prefix + "Stage"
395395
else:
396-
generator = logical_id_generator.LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix)
396+
generator = LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix)
397397
stage_logical_id = generator.gen()
398398
stage = ApiGatewayStage(stage_logical_id, attributes=self.passthrough_resource_attributes)
399399
stage.RestApiId = ref(self.logical_id)
@@ -417,7 +417,7 @@ def _construct_stage(self, deployment, swagger, redeploy_restapi_parameters):
417417

418418
return stage
419419

420-
def _construct_api_domain(self, rest_api):
420+
def _construct_api_domain(self, rest_api, route53_record_set_groups):
421421
"""
422422
Constructs and returns the ApiGateway Domain and BasepathMapping
423423
"""
@@ -430,7 +430,7 @@ def _construct_api_domain(self, rest_api):
430430
)
431431

432432
self.domain["ApiDomainName"] = "{}{}".format(
433-
"ApiGatewayDomainName", logical_id_generator.LogicalIdGenerator("", self.domain.get("DomainName")).gen()
433+
"ApiGatewayDomainName", LogicalIdGenerator("", self.domain.get("DomainName")).gen()
434434
)
435435

436436
domain = ApiGatewayDomainName(self.domain.get("ApiDomainName"), attributes=self.passthrough_resource_attributes)
@@ -534,17 +534,23 @@ def _construct_api_domain(self, rest_api):
534534
self.logical_id,
535535
"HostedZoneId or HostedZoneName is required to enable Route53 support on Custom Domains.",
536536
)
537-
logical_id = logical_id_generator.LogicalIdGenerator(
537+
538+
logical_id_suffix = LogicalIdGenerator(
538539
"", route53.get("HostedZoneId") or route53.get("HostedZoneName")
539540
).gen()
540-
record_set_group = Route53RecordSetGroup(
541-
"RecordSetGroup" + logical_id, attributes=self.passthrough_resource_attributes
542-
)
543-
if "HostedZoneId" in route53:
544-
record_set_group.HostedZoneId = route53.get("HostedZoneId")
545-
if "HostedZoneName" in route53:
546-
record_set_group.HostedZoneName = route53.get("HostedZoneName")
547-
record_set_group.RecordSets = self._construct_record_sets_for_domain(self.domain)
541+
logical_id = "RecordSetGroup" + logical_id_suffix
542+
543+
record_set_group = route53_record_set_groups.get(logical_id)
544+
if not record_set_group:
545+
record_set_group = Route53RecordSetGroup(logical_id, attributes=self.passthrough_resource_attributes)
546+
if "HostedZoneId" in route53:
547+
record_set_group.HostedZoneId = route53.get("HostedZoneId")
548+
if "HostedZoneName" in route53:
549+
record_set_group.HostedZoneName = route53.get("HostedZoneName")
550+
record_set_group.RecordSets = []
551+
route53_record_set_groups[logical_id] = record_set_group
552+
553+
record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain)
548554

549555
return domain, basepath_resource_list, record_set_group
550556

@@ -585,14 +591,14 @@ def _construct_alias_target(self, domain):
585591
return alias_target
586592

587593
@cw_timer(prefix="Generator", name="Api")
588-
def to_cloudformation(self, redeploy_restapi_parameters):
594+
def to_cloudformation(self, redeploy_restapi_parameters, route53_record_set_groups):
589595
"""Generates CloudFormation resources from a SAM API resource
590596
591597
:returns: a tuple containing the RestApi, Deployment, and Stage for an empty Api.
592598
:rtype: tuple
593599
"""
594600
rest_api = self._construct_rest_api()
595-
domain, basepath_mapping, route53 = self._construct_api_domain(rest_api)
601+
domain, basepath_mapping, route53 = self._construct_api_domain(rest_api, route53_record_set_groups)
596602
deployment = self._construct_deployment(rest_api)
597603

598604
swagger = None

samtranslator/model/api/http_api_generator.py

+31-22
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from samtranslator.model.exceptions import InvalidResourceException
1414
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
1515
from samtranslator.open_api.open_api import OpenApiEditor
16-
from samtranslator.translator import logical_id_generator
16+
from samtranslator.translator.logical_id_generator import LogicalIdGenerator
1717
from samtranslator.model.tags.resource_tagging import get_tag_list
1818
from samtranslator.model.intrinsics import is_intrinsic, is_intrinsic_no_value
1919
from samtranslator.model.route53 import Route53RecordSetGroup
@@ -218,7 +218,7 @@ def _add_cors(self):
218218
# Assign the OpenApi back to template
219219
self.definition_body = editor.openapi
220220

221-
def _construct_api_domain(self, http_api):
221+
def _construct_api_domain(self, http_api, route53_record_set_groups):
222222
"""
223223
Constructs and returns the ApiGateway Domain and BasepathMapping
224224
"""
@@ -231,7 +231,7 @@ def _construct_api_domain(self, http_api):
231231
)
232232

233233
self.domain["ApiDomainName"] = "{}{}".format(
234-
"ApiGatewayDomainNameV2", logical_id_generator.LogicalIdGenerator("", self.domain.get("DomainName")).gen()
234+
"ApiGatewayDomainNameV2", LogicalIdGenerator("", self.domain.get("DomainName")).gen()
235235
)
236236

237237
domain = ApiGatewayV2DomainName(
@@ -302,31 +302,40 @@ def _construct_api_domain(self, http_api):
302302
basepath_resource_list = self._construct_basepath_mappings(basepaths, http_api)
303303

304304
# Create the Route53 RecordSetGroup resource
305-
record_set_group = self._construct_route53_recordsetgroup()
305+
record_set_group = self._construct_route53_recordsetgroup(route53_record_set_groups)
306306

307307
return domain, basepath_resource_list, record_set_group
308308

309-
def _construct_route53_recordsetgroup(self):
310-
record_set_group = None
311-
if self.domain.get("Route53") is not None:
312-
route53 = self.domain.get("Route53")
313-
if route53.get("HostedZoneId") is None and route53.get("HostedZoneName") is None:
314-
raise InvalidResourceException(
315-
self.logical_id,
316-
"HostedZoneId or HostedZoneName is required to enable Route53 support on Custom Domains.",
317-
)
318-
logical_id = logical_id_generator.LogicalIdGenerator(
319-
"", route53.get("HostedZoneId") or route53.get("HostedZoneName")
320-
).gen()
321-
record_set_group = Route53RecordSetGroup(
322-
"RecordSetGroup" + logical_id, attributes=self.passthrough_resource_attributes
309+
def _construct_route53_recordsetgroup(self, route53_record_set_groups):
310+
if self.domain.get("Route53") is None:
311+
return
312+
route53 = self.domain.get("Route53")
313+
if not isinstance(route53, dict):
314+
raise InvalidResourceException(
315+
self.logical_id,
316+
"Invalid property type '{}' for Route53. "
317+
"Expected a map defines an Amazon Route 53 configuration'.".format(type(route53).__name__),
323318
)
319+
if route53.get("HostedZoneId") is None and route53.get("HostedZoneName") is None:
320+
raise InvalidResourceException(
321+
self.logical_id,
322+
"HostedZoneId or HostedZoneName is required to enable Route53 support on Custom Domains.",
323+
)
324+
325+
logical_id_suffix = LogicalIdGenerator("", route53.get("HostedZoneId") or route53.get("HostedZoneName")).gen()
326+
logical_id = "RecordSetGroup" + logical_id_suffix
327+
328+
record_set_group = route53_record_set_groups.get(logical_id)
329+
if not record_set_group:
330+
record_set_group = Route53RecordSetGroup(logical_id, attributes=self.passthrough_resource_attributes)
324331
if "HostedZoneId" in route53:
325332
record_set_group.HostedZoneId = route53.get("HostedZoneId")
326333
elif "HostedZoneName" in route53:
327334
record_set_group.HostedZoneName = route53.get("HostedZoneName")
328-
record_set_group.RecordSets = self._construct_record_sets_for_domain(self.domain)
335+
record_set_group.RecordSets = []
336+
route53_record_set_groups[logical_id] = record_set_group
329337

338+
record_set_group.RecordSets += self._construct_record_sets_for_domain(self.domain)
330339
return record_set_group
331340

332341
def _construct_basepath_mappings(self, basepaths, http_api):
@@ -592,7 +601,7 @@ def _construct_stage(self):
592601
elif stage_name_prefix == DefaultStageName:
593602
stage_logical_id = self.logical_id + "ApiGatewayDefaultStage"
594603
else:
595-
generator = logical_id_generator.LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix)
604+
generator = LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix)
596605
stage_logical_id = generator.gen()
597606
stage = ApiGatewayV2Stage(stage_logical_id, attributes=self.passthrough_resource_attributes)
598607
stage.ApiId = ref(self.logical_id)
@@ -628,14 +637,14 @@ def _add_description(self):
628637
self.definition_body = open_api_editor.openapi
629638

630639
@cw_timer(prefix="Generator", name="HttpApi")
631-
def to_cloudformation(self):
640+
def to_cloudformation(self, route53_record_set_groups):
632641
"""Generates CloudFormation resources from a SAM HTTP API resource
633642
634643
:returns: a tuple containing the HttpApi and Stage for an empty Api.
635644
:rtype: tuple
636645
"""
637646
http_api = self._construct_http_api()
638-
domain, basepath_mapping, route53 = self._construct_api_domain(http_api)
647+
domain, basepath_mapping, route53 = self._construct_api_domain(http_api, route53_record_set_groups)
639648
stage = self._construct_stage()
640649

641650
return http_api, stage, domain, basepath_mapping, route53

samtranslator/model/sam_resources.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,7 @@ def to_cloudformation(self, **kwargs):
11071107
redeploy_restapi_parameters = kwargs.get("redeploy_restapi_parameters")
11081108
shared_api_usage_plan = kwargs.get("shared_api_usage_plan")
11091109
template_conditions = kwargs.get("conditions")
1110+
route53_record_set_groups = kwargs.get("route53_record_set_groups", {})
11101111

11111112
api_generator = ApiGenerator(
11121113
self.logical_id,
@@ -1152,7 +1153,7 @@ def to_cloudformation(self, **kwargs):
11521153
basepath_mapping,
11531154
route53,
11541155
usage_plan_resources,
1155-
) = api_generator.to_cloudformation(redeploy_restapi_parameters)
1156+
) = api_generator.to_cloudformation(redeploy_restapi_parameters, route53_record_set_groups)
11561157

11571158
resources.extend([rest_api, deployment, stage])
11581159
resources.extend(permissions)
@@ -1212,8 +1213,6 @@ def to_cloudformation(self, **kwargs):
12121213
resources = []
12131214
intrinsics_resolver = kwargs["intrinsics_resolver"]
12141215
self.CorsConfiguration = intrinsics_resolver.resolve_parameter_refs(self.CorsConfiguration)
1215-
1216-
intrinsics_resolver = kwargs["intrinsics_resolver"]
12171216
self.Domain = intrinsics_resolver.resolve_parameter_refs(self.Domain)
12181217

12191218
api_generator = HttpApiGenerator(
@@ -1243,7 +1242,7 @@ def to_cloudformation(self, **kwargs):
12431242
domain,
12441243
basepath_mapping,
12451244
route53,
1246-
) = api_generator.to_cloudformation()
1245+
) = api_generator.to_cloudformation(kwargs.get("route53_record_set_groups", {}))
12471246

12481247
resources.append(http_api)
12491248
if domain:

samtranslator/translator/translator.py

+2
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def translate(self, sam_template, parameter_values, feature_toggle=None, passthr
125125
shared_api_usage_plan = SharedApiUsagePlan()
126126
document_errors = []
127127
changed_logical_ids = {}
128+
route53_record_set_groups = {}
128129
for logical_id, resource_dict in self._get_resources_to_iterate(sam_template, macro_resolver):
129130
try:
130131
macro = macro_resolver.resolve_resource_type(resource_dict).from_dict(
@@ -144,6 +145,7 @@ def translate(self, sam_template, parameter_values, feature_toggle=None, passthr
144145
kwargs["redeploy_restapi_parameters"] = self.redeploy_restapi_parameters
145146
kwargs["shared_api_usage_plan"] = shared_api_usage_plan
146147
kwargs["feature_toggle"] = self.feature_toggle
148+
kwargs["route53_record_set_groups"] = route53_record_set_groups
147149
translated = macro.to_cloudformation(**kwargs)
148150

149151
supported_resource_refs = macro.get_resource_references(translated, supported_resource_refs)

tests/model/api/test_http_api_generator.py

+24-11
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,14 @@ class TestCustomDomains(TestCase):
216216
"passthrough_resource_attributes": None,
217217
"domain": None,
218218
}
219+
route53_record_set_groups = {}
219220

220221
def test_no_domain(self):
221222
self.kwargs["domain"] = None
222223
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
223-
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
224+
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(
225+
http_api, self.route53_record_set_groups
226+
)
224227
self.assertIsNone(domain)
225228
self.assertIsNone(basepath)
226229
self.assertIsNone(route)
@@ -229,7 +232,7 @@ def test_no_domain_name(self):
229232
self.kwargs["domain"] = {"CertificateArn": "someurl"}
230233
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
231234
with pytest.raises(InvalidResourceException) as e:
232-
HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
235+
HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api, self.route53_record_set_groups)
233236
self.assertEqual(
234237
e.value.message,
235238
"Resource with id [HttpApiId] is invalid. "
@@ -240,7 +243,7 @@ def test_no_cert_arn(self):
240243
self.kwargs["domain"] = {"DomainName": "example.com"}
241244
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
242245
with pytest.raises(InvalidResourceException) as e:
243-
HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
246+
HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api, self.route53_record_set_groups)
244247
self.assertEqual(
245248
e.value.message,
246249
"Resource with id [HttpApiId] is invalid. "
@@ -250,7 +253,9 @@ def test_no_cert_arn(self):
250253
def test_basic_domain_default_endpoint(self):
251254
self.kwargs["domain"] = {"DomainName": "example.com", "CertificateArn": "some-url"}
252255
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
253-
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
256+
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(
257+
http_api, self.route53_record_set_groups
258+
)
254259
self.assertIsNotNone(domain, None)
255260
self.assertIsNotNone(basepath, None)
256261
self.assertEqual(len(basepath), 1)
@@ -264,7 +269,9 @@ def test_basic_domain_regional_endpoint(self):
264269
"EndpointConfiguration": "REGIONAL",
265270
}
266271
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
267-
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
272+
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(
273+
http_api, self.route53_record_set_groups
274+
)
268275
self.assertIsNotNone(domain, None)
269276
self.assertIsNotNone(basepath, None)
270277
self.assertEqual(len(basepath), 1)
@@ -279,7 +286,7 @@ def test_basic_domain_edge_endpoint(self):
279286
}
280287
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
281288
with pytest.raises(InvalidResourceException) as e:
282-
HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
289+
HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api, self.route53_record_set_groups)
283290
self.assertEqual(
284291
e.value.message,
285292
"Resource with id [HttpApiId] is invalid. EndpointConfiguration for Custom Domains must be one of ['REGIONAL'].",
@@ -293,7 +300,7 @@ def test_bad_endpoint(self):
293300
}
294301
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
295302
with pytest.raises(InvalidResourceException) as e:
296-
HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
303+
HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api, self.route53_record_set_groups)
297304
self.assertEqual(
298305
e.value.message,
299306
"Resource with id [HttpApiId] is invalid. "
@@ -307,7 +314,9 @@ def test_basic_route53(self):
307314
"Route53": {"HostedZoneId": "xyz"},
308315
}
309316
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
310-
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
317+
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(
318+
http_api, self.route53_record_set_groups
319+
)
311320
self.assertIsNotNone(domain, None)
312321
self.assertIsNotNone(basepath, None)
313322
self.assertEqual(len(basepath), 1)
@@ -322,7 +331,9 @@ def test_basepaths(self):
322331
"Route53": {"HostedZoneId": "xyz"},
323332
}
324333
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
325-
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
334+
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(
335+
http_api, self.route53_record_set_groups
336+
)
326337
self.assertIsNotNone(domain, None)
327338
self.assertIsNotNone(basepath, None)
328339
self.assertEqual(len(basepath), 3)
@@ -338,7 +349,7 @@ def test_invalid_basepaths(self):
338349
}
339350
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
340351
with pytest.raises(InvalidResourceException) as e:
341-
HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
352+
HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api, self.route53_record_set_groups)
342353
self.assertEqual(
343354
e.value.message, "Resource with id [HttpApiId] is invalid. " + "Invalid Basepath name provided."
344355
)
@@ -351,7 +362,9 @@ def test_basepaths(self):
351362
"Route53": {"HostedZoneId": "xyz", "HostedZoneName": "abc", "IpV6": True},
352363
}
353364
http_api = HttpApiGenerator(**self.kwargs)._construct_http_api()
354-
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(http_api)
365+
domain, basepath, route = HttpApiGenerator(**self.kwargs)._construct_api_domain(
366+
http_api, self.route53_record_set_groups
367+
)
355368
self.assertIsNotNone(domain, None)
356369
self.assertIsNotNone(basepath, None)
357370
self.assertEqual(len(basepath), 8)

0 commit comments

Comments
 (0)