Skip to content

Commit

Permalink
feat: Add a new property SeparateRecordSetGroup to disable merging …
Browse files Browse the repository at this point in the history
…into record set group (#2993)
  • Loading branch information
xazhao authored Mar 16, 2023
1 parent 7aa03ec commit 2fa9718
Show file tree
Hide file tree
Showing 11 changed files with 1,330 additions and 36 deletions.
1 change: 1 addition & 0 deletions samtranslator/internal/schema_source/aws_serverless_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class Route53(BaseModel):
IpV6: Optional[bool] = route53("IpV6")
SetIdentifier: Optional[PassThroughProp] # TODO: add docs
Region: Optional[PassThroughProp] # TODO: add docs
SeparateRecordSetGroup: Optional[bool] # TODO: add docs


class Domain(BaseModel):
Expand Down
101 changes: 90 additions & 11 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import logging
from collections import namedtuple
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast

from samtranslator.metrics.method_decorator import cw_timer
from samtranslator.model import Resource
from samtranslator.model.apigateway import (
ApiGatewayApiKey,
ApiGatewayAuthorizer,
Expand Down Expand Up @@ -67,6 +69,13 @@
GatewayResponseProperties = ["ResponseParameters", "ResponseTemplates", "StatusCode"]


@dataclass
class ApiDomainResponse:
domain: Optional[ApiGatewayDomainName]
apigw_basepath_mapping_list: Optional[List[ApiGatewayBasePathMapping]]
recordset_group: Any


class SharedApiUsagePlan:
"""
Collects API information from different API resources in the same template,
Expand Down Expand Up @@ -443,12 +452,12 @@ def _construct_stage(

def _construct_api_domain( # noqa: too-many-branches
self, rest_api: ApiGatewayRestApi, route53_record_set_groups: Any
) -> Tuple[Optional[ApiGatewayDomainName], Optional[List[ApiGatewayBasePathMapping]], Any]:
) -> ApiDomainResponse:
"""
Constructs and returns the ApiGateway Domain and BasepathMapping
"""
if self.domain is None:
return None, None, None
return ApiDomainResponse(None, None, None)

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

record_set_group = route53_record_set_groups.get(logical_id)

if route53.get("SeparateRecordSetGroup"):
sam_expect(
route53.get("SeparateRecordSetGroup"), self.logical_id, "Domain.Route53.SeparateRecordSetGroup"
).to_be_a_bool()
return ApiDomainResponse(
domain,
basepath_resource_list,
self._construct_single_record_set_group(self.domain, api_domain_name, route53),
)

if not record_set_group:
record_set_group = Route53RecordSetGroup(logical_id, attributes=self.passthrough_resource_attributes)
if "HostedZoneId" in route53:
Expand All @@ -576,27 +596,46 @@ def _construct_api_domain( # noqa: too-many-branches

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

return domain, basepath_resource_list, record_set_group
return ApiDomainResponse(domain, basepath_resource_list, record_set_group)

def _construct_single_record_set_group(
self, domain: Dict[str, Any], api_domain_name: str, route53: Any
) -> Route53RecordSetGroup:
hostedZoneId = route53.get("HostedZoneId")
hostedZoneName = route53.get("HostedZoneName")
domainName = domain.get("DomainName")
logical_id = logical_id = LogicalIdGenerator(
"RecordSetGroup", [hostedZoneId or hostedZoneName, domainName]
).gen()

record_set_group = Route53RecordSetGroup(logical_id, attributes=self.passthrough_resource_attributes)
if hostedZoneId:
record_set_group.HostedZoneId = hostedZoneId
if hostedZoneName:
record_set_group.HostedZoneName = hostedZoneName

record_set_group.RecordSets = []
record_set_group.RecordSets += self._construct_record_sets_for_domain(domain, api_domain_name, route53)

return record_set_group

def _construct_record_sets_for_domain(
self, custom_domain_config: Dict[str, Any], api_domain_name: str, route53_config: Dict[str, Any]
) -> List[Dict[str, Any]]:
recordset_list = []

alias_target = self._construct_alias_target(custom_domain_config, api_domain_name, route53_config)
recordset = {}
recordset["Name"] = custom_domain_config.get("DomainName")
recordset["Type"] = "A"
recordset["AliasTarget"] = self._construct_alias_target(custom_domain_config, api_domain_name, route53_config)
recordset["AliasTarget"] = alias_target
self._update_route53_routing_policy_properties(route53_config, recordset)
recordset_list.append(recordset)

if route53_config.get("IpV6") is not None and route53_config.get("IpV6") is True:
recordset_ipv6 = {}
recordset_ipv6["Name"] = custom_domain_config.get("DomainName")
recordset_ipv6["Type"] = "AAAA"
recordset_ipv6["AliasTarget"] = self._construct_alias_target(
custom_domain_config, api_domain_name, route53_config
)
recordset_ipv6["AliasTarget"] = alias_target
self._update_route53_routing_policy_properties(route53_config, recordset_ipv6)
recordset_list.append(recordset_ipv6)

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

@cw_timer(prefix="Generator", name="Api")
def to_cloudformation(self, redeploy_restapi_parameters, route53_record_set_groups): # type: ignore[no-untyped-def]
def to_cloudformation(
self, redeploy_restapi_parameters: Optional[Any], route53_record_set_groups: Dict[str, Route53RecordSetGroup]
) -> List[Resource]:
"""Generates CloudFormation resources from a SAM API resource
:returns: a tuple containing the RestApi, Deployment, and Stage for an empty Api.
:rtype: tuple
"""
rest_api = self._construct_rest_api()
domain, basepath_mapping, route53 = self._construct_api_domain(rest_api, route53_record_set_groups)
api_domain_response = self._construct_api_domain(rest_api, route53_record_set_groups)
domain = api_domain_response.domain
basepath_mapping = api_domain_response.apigw_basepath_mapping_list
route53_recordsetGroup = api_domain_response.recordset_group

deployment = self._construct_deployment(rest_api)

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

return rest_api, deployment, stage, permissions, domain, basepath_mapping, route53, usage_plan
# mypy complains if the type in List doesn't match exactly
# TODO: refactor to have a list of single resource
generated_resources: List[
Union[
Optional[Resource],
List[Resource],
Tuple[Resource],
List[LambdaPermission],
List[ApiGatewayBasePathMapping],
],
] = []

generated_resources.extend(
[
rest_api,
deployment,
stage,
permissions,
domain,
basepath_mapping,
route53_recordsetGroup,
usage_plan,
]
)

# Make a list of single resources
generated_resources_list: List[Resource] = []
for resource in generated_resources:
if resource:
if isinstance(resource, (list, tuple)):
generated_resources_list.extend(resource)
else:
generated_resources_list.extend([resource])

return generated_resources_list

def _add_cors(self) -> None:
"""
Expand Down
27 changes: 2 additions & 25 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -1220,15 +1220,14 @@ class SamApi(SamResourceMacro):
}

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

intrinsics_resolver = kwargs["intrinsics_resolver"]
self.BinaryMediaTypes = intrinsics_resolver.resolve_parameter_refs(self.BinaryMediaTypes)
Expand Down Expand Up @@ -1276,29 +1275,7 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]
always_deploy=self.AlwaysDeploy,
)

(
rest_api,
deployment,
stage,
permissions,
domain,
basepath_mapping,
route53,
usage_plan_resources,
) = api_generator.to_cloudformation(redeploy_restapi_parameters, route53_record_set_groups)

resources.extend([rest_api, deployment, stage])
resources.extend(permissions)
if domain:
resources.extend([domain])
if basepath_mapping:
resources.extend(basepath_mapping)
if route53:
resources.extend([route53])
# contains usage plan, api key and usageplan key resources
if usage_plan_resources:
resources.extend(usage_plan_resources)
return resources
return api_generator.to_cloudformation(redeploy_restapi_parameters, route53_record_set_groups)


class SamHttpApi(SamResourceMacro):
Expand Down
4 changes: 4 additions & 0 deletions samtranslator/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -198371,6 +198371,10 @@
"Region": {
"$ref": "#/definitions/PassThroughProp"
},
"SeparateRecordSetGroup": {
"title": "Separaterecordsetgroup",
"type": "boolean"
},
"SetIdentifier": {
"$ref": "#/definitions/PassThroughProp"
}
Expand Down
4 changes: 4 additions & 0 deletions schema_source/sam.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4094,6 +4094,10 @@
"Region": {
"$ref": "#/definitions/PassThroughProp"
},
"SeparateRecordSetGroup": {
"title": "Separaterecordsetgroup",
"type": "boolean"
},
"SetIdentifier": {
"$ref": "#/definitions/PassThroughProp"
}
Expand Down
82 changes: 82 additions & 0 deletions tests/translator/input/error_separate_route53_recordset_group.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
apigateway-2402
Sample SAM Template for apigateway-2402
Parameters:
EnvType:
Description: Environment type.
Default: test
Type: String
AllowedValues:
- prod
- test
ConstraintDescription: must specify prod or test.
Conditions:
CreateProdResources: !Equals
- !Ref EnvType
- prod
Resources:
ApiGatewayAdminOne:
Type: AWS::Serverless::Api
Properties:
Name: App-Prod-Web
StageName: Prod
TracingEnabled: true
MethodSettings:
- LoggingLevel: Info
ResourcePath: /*
HttpMethod: '*'
Domain:
DomainName: admin.one.amazon.com
CertificateArn: arn::cert::abc
EndpointConfiguration: REGIONAL
Route53:
HostedZoneId: abc123456
EndpointConfiguration:
Type: REGIONAL


ApiGatewayAdminTwo:
Type: AWS::Serverless::Api
Condition: CreateProdResources
Properties:
Name: App-Prod-Web
StageName: Prod
TracingEnabled: true
MethodSettings:
- LoggingLevel: Info
ResourcePath: /*
HttpMethod: '*'
Domain:
DomainName: admin.two.amazon.com
CertificateArn: arn::cert::abc
EndpointConfiguration: REGIONAL
Route53:
HostedZoneId: abc123456
SeparateRecordSetGroup: [true]
EndpointConfiguration:
Type: REGIONAL


ApiGatewayAdminThree:
Type: AWS::Serverless::Api
Properties:
Name: App-Prod-Web
StageName: Prod
TracingEnabled: true
MethodSettings:
- LoggingLevel: Info
ResourcePath: /*
HttpMethod: '*'
Domain:
DomainName: admin.three.amazon.com
CertificateArn: arn::cert::abc
EndpointConfiguration: REGIONAL
Route53:
HostedZoneId: abc123456
SeparateRecordSetGroup: true
EndpointConfiguration:
Type: REGIONAL
Loading

0 comments on commit 2fa9718

Please sign in to comment.