Skip to content

Commit a9388b6

Browse files
authored
chore: merge pull request #1291 from awslabs/release/v1.19.0
chore: merge release/v1.19.0 into master
2 parents 818b4ef + 78cd53c commit a9388b6

File tree

88 files changed

+12172
-282
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+12172
-282
lines changed

docs/globals.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ Currently, the following resources and properties are being supported:
9090
TracingEnabled:
9191
OpenApiVersion:
9292
93+
HttpApi:
94+
# Properties of AWS::Serverless::HttpApi
95+
# Also works with Implicit APIs
96+
Auth:
97+
9398
SimpleTable:
9499
# Properties of AWS::Serverless::SimpleTable
95100
SSESpecification:
@@ -119,6 +124,12 @@ issues.
119124
* StageName
120125
* DefinitionBody
121126

127+
**AWS::Serverless::HttpApi:**
128+
129+
* StageName
130+
* DefinitionBody
131+
* DefinitionUri
132+
122133
Overridable
123134
-----------
124135

docs/internals/generated_resources.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,40 @@ AWS::Lambda::Permission MyFunction\ **ThumbnailApi**\ Permission\ **P
128128

129129
NOTE: ``ServerlessRestApi*`` resources are generated one per stack.
130130

131+
HTTP API
132+
^^^
133+
This is called an "Implicit HTTP API". There can be many functions in the template that define these APIs. Behind the
134+
scenes, SAM will collect all implicit HTTP APIs from all Functions in the template, generate an OpenApi doc, and create an
135+
implicit ``AWS::Serverless::HttpApi`` using this OpenApi. This API defaults to a StageName called "$default" that cannot be
136+
configured.
137+
138+
.. code:: yaml
139+
140+
MyFunction:
141+
Type: AWS::Serverless::Function
142+
Properties:
143+
...
144+
Events:
145+
ThumbnailApi:
146+
Type: HttpApi
147+
Properties:
148+
Path: /thumbnail
149+
Method: GET
150+
...
151+
152+
Additional generated resources:
153+
154+
================================== ================================
155+
CloudFormation Resource Type Logical ID
156+
================================== ================================
157+
AWS::ApiGatewayV2::Api *ServerlessHttpApi*
158+
AWS::ApiGateway::Stage *ServerlessHttpApiApiGatewayDefaultStage*
159+
AWS::Lambda::Permission MyFunction\ **ThumbnailApi**\ Permission
160+
================================== ================================
161+
162+
163+
NOTE: ``ServerlessHttpApi*`` resources are generated one per stack.
164+
131165
Cognito
132166
^^^
133167

examples/2016-10-31/cloudwatch-event-to-msteams/template.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Resources:
4545
Type: CloudWatchEvent
4646
Description: Detects EC2 Security Group Events to Send to Teams
4747
Properties:
48-
EventBusName: event-bus-name
48+
EventBusName: event-bus-name
4949
Pattern:
5050
source:
5151
- "aws.ec2"

samtranslator/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.18.0'
1+
__version__ = '1.19.0'

samtranslator/model/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ def _validate_resource_dict(cls, logical_id, resource_dict):
153153
if resource_dict['Type'] != cls.resource_type:
154154
raise InvalidResourceException(logical_id, "Resource has incorrect Type; expected '{expected}', "
155155
"got '{actual}'".format(
156-
expected=cls.resource_type,
157-
actual=resource_dict['Type']))
156+
expected=cls.resource_type,
157+
actual=resource_dict['Type']))
158158

159159
if 'Properties' in resource_dict and not isinstance(resource_dict['Properties'], dict):
160160
raise InvalidResourceException(logical_id, "Properties of a resource must be an object.")
@@ -475,7 +475,7 @@ def resolve_resource_type(self, resource_dict):
475475
"""
476476
if not self.can_resolve(resource_dict):
477477
raise TypeError("Resource dict has missing or invalid value for key Type. Event Type is: {}.".format(
478-
resource_dict.get('Type')))
478+
resource_dict.get('Type')))
479479
if resource_dict['Type'] not in self.resource_types:
480480
raise TypeError("Invalid resource type {resource_type}".format(resource_type=resource_dict['Type']))
481481
return self.resource_types[resource_dict['Type']]
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
from collections import namedtuple
2+
from six import string_types
3+
from samtranslator.model.intrinsics import ref
4+
from samtranslator.model.apigatewayv2 import ApiGatewayV2HttpApi, ApiGatewayV2Stage, ApiGatewayV2Authorizer
5+
from samtranslator.model.exceptions import InvalidResourceException
6+
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
7+
from samtranslator.open_api.open_api import OpenApiEditor
8+
from samtranslator.translator import logical_id_generator
9+
from samtranslator.model.tags.resource_tagging import get_tag_list
10+
11+
AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer"])
12+
AuthProperties.__new__.__defaults__ = (None, None)
13+
DefaultStageName = "$default"
14+
15+
16+
class HttpApiGenerator(object):
17+
18+
def __init__(self, logical_id, stage_variables, depends_on, definition_body, definition_uri,
19+
stage_name, tags=None, auth=None, access_log_settings=None,
20+
resource_attributes=None, passthrough_resource_attributes=None):
21+
"""Constructs an API Generator class that generates API Gateway resources
22+
23+
:param logical_id: Logical id of the SAM API Resource
24+
:param stage_variables: API Gateway Variables
25+
:param depends_on: Any resources that need to be depended on
26+
:param definition_body: API definition
27+
:param definition_uri: URI to API definition
28+
:param name: Name of the API Gateway resource
29+
:param stage_name: Name of the Stage
30+
:param tags: Stage and API Tags
31+
:param access_log_settings: Whether to send access logs and where for Stage
32+
:param resource_attributes: Resource attributes to add to API resources
33+
:param passthrough_resource_attributes: Attributes such as `Condition` that are added to derived resources
34+
"""
35+
self.logical_id = logical_id
36+
self.stage_variables = stage_variables
37+
self.depends_on = depends_on
38+
self.definition_body = definition_body
39+
self.definition_uri = definition_uri
40+
self.stage_name = stage_name
41+
if not self.stage_name:
42+
self.stage_name = DefaultStageName
43+
self.auth = auth
44+
self.tags = tags
45+
self.access_log_settings = access_log_settings
46+
self.resource_attributes = resource_attributes
47+
self.passthrough_resource_attributes = passthrough_resource_attributes
48+
49+
def _construct_http_api(self):
50+
"""Constructs and returns the ApiGatewayV2 HttpApi.
51+
52+
:returns: the HttpApi to which this SAM Api corresponds
53+
:rtype: model.apigatewayv2.ApiGatewayHttpApi
54+
"""
55+
http_api = ApiGatewayV2HttpApi(self.logical_id, depends_on=self.depends_on, attributes=self.resource_attributes)
56+
57+
if self.definition_uri and self.definition_body:
58+
raise InvalidResourceException(self.logical_id,
59+
"Specify either 'DefinitionUri' or 'DefinitionBody' property and not both")
60+
61+
self._add_auth()
62+
63+
if self.definition_uri:
64+
http_api.BodyS3Location = self._construct_body_s3_dict()
65+
elif self.definition_body:
66+
http_api.Body = self.definition_body
67+
else:
68+
raise InvalidResourceException(self.logical_id,
69+
"'DefinitionUri' or 'DefinitionBody' are required properties of an "
70+
"'AWS::Serverless::HttpApi'. Add a value for one of these properties or "
71+
"add a 'HttpApi' event to an 'AWS::Serverless::Function'")
72+
73+
if self.tags is not None:
74+
http_api.Tags = get_tag_list(self.tags)
75+
76+
return http_api
77+
78+
def _add_auth(self):
79+
"""
80+
Add Auth configuration to the OAS file, if necessary
81+
"""
82+
if not self.auth:
83+
return
84+
85+
if self.auth and not self.definition_body:
86+
raise InvalidResourceException(self.logical_id,
87+
"Auth works only with inline Swagger specified in "
88+
"'DefinitionBody' property")
89+
90+
# Make sure keys in the dict are recognized
91+
if not all(key in AuthProperties._fields for key in self.auth.keys()):
92+
raise InvalidResourceException(
93+
self.logical_id, "Invalid value for 'Auth' property")
94+
95+
if not OpenApiEditor.is_valid(self.definition_body):
96+
raise InvalidResourceException(self.logical_id, "Unable to add Auth configuration because "
97+
"'DefinitionBody' does not contain a valid Swagger")
98+
open_api_editor = OpenApiEditor(self.definition_body)
99+
auth_properties = AuthProperties(**self.auth)
100+
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer)
101+
102+
# authorizers is guaranteed to return a value or raise an exception
103+
open_api_editor.add_authorizers_security_definitions(authorizers)
104+
self._set_default_authorizer(open_api_editor, authorizers, auth_properties.DefaultAuthorizer,
105+
auth_properties.Authorizers)
106+
self.definition_body = open_api_editor.openapi
107+
108+
def _set_default_authorizer(self, open_api_editor, authorizers, default_authorizer, api_authorizers):
109+
"""
110+
Sets the default authorizer if one is given in the template
111+
:param open_api_editor: editor object that contains the OpenApi definition
112+
:param authorizers: authorizer definitions converted from the API auth section
113+
:param default_authorizer: name of the default authorizer
114+
:param api_authorizers: API auth section authorizer defintions
115+
"""
116+
if not default_authorizer:
117+
return
118+
119+
if not authorizers.get(default_authorizer):
120+
raise InvalidResourceException(self.logical_id, "Unable to set DefaultAuthorizer because '" +
121+
default_authorizer + "' was not defined in 'Authorizers'")
122+
123+
for path in open_api_editor.iter_on_path():
124+
open_api_editor.set_path_default_authorizer(path, default_authorizer, authorizers=authorizers,
125+
api_authorizers=api_authorizers)
126+
127+
def _get_authorizers(self, authorizers_config, default_authorizer=None):
128+
"""
129+
Returns all authorizers for an API as an ApiGatewayV2Authorizer object
130+
:param authorizers_config: authorizer configuration from the API Auth section
131+
:param default_authorizer: name of the default authorizer
132+
"""
133+
authorizers = {}
134+
135+
if not isinstance(authorizers_config, dict):
136+
raise InvalidResourceException(self.logical_id,
137+
"Authorizers must be a dictionary")
138+
139+
for authorizer_name, authorizer in authorizers_config.items():
140+
if not isinstance(authorizer, dict):
141+
raise InvalidResourceException(self.logical_id,
142+
"Authorizer %s must be a dictionary." % (authorizer_name))
143+
144+
authorizers[authorizer_name] = ApiGatewayV2Authorizer(
145+
api_logical_id=self.logical_id,
146+
name=authorizer_name,
147+
open_id_connect_url=authorizer.get('OpenIdConnectUrl'),
148+
authorization_scopes=authorizer.get('AuthorizationScopes'),
149+
jwt_configuration=authorizer.get('JwtConfiguration'),
150+
id_source=authorizer.get('IdentitySource')
151+
)
152+
return authorizers
153+
154+
def _construct_body_s3_dict(self):
155+
"""
156+
Constructs the HttpApi's `BodyS3Location property`, from the SAM Api's DefinitionUri property.
157+
:returns: a BodyS3Location dict, containing the S3 Bucket, Key, and Version of the OpenApi definition
158+
:rtype: dict
159+
"""
160+
if isinstance(self.definition_uri, dict):
161+
if not self.definition_uri.get("Bucket", None) or not self.definition_uri.get("Key", None):
162+
# DefinitionUri is a dictionary but does not contain Bucket or Key property
163+
raise InvalidResourceException(self.logical_id,
164+
"'DefinitionUri' requires Bucket and Key properties to be specified")
165+
s3_pointer = self.definition_uri
166+
167+
else:
168+
# DefinitionUri is a string
169+
s3_pointer = parse_s3_uri(self.definition_uri)
170+
if s3_pointer is None:
171+
raise InvalidResourceException(self.logical_id,
172+
'\'DefinitionUri\' is not a valid S3 Uri of the form '
173+
'"s3://bucket/key" with optional versionId query parameter.')
174+
175+
body_s3 = {
176+
'Bucket': s3_pointer['Bucket'],
177+
'Key': s3_pointer['Key']
178+
}
179+
if 'Version' in s3_pointer:
180+
body_s3['Version'] = s3_pointer['Version']
181+
return body_s3
182+
183+
def _construct_stage(self):
184+
"""Constructs and returns the ApiGatewayV2 Stage.
185+
186+
:returns: the Stage to which this SAM Api corresponds
187+
:rtype: model.apigatewayv2.ApiGatewayV2Stage
188+
"""
189+
190+
# If there are no special configurations, don't create a stage and use the default
191+
if not self.stage_name and not self.stage_variables and not self.access_log_settings:
192+
return
193+
194+
# If StageName is some intrinsic function, then don't prefix the Stage's logical ID
195+
# This will NOT create duplicates because we allow only ONE stage per API resource
196+
stage_name_prefix = self.stage_name if isinstance(self.stage_name, string_types) else ""
197+
if stage_name_prefix.isalnum():
198+
stage_logical_id = self.logical_id + stage_name_prefix + "Stage"
199+
elif stage_name_prefix == DefaultStageName:
200+
stage_logical_id = self.logical_id + "ApiGatewayDefaultStage"
201+
else:
202+
generator = logical_id_generator.LogicalIdGenerator(self.logical_id + "Stage", stage_name_prefix)
203+
stage_logical_id = generator.gen()
204+
stage = ApiGatewayV2Stage(stage_logical_id,
205+
attributes=self.passthrough_resource_attributes)
206+
stage.ApiId = ref(self.logical_id)
207+
stage.StageName = self.stage_name
208+
stage.StageVariables = self.stage_variables
209+
stage.AccessLogSettings = self.access_log_settings
210+
stage.AutoDeploy = True
211+
212+
if self.tags is not None:
213+
stage.Tags = get_tag_list(self.tags)
214+
215+
return stage
216+
217+
def to_cloudformation(self):
218+
"""Generates CloudFormation resources from a SAM API resource
219+
220+
:returns: a tuple containing the HttpApi and Stage for an empty Api.
221+
:rtype: tuple
222+
"""
223+
http_api = self._construct_http_api()
224+
225+
stage = self._construct_stage()
226+
227+
return http_api, stage

0 commit comments

Comments
 (0)