Skip to content
Merged
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Here are some questions that you should answer in your plan:

We welcome you to use the GitHub issue tracker to report bugs or suggest features.

When filing an issue, please check [existing open](https://github.com/awslabs/PRIVATE-aws-sam-development/issues), or [recently closed](https://github.com/awslabs/PRIVATE-aws-sam-development/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already
When filing an issue, please check [existing open](https://github.com/awslabs/serverless-application-model/issues), or [recently closed](https://github.com/awslabs/serverless-application-model/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already
reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:

- A reproducible test case or series of steps
Expand Down
1 change: 1 addition & 0 deletions docs/cloudformation_compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ MinimumCompressionSize All
Cors All
TracingEnabled All
OpenApiVersion None
Domain All
================================== ======================== ========================


Expand Down
1 change: 1 addition & 0 deletions docs/globals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Currently, the following resources and properties are being supported:
CanarySetting:
TracingEnabled:
OpenApiVersion:
Domain:

SimpleTable:
# Properties of AWS::Serverless::SimpleTable
Expand Down
19 changes: 19 additions & 0 deletions examples/2016-10-31/custom_domains_without_route53/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Custom Domains support

Example SAM template for setting up Api Gateway resources for custom domains.

## Prerequisites for setting up custom domains
1. A domain name. You can purchase a domain name from a domain name provider.
1. A certificate ARN. Set up or import a valid certificate into AWS Certificate Manager.

## PostRequisites
After deploying the template, make sure you configure the DNS settings on the domain name provider's website. You will need to add Type A and Type AAAA DNS records that are point to ApiGateway's Hosted Zone Id. Read more [here](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-api-gateway.html)

## Running the example

```bash
$ sam deploy \
--template-file /path_to_template/packaged-template.yaml \
--stack-name my-new-stack \
--capabilities CAPABILITY_IAM
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Parameters:
MyDomainName:
Type: String
Default: another-example.com

MyDomainCert:
Type: String
Default: another-api-arn

Globals:
Api:
Domain:
DomainName: !Ref MyDomainName
CertificateArn: !Ref MyDomainCert
EndpointConfiguration: 'REGIONAL'
BasePath: ['/get']

Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
Handler: index.handler
Runtime: nodejs8.10
Events:
ImplicitGet:
Type: Api
Properties:
Method: Get
Path: /get
76 changes: 72 additions & 4 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from samtranslator.model.intrinsics import ref
from samtranslator.model.apigateway import (ApiGatewayDeployment, ApiGatewayRestApi,
ApiGatewayStage, ApiGatewayAuthorizer,
ApiGatewayResponse)
ApiGatewayResponse, ApiGatewayDomainName,
ApiGatewayBasePathMapping)
from samtranslator.model.exceptions import InvalidResourceException
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
from samtranslator.region_configuration import RegionConfiguration
Expand Down Expand Up @@ -35,7 +36,7 @@ def __init__(self, logical_id, cache_cluster_enabled, cache_cluster_size, variab
method_settings=None, binary_media=None, minimum_compression_size=None, cors=None,
auth=None, gateway_responses=None, access_log_setting=None, canary_setting=None,
tracing_enabled=None, resource_attributes=None, passthrough_resource_attributes=None,
open_api_version=None, models=None):
open_api_version=None, models=None, domain=None):
"""Constructs an API Generator class that generates API Gateway resources

:param logical_id: Logical id of the SAM API Resource
Expand Down Expand Up @@ -80,6 +81,7 @@ def __init__(self, logical_id, cache_cluster_enabled, cache_cluster_size, variab
self.open_api_version = open_api_version
self.remove_extra_stage = open_api_version
self.models = models
self.domain = domain

def _construct_rest_api(self):
"""Constructs and returns the ApiGateway RestApi.
Expand Down Expand Up @@ -204,20 +206,86 @@ def _construct_stage(self, deployment, swagger):
stage.TracingEnabled = self.tracing_enabled

if swagger is not None:
deployment.make_auto_deployable(stage, self.remove_extra_stage, swagger)
deployment.make_auto_deployable(stage, self.remove_extra_stage, swagger, self.domain)

if self.tags is not None:
stage.Tags = get_tag_list(self.tags)

return stage

def _construct_api_domain(self, rest_api):
"""
Constructs and returns the ApiGateway Domain and BasepathMapping
"""
if self.domain is None:
return None, None

if self.domain.get('DomainName') is None or \
self.domain.get('CertificateArn') is None:
raise InvalidResourceException(self.logical_id,
"Custom Domains only works if both DomainName and CertificateArn"
" are provided")

logical_id = logical_id_generator.LogicalIdGenerator("", self.domain).gen()

domain = ApiGatewayDomainName('ApiGatewayDomainName' + logical_id,
attributes=self.passthrough_resource_attributes)
domain.DomainName = self.domain.get('DomainName')
endpoint = self.domain.get('EndpointConfiguration')

if endpoint is None:
endpoint = 'REGIONAL'
elif endpoint not in ['EDGE', 'REGIONAL']:
raise InvalidResourceException(self.logical_id,
"EndpointConfiguration for Custom Domains must be"
" one of {}".format(['EDGE', 'REGIONAL']))

if endpoint == 'REGIONAL':
domain.RegionalCertificateArn = self.domain.get('CertificateArn')
else:
domain.CertificateArn = self.domain.get('CertificateArn')

domain.EndpointConfiguration = {"Types": [endpoint]}

# Create BasepathMappings
if self.domain.get('BasePath') and isinstance(self.domain.get('BasePath'), string_types):
basepaths = [self.domain.get('BasePath')]
elif self.domain.get('BasePath') and isinstance(self.domain.get('BasePath'), list):
basepaths = self.domain.get('BasePath')
else:
basepaths = None

basepath_resource_list = []

if basepaths is None:
basepath_mapping = ApiGatewayBasePathMapping(self.logical_id + 'BasePathMapping',
attributes=self.passthrough_resource_attributes)
basepath_mapping.DomainName = self.domain.get('DomainName')
basepath_mapping.RestApiId = ref(rest_api.logical_id)
basepath_mapping.Stage = ref(rest_api.logical_id + '.Stage')
basepath_resource_list.extend([basepath_mapping])
else:
for path in basepaths:
path = ''.join(e for e in path if e.isalnum())
logical_id = "{}{}{}".format(self.logical_id, path, 'BasePathMapping')
basepath_mapping = ApiGatewayBasePathMapping(logical_id,
attributes=self.passthrough_resource_attributes)
basepath_mapping.DomainName = self.domain.get('DomainName')
basepath_mapping.RestApiId = ref(rest_api.logical_id)
basepath_mapping.Stage = ref(rest_api.logical_id + '.Stage')
basepath_mapping.BasePath = path
basepath_resource_list.extend([basepath_mapping])

return domain, basepath_resource_list

def to_cloudformation(self):
"""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 = self._construct_api_domain(rest_api)
deployment = self._construct_deployment(rest_api)

swagger = None
Expand All @@ -229,7 +297,7 @@ def to_cloudformation(self):
stage = self._construct_stage(deployment, swagger)
permissions = self._construct_authorizer_lambda_permission()

return rest_api, deployment, stage, permissions
return rest_api, deployment, stage, permissions, domain, basepath_mapping

def _add_cors(self):
"""
Expand Down
31 changes: 28 additions & 3 deletions samtranslator/model/apigateway.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from re import match

from samtranslator.model import PropertyType, Resource
from samtranslator.model.exceptions import InvalidResourceException
from samtranslator.model.types import is_type, one_of, is_str, list_of
Expand Down Expand Up @@ -76,12 +76,14 @@ class ApiGatewayDeployment(Resource):
"deployment_id": lambda self: ref(self.logical_id),
}

def make_auto_deployable(self, stage, openapi_version=None, swagger=None):
def make_auto_deployable(self, stage, openapi_version=None, swagger=None, domain=None):
"""
Sets up the resource such that it will trigger a re-deployment when Swagger changes
or the openapi version changes.
or the openapi version changes or a domain resource changes.

:param swagger: Dictionary containing the Swagger definition of the API
:param openapi_version: string containing value of OpenApiVersion flag in the template
:param domain: Dictionary containing the custom domain configuration for the API
"""
if not swagger:
return
Expand All @@ -95,6 +97,9 @@ def make_auto_deployable(self, stage, openapi_version=None, swagger=None):
hash_input = [str(swagger)]
if openapi_version:
hash_input.append(str(openapi_version))
if domain:
hash_input.append(self._X_HASH_DELIMITER)
hash_input.append(json.dumps(domain))

data = self._X_HASH_DELIMITER.join(hash_input)
generator = logical_id_generator.LogicalIdGenerator(self.logical_id, data)
Expand Down Expand Up @@ -153,6 +158,26 @@ def _status_code_string(self, status_code):
return None if status_code is None else str(status_code)


class ApiGatewayDomainName(Resource):
resource_type = 'AWS::ApiGateway::DomainName'
property_types = {
'RegionalCertificateArn': PropertyType(False, is_str()),
'DomainName': PropertyType(True, is_str()),
'EndpointConfiguration': PropertyType(False, is_type(dict)),
'CertificateArn': PropertyType(False, is_str())
}


class ApiGatewayBasePathMapping(Resource):
resource_type = 'AWS::ApiGateway::BasePathMapping'
property_types = {
'BasePath': PropertyType(False, is_str()),
'DomainName': PropertyType(True, is_str()),
'RestApiId': PropertyType(False, is_str()),
'Stage': PropertyType(False, is_str())
}


class ApiGatewayAuthorizer(object):
_VALID_FUNCTION_PAYLOAD_TYPES = [None, 'TOKEN', 'REQUEST']

Expand Down
14 changes: 10 additions & 4 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,8 @@ class SamApi(SamResourceMacro):
'CanarySetting': PropertyType(False, is_type(dict)),
'TracingEnabled': PropertyType(False, is_type(bool)),
'OpenApiVersion': PropertyType(False, is_str()),
'Models': PropertyType(False, is_type(dict))
'Models': PropertyType(False, is_type(dict)),
'Domain': PropertyType(False, is_type(dict))
}

referable_properties = {
Expand All @@ -502,6 +503,7 @@ def to_cloudformation(self, **kwargs):

intrinsics_resolver = kwargs["intrinsics_resolver"]
self.BinaryMediaTypes = intrinsics_resolver.resolve_parameter_refs(self.BinaryMediaTypes)
self.Domain = intrinsics_resolver.resolve_parameter_refs(self.Domain)

api_generator = ApiGenerator(self.logical_id,
self.CacheClusterEnabled,
Expand All @@ -526,13 +528,17 @@ def to_cloudformation(self, **kwargs):
resource_attributes=self.resource_attributes,
passthrough_resource_attributes=self.get_passthrough_resource_attributes(),
open_api_version=self.OpenApiVersion,
models=self.Models)
models=self.Models,
domain=self.Domain)

rest_api, deployment, stage, permissions = api_generator.to_cloudformation()
rest_api, deployment, stage, permissions, domain, basepath_mapping = api_generator.to_cloudformation()

resources.extend([rest_api, deployment, stage])
resources.extend(permissions)

if domain:
resources.extend([domain])
if basepath_mapping:
resources.extend(basepath_mapping)
return resources


Expand Down
3 changes: 2 additions & 1 deletion samtranslator/plugins/globals/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ class Globals(object):
"AccessLogSetting",
"CanarySetting",
"TracingEnabled",
"OpenApiVersion"
"OpenApiVersion",
"Domain"
],

SamResourceType.SimpleTable.value: [
Expand Down
1 change: 0 additions & 1 deletion samtranslator/plugins/globals/globals_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ def on_before_transform_template(self, template_dict):

:param dict template_dict: SAM template as a dictionary
"""

try:
global_section = Globals(template_dict)
except InvalidGlobalsSectionException as ex:
Expand Down
4 changes: 3 additions & 1 deletion samtranslator/translator/verify_logical_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
'AWS::SNS::Topic': 'AWS::SNS::Topic',
'AWS::DynamoDB::Table': 'AWS::Serverless::SimpleTable',
'AWS::CloudFormation::Stack': 'AWS::Serverless::Application',
'AWS::Cognito::UserPool': 'AWS::Cognito::UserPool'
'AWS::Cognito::UserPool': 'AWS::Cognito::UserPool',
'AWS::ApiGateway::DomainName': 'AWS::ApiGateway::DomainName',
'AWS::ApiGateway::BasePathMapping': 'AWS::ApiGateway::BasePathMapping'
}


Expand Down
Loading