Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a new property SeparateRecordSetGroup to disable merging into record set group #2993

Merged
merged 17 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
hoffa marked this conversation as resolved.
Show resolved Hide resolved


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
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