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
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
@@ -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):
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,
@@ -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,
@@ -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(
@@ -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:
@@ -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)

@@ -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
@@ -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:
"""
27 changes: 2 additions & 25 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
@@ -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)
@@ -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):
4 changes: 4 additions & 0 deletions samtranslator/schema/schema.json
Original file line number Diff line number Diff line change
@@ -198371,6 +198371,10 @@
"Region": {
"$ref": "#/definitions/PassThroughProp"
},
"SeparateRecordSetGroup": {
"title": "Separaterecordsetgroup",
"type": "boolean"
},
"SetIdentifier": {
"$ref": "#/definitions/PassThroughProp"
}
4 changes: 4 additions & 0 deletions schema_source/sam.schema.json
Original file line number Diff line number Diff line change
@@ -4094,6 +4094,10 @@
"Region": {
"$ref": "#/definitions/PassThroughProp"
},
"SeparateRecordSetGroup": {
"title": "Separaterecordsetgroup",
"type": "boolean"
},
"SetIdentifier": {
"$ref": "#/definitions/PassThroughProp"
}
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