Skip to content

Commit 2368c33

Browse files
viksrivatjfuss
authored andcommitted
feat(start-api): CloudFormation AWS::ApiGateway::RestApi support (#1238)
1 parent 65b6f53 commit 2368c33

File tree

14 files changed

+1218
-459
lines changed

14 files changed

+1218
-459
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"""
2+
Class to store the API configurations in the SAM Template. This class helps store both implicit and explicit
3+
APIs in a standardized format
4+
"""
5+
6+
import logging
7+
from collections import namedtuple
8+
9+
from six import string_types
10+
11+
LOG = logging.getLogger(__name__)
12+
13+
14+
class ApiCollector(object):
15+
# Properties of each API. The structure is quite similar to the properties of AWS::Serverless::Api resource.
16+
# This is intentional because it allows us to easily extend this class to support future properties on the API.
17+
# We will store properties of Implicit APIs also in this format which converges the handling of implicit & explicit
18+
# APIs.
19+
Properties = namedtuple("Properties", ["apis", "binary_media_types", "cors", "stage_name", "stage_variables"])
20+
21+
def __init__(self):
22+
# API properties stored per resource. Key is the LogicalId of the AWS::Serverless::Api resource and
23+
# value is the properties
24+
self.by_resource = {}
25+
26+
def __iter__(self):
27+
"""
28+
Iterator to iterate through all the APIs stored in the collector. In each iteration, this yields the
29+
LogicalId of the API resource and a list of APIs available in this resource.
30+
31+
Yields
32+
-------
33+
str
34+
LogicalID of the AWS::Serverless::Api resource
35+
list samcli.commands.local.lib.provider.Api
36+
List of the API available in this resource along with additional configuration like binary media types.
37+
"""
38+
39+
for logical_id, _ in self.by_resource.items():
40+
yield logical_id, self._get_apis_with_config(logical_id)
41+
42+
def add_apis(self, logical_id, apis):
43+
"""
44+
Stores the given APIs tagged under the given logicalId
45+
46+
Parameters
47+
----------
48+
logical_id : str
49+
LogicalId of the AWS::Serverless::Api resource
50+
51+
apis : list of samcli.commands.local.lib.provider.Api
52+
List of APIs available in this resource
53+
"""
54+
properties = self._get_properties(logical_id)
55+
properties.apis.extend(apis)
56+
57+
def add_binary_media_types(self, logical_id, binary_media_types):
58+
"""
59+
Stores the binary media type configuration for the API with given logical ID
60+
61+
Parameters
62+
----------
63+
logical_id : str
64+
LogicalId of the AWS::Serverless::Api resource
65+
66+
binary_media_types : list of str
67+
List of binary media types supported by this resource
68+
69+
"""
70+
properties = self._get_properties(logical_id)
71+
72+
binary_media_types = binary_media_types or []
73+
for value in binary_media_types:
74+
normalized_value = self._normalize_binary_media_type(value)
75+
76+
# If the value is not supported, then just skip it.
77+
if normalized_value:
78+
properties.binary_media_types.add(normalized_value)
79+
else:
80+
LOG.debug("Unsupported data type of binary media type value of resource '%s'", logical_id)
81+
82+
def add_stage_name(self, logical_id, stage_name):
83+
"""
84+
Stores the stage name for the API with the given local ID
85+
86+
Parameters
87+
----------
88+
logical_id : str
89+
LogicalId of the AWS::Serverless::Api resource
90+
91+
stage_name : str
92+
The stage_name string
93+
94+
"""
95+
properties = self._get_properties(logical_id)
96+
properties = properties._replace(stage_name=stage_name)
97+
self._set_properties(logical_id, properties)
98+
99+
def add_stage_variables(self, logical_id, stage_variables):
100+
"""
101+
Stores the stage variables for the API with the given local ID
102+
103+
Parameters
104+
----------
105+
logical_id : str
106+
LogicalId of the AWS::Serverless::Api resource
107+
108+
stage_variables : dict
109+
A dictionary containing stage variables.
110+
111+
"""
112+
properties = self._get_properties(logical_id)
113+
properties = properties._replace(stage_variables=stage_variables)
114+
self._set_properties(logical_id, properties)
115+
116+
def _get_apis_with_config(self, logical_id):
117+
"""
118+
Returns the list of APIs in this resource along with other extra configuration such as binary media types,
119+
cors etc. Additional configuration is merged directly into the API data because these properties, although
120+
defined globally, actually apply to each API.
121+
122+
Parameters
123+
----------
124+
logical_id : str
125+
Logical ID of the resource to fetch data for
126+
127+
Returns
128+
-------
129+
list of samcli.commands.local.lib.provider.Api
130+
List of APIs with additional configurations for the resource with given logicalId. If there are no APIs,
131+
then it returns an empty list
132+
"""
133+
134+
properties = self._get_properties(logical_id)
135+
136+
# These configs need to be applied to each API
137+
binary_media = sorted(list(properties.binary_media_types)) # Also sort the list to keep the ordering stable
138+
cors = properties.cors
139+
stage_name = properties.stage_name
140+
stage_variables = properties.stage_variables
141+
142+
result = []
143+
for api in properties.apis:
144+
# Create a copy of the API with updated configuration
145+
updated_api = api._replace(binary_media_types=binary_media,
146+
cors=cors,
147+
stage_name=stage_name,
148+
stage_variables=stage_variables)
149+
result.append(updated_api)
150+
151+
return result
152+
153+
def _get_properties(self, logical_id):
154+
"""
155+
Returns the properties of resource with given logical ID. If a resource is not found, then it returns an
156+
empty data.
157+
158+
Parameters
159+
----------
160+
logical_id : str
161+
Logical ID of the resource
162+
163+
Returns
164+
-------
165+
samcli.commands.local.lib.sam_api_provider.ApiCollector.Properties
166+
Properties object for this resource.
167+
"""
168+
169+
if logical_id not in self.by_resource:
170+
self.by_resource[logical_id] = self.Properties(apis=[],
171+
# Use a set() to be able to easily de-dupe
172+
binary_media_types=set(),
173+
cors=None,
174+
stage_name=None,
175+
stage_variables=None)
176+
177+
return self.by_resource[logical_id]
178+
179+
def _set_properties(self, logical_id, properties):
180+
"""
181+
Sets the properties of resource with given logical ID. If a resource is not found, it does nothing
182+
183+
Parameters
184+
----------
185+
logical_id : str
186+
Logical ID of the resource
187+
properties : samcli.commands.local.lib.sam_api_provider.ApiCollector.Properties
188+
Properties object for this resource.
189+
"""
190+
191+
if logical_id in self.by_resource:
192+
self.by_resource[logical_id] = properties
193+
194+
@staticmethod
195+
def _normalize_binary_media_type(value):
196+
"""
197+
Converts binary media types values to the canonical format. Ex: image~1gif -> image/gif. If the value is not
198+
a string, then this method just returns None
199+
200+
Parameters
201+
----------
202+
value : str
203+
Value to be normalized
204+
205+
Returns
206+
-------
207+
str or None
208+
Normalized value. If the input was not a string, then None is returned
209+
"""
210+
211+
if not isinstance(value, string_types):
212+
# It is possible that user specified a dict value for one of the binary media types. We just skip them
213+
return None
214+
215+
return value.replace("~1", "/")
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Class that provides Apis from a SAM Template"""
2+
3+
import logging
4+
5+
from samcli.commands.local.lib.cfn_base_api_provider import CfnBaseApiProvider
6+
from samcli.commands.local.lib.api_collector import ApiCollector
7+
from samcli.commands.local.lib.provider import AbstractApiProvider
8+
from samcli.commands.local.lib.sam_base_provider import SamBaseProvider
9+
from samcli.commands.local.lib.sam_api_provider import SamApiProvider
10+
from samcli.commands.local.lib.cfn_api_provider import CfnApiProvider
11+
12+
LOG = logging.getLogger(__name__)
13+
14+
15+
class ApiProvider(AbstractApiProvider):
16+
17+
def __init__(self, template_dict, parameter_overrides=None, cwd=None):
18+
"""
19+
Initialize the class with SAM template data. The template_dict (SAM Templated) is assumed
20+
to be valid, normalized and a dictionary. template_dict should be normalized by running any and all
21+
pre-processing before passing to this class.
22+
This class does not perform any syntactic validation of the template.
23+
24+
After the class is initialized, changes to ``template_dict`` will not be reflected in here.
25+
You will need to explicitly update the class with new template, if necessary.
26+
27+
Parameters
28+
----------
29+
template_dict : dict
30+
SAM Template as a dictionary
31+
32+
cwd : str
33+
Optional working directory with respect to which we will resolve relative path to Swagger file
34+
"""
35+
self.template_dict = SamBaseProvider.get_template(template_dict, parameter_overrides)
36+
self.resources = self.template_dict.get("Resources", {})
37+
38+
LOG.debug("%d resources found in the template", len(self.resources))
39+
40+
# Store a set of apis
41+
self.cwd = cwd
42+
self.apis = self._extract_apis(self.resources)
43+
44+
LOG.debug("%d APIs found in the template", len(self.apis))
45+
46+
def get_all(self):
47+
"""
48+
Yields all the Lambda functions with Api Events available in the SAM Template.
49+
50+
:yields Api: namedtuple containing the Api information
51+
"""
52+
53+
for api in self.apis:
54+
yield api
55+
56+
def _extract_apis(self, resources):
57+
"""
58+
Extracts all the Apis by running through the one providers. The provider that has the first type matched
59+
will be run across all the resources
60+
61+
Parameters
62+
----------
63+
resources: dict
64+
The dictionary containing the different resources within the template
65+
Returns
66+
---------
67+
list of Apis extracted from the resources
68+
"""
69+
collector = ApiCollector()
70+
provider = self.find_api_provider(resources)
71+
apis = provider.extract_resource_api(resources, collector, cwd=self.cwd)
72+
return self.normalize_apis(apis)
73+
74+
@staticmethod
75+
def find_api_provider(resources):
76+
"""
77+
Finds the ApiProvider given the first api type of the resource
78+
79+
Parameters
80+
-----------
81+
resources: dict
82+
The dictionary containing the different resources within the template
83+
84+
Return
85+
----------
86+
Instance of the ApiProvider that will be run on the template with a default of SamApiProvider
87+
"""
88+
for _, resource in resources.items():
89+
if resource.get(CfnBaseApiProvider.RESOURCE_TYPE) in SamApiProvider.TYPES:
90+
return SamApiProvider()
91+
elif resource.get(CfnBaseApiProvider.RESOURCE_TYPE) in CfnApiProvider.TYPES:
92+
return CfnApiProvider()
93+
94+
return SamApiProvider()
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Parses SAM given a template"""
2+
import logging
3+
4+
from samcli.commands.local.lib.cfn_base_api_provider import CfnBaseApiProvider
5+
6+
LOG = logging.getLogger(__name__)
7+
8+
9+
class CfnApiProvider(CfnBaseApiProvider):
10+
APIGATEWAY_RESTAPI = "AWS::ApiGateway::RestApi"
11+
TYPES = [
12+
APIGATEWAY_RESTAPI
13+
]
14+
15+
def extract_resource_api(self, resources, collector, cwd=None):
16+
"""
17+
Extract the Api Object from a given resource and adds it to the ApiCollector.
18+
19+
Parameters
20+
----------
21+
resources: dict
22+
The dictionary containing the different resources within the template
23+
24+
collector: ApiCollector
25+
Instance of the API collector that where we will save the API information
26+
27+
cwd : str
28+
Optional working directory with respect to which we will resolve relative path to Swagger file
29+
30+
Return
31+
-------
32+
Returns a list of Apis
33+
"""
34+
for logical_id, resource in resources.items():
35+
resource_type = resource.get(CfnBaseApiProvider.RESOURCE_TYPE)
36+
if resource_type == CfnApiProvider.APIGATEWAY_RESTAPI:
37+
self._extract_cloud_formation_api(logical_id, resource, collector, cwd)
38+
all_apis = []
39+
for _, apis in collector:
40+
all_apis.extend(apis)
41+
return all_apis
42+
43+
def _extract_cloud_formation_api(self, logical_id, api_resource, collector, cwd=None):
44+
"""
45+
Extract APIs from AWS::ApiGateway::RestApi resource by reading and parsing Swagger documents. The result is
46+
added to the collector.
47+
48+
Parameters
49+
----------
50+
logical_id : str
51+
Logical ID of the resource
52+
53+
api_resource : dict
54+
Resource definition, including its properties
55+
56+
collector : ApiCollector
57+
Instance of the API collector that where we will save the API information
58+
"""
59+
properties = api_resource.get("Properties", {})
60+
body = properties.get("Body")
61+
body_s3_location = properties.get("BodyS3Location")
62+
binary_media = properties.get("BinaryMediaTypes", [])
63+
64+
if not body and not body_s3_location:
65+
# Swagger is not found anywhere.
66+
LOG.debug("Skipping resource '%s'. Swagger document not found in Body and BodyS3Location",
67+
logical_id)
68+
return
69+
self.extract_swagger_api(logical_id, body, body_s3_location, binary_media, collector, cwd)

0 commit comments

Comments
 (0)