Skip to content

Commit

Permalink
feat(start-api): CloudFormation AWS::ApiGateway::Stage Support (#1239)
Browse files Browse the repository at this point in the history
  • Loading branch information
viksrivat authored and jfuss committed Aug 7, 2019
1 parent 2368c33 commit 6e083a9
Show file tree
Hide file tree
Showing 20 changed files with 1,011 additions and 907 deletions.
230 changes: 98 additions & 132 deletions samcli/commands/local/lib/api_collector.py
Original file line number Diff line number Diff line change
@@ -1,207 +1,173 @@
"""
Class to store the API configurations in the SAM Template. This class helps store both implicit and explicit
APIs in a standardized format
routes in a standardized format
"""

import logging
from collections import namedtuple
from collections import defaultdict

from six import string_types

from samcli.local.apigw.local_apigw_service import Route
from samcli.commands.local.lib.provider import Api

LOG = logging.getLogger(__name__)


class ApiCollector(object):
# Properties of each API. The structure is quite similar to the properties of AWS::Serverless::Api resource.
# This is intentional because it allows us to easily extend this class to support future properties on the API.
# We will store properties of Implicit APIs also in this format which converges the handling of implicit & explicit
# APIs.
Properties = namedtuple("Properties", ["apis", "binary_media_types", "cors", "stage_name", "stage_variables"])

def __init__(self):
# API properties stored per resource. Key is the LogicalId of the AWS::Serverless::Api resource and
# value is the properties
self.by_resource = {}
# Route properties stored per resource.
self._route_per_resource = defaultdict(list)

# processed values to be set before creating the api
self._routes = []
self.binary_media_types_set = set()
self.stage_name = None
self.stage_variables = None

def __iter__(self):
"""
Iterator to iterate through all the APIs stored in the collector. In each iteration, this yields the
LogicalId of the API resource and a list of APIs available in this resource.
Iterator to iterate through all the routes stored in the collector. In each iteration, this yields the
LogicalId of the route resource and a list of routes available in this resource.
Yields
-------
str
LogicalID of the AWS::Serverless::Api resource
LogicalID of the AWS::Serverless::Api or AWS::ApiGateway::RestApi resource
list samcli.commands.local.lib.provider.Api
List of the API available in this resource along with additional configuration like binary media types.
"""

for logical_id, _ in self.by_resource.items():
yield logical_id, self._get_apis_with_config(logical_id)
for logical_id, _ in self._route_per_resource.items():
yield logical_id, self._get_routes(logical_id)

def add_apis(self, logical_id, apis):
def add_routes(self, logical_id, routes):
"""
Stores the given APIs tagged under the given logicalId
Stores the given routes tagged under the given logicalId
Parameters
----------
logical_id : str
LogicalId of the AWS::Serverless::Api resource
apis : list of samcli.commands.local.lib.provider.Api
List of APIs available in this resource
LogicalId of the AWS::Serverless::Api or AWS::ApiGateway::RestApi resource
routes : list of samcli.commands.local.agiw.local_apigw_service.Route
List of routes available in this resource
"""
properties = self._get_properties(logical_id)
properties.apis.extend(apis)
self._get_routes(logical_id).extend(routes)

def add_binary_media_types(self, logical_id, binary_media_types):
def _get_routes(self, logical_id):
"""
Stores the binary media type configuration for the API with given logical ID
Returns the properties of resource with given logical ID. If a resource is not found, then it returns an
empty data.
Parameters
----------
logical_id : str
LogicalId of the AWS::Serverless::Api resource
binary_media_types : list of str
List of binary media types supported by this resource
"""
properties = self._get_properties(logical_id)

binary_media_types = binary_media_types or []
for value in binary_media_types:
normalized_value = self._normalize_binary_media_type(value)

# If the value is not supported, then just skip it.
if normalized_value:
properties.binary_media_types.add(normalized_value)
else:
LOG.debug("Unsupported data type of binary media type value of resource '%s'", logical_id)

def add_stage_name(self, logical_id, stage_name):
Logical ID of the resource
Returns
-------
samcli.commands.local.lib.Routes
Properties object for this resource.
"""
Stores the stage name for the API with the given local ID

Parameters
----------
logical_id : str
LogicalId of the AWS::Serverless::Api resource
return self._route_per_resource[logical_id]

stage_name : str
The stage_name string
@property
def routes(self):
return self._routes if self._routes else self.all_routes()

"""
properties = self._get_properties(logical_id)
properties = properties._replace(stage_name=stage_name)
self._set_properties(logical_id, properties)
@routes.setter
def routes(self, routes):
self._routes = routes

def add_stage_variables(self, logical_id, stage_variables):
def all_routes(self):
"""
Stores the stage variables for the API with the given local ID
Parameters
----------
logical_id : str
LogicalId of the AWS::Serverless::Api resource
stage_variables : dict
A dictionary containing stage variables.
Gets all the routes within the _route_per_resource
Return
-------
All the routes within the _route_per_resource
"""
properties = self._get_properties(logical_id)
properties = properties._replace(stage_variables=stage_variables)
self._set_properties(logical_id, properties)
routes = []
for logical_id in self._route_per_resource.keys():
routes.extend(self._get_routes(logical_id))
return routes

def _get_apis_with_config(self, logical_id):
def get_api(self):
"""
Returns the list of APIs in this resource along with other extra configuration such as binary media types,
cors etc. Additional configuration is merged directly into the API data because these properties, although
defined globally, actually apply to each API.
Creates the api using the parts from the ApiCollector. The routes are also deduped so that there is no
duplicate routes with the same function name, path, but different method.
Parameters
----------
logical_id : str
Logical ID of the resource to fetch data for
The normalised_routes are the routes that have been processed. By default, this will get all the routes.
However, it can be changed to override the default value of normalised routes such as in SamApiProvider
Returns
Return
-------
list of samcli.commands.local.lib.provider.Api
List of APIs with additional configurations for the resource with given logicalId. If there are no APIs,
then it returns an empty list
An Api object with all the properties
"""
api = Api()
api.routes = self.dedupe_function_routes(self.routes)
api.binary_media_types_set = self.binary_media_types_set
api.stage_name = self.stage_name
api.stage_variables = self.stage_variables
return api

properties = self._get_properties(logical_id)
@staticmethod
def dedupe_function_routes(routes):
"""
Remove duplicate routes that have the same function_name and method
# These configs need to be applied to each API
binary_media = sorted(list(properties.binary_media_types)) # Also sort the list to keep the ordering stable
cors = properties.cors
stage_name = properties.stage_name
stage_variables = properties.stage_variables
route: list(Route)
List of Routes
result = []
for api in properties.apis:
# Create a copy of the API with updated configuration
updated_api = api._replace(binary_media_types=binary_media,
cors=cors,
stage_name=stage_name,
stage_variables=stage_variables)
result.append(updated_api)
Return
-------
A list of routes without duplicate routes with the same function_name and method
"""
grouped_routes = {}

return result
for route in routes:
key = "{}-{}".format(route.function_name, route.path)
config = grouped_routes.get(key, None)
methods = route.methods
if config:
methods += config.methods
sorted_methods = sorted(methods)
grouped_routes[key] = Route(function_name=route.function_name, path=route.path, methods=sorted_methods)
return list(grouped_routes.values())

def _get_properties(self, logical_id):
def add_binary_media_types(self, logical_id, binary_media_types):
"""
Returns the properties of resource with given logical ID. If a resource is not found, then it returns an
empty data.
Stores the binary media type configuration for the API with given logical ID
Parameters
----------
logical_id : str
Logical ID of the resource
Returns
-------
samcli.commands.local.lib.sam_api_provider.ApiCollector.Properties
Properties object for this resource.
"""

if logical_id not in self.by_resource:
self.by_resource[logical_id] = self.Properties(apis=[],
# Use a set() to be able to easily de-dupe
binary_media_types=set(),
cors=None,
stage_name=None,
stage_variables=None)
logical_id : str
LogicalId of the AWS::Serverless::Api resource
return self.by_resource[logical_id]
api: samcli.commands.local.lib.provider.Api
Instance of the Api which will save all the api configurations
def _set_properties(self, logical_id, properties):
binary_media_types : list of str
List of binary media types supported by this resource
"""
Sets the properties of resource with given logical ID. If a resource is not found, it does nothing

Parameters
----------
logical_id : str
Logical ID of the resource
properties : samcli.commands.local.lib.sam_api_provider.ApiCollector.Properties
Properties object for this resource.
"""
binary_media_types = binary_media_types or []
for value in binary_media_types:
normalized_value = self.normalize_binary_media_type(value)

if logical_id in self.by_resource:
self.by_resource[logical_id] = properties
# If the value is not supported, then just skip it.
if normalized_value:
self.binary_media_types_set.add(normalized_value)
else:
LOG.debug("Unsupported data type of binary media type value of resource '%s'", logical_id)

@staticmethod
def _normalize_binary_media_type(value):
def normalize_binary_media_type(value):
"""
Converts binary media types values to the canonical format. Ex: image~1gif -> image/gif. If the value is not
a string, then this method just returns None
Parameters
----------
value : str
Value to be normalized
Returns
-------
str or None
Expand Down
35 changes: 17 additions & 18 deletions samcli/commands/local/lib/api_provider.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
"""Class that provides Apis from a SAM Template"""
"""Class that provides the Api with a list of routes from a Template"""

import logging

from samcli.commands.local.lib.cfn_base_api_provider import CfnBaseApiProvider
from samcli.commands.local.lib.api_collector import ApiCollector
from samcli.commands.local.lib.cfn_api_provider import CfnApiProvider
from samcli.commands.local.lib.cfn_base_api_provider import CfnBaseApiProvider
from samcli.commands.local.lib.provider import AbstractApiProvider
from samcli.commands.local.lib.sam_base_provider import SamBaseProvider
from samcli.commands.local.lib.sam_api_provider import SamApiProvider
from samcli.commands.local.lib.cfn_api_provider import CfnApiProvider
from samcli.commands.local.lib.sam_base_provider import SamBaseProvider

LOG = logging.getLogger(__name__)

Expand All @@ -16,7 +16,7 @@ class ApiProvider(AbstractApiProvider):

def __init__(self, template_dict, parameter_overrides=None, cwd=None):
"""
Initialize the class with SAM template data. The template_dict (SAM Templated) is assumed
Initialize the class with template data. The template_dict is assumed
to be valid, normalized and a dictionary. template_dict should be normalized by running any and all
pre-processing before passing to this class.
This class does not perform any syntactic validation of the template.
Expand All @@ -27,7 +27,7 @@ def __init__(self, template_dict, parameter_overrides=None, cwd=None):
Parameters
----------
template_dict : dict
SAM Template as a dictionary
Template as a dictionary
cwd : str
Optional working directory with respect to which we will resolve relative path to Swagger file
Expand All @@ -39,23 +39,22 @@ def __init__(self, template_dict, parameter_overrides=None, cwd=None):

# Store a set of apis
self.cwd = cwd
self.apis = self._extract_apis(self.resources)

LOG.debug("%d APIs found in the template", len(self.apis))
self.api = self._extract_api(self.resources)
self.routes = self.api.routes
LOG.debug("%d APIs found in the template", len(self.routes))

def get_all(self):
"""
Yields all the Lambda functions with Api Events available in the SAM Template.
Yields all the Apis in the current Provider
:yields Api: namedtuple containing the Api information
:yields api: an Api object with routes and properties
"""

for api in self.apis:
yield api
yield self.api

def _extract_apis(self, resources):
def _extract_api(self, resources):
"""
Extracts all the Apis by running through the one providers. The provider that has the first type matched
Extracts all the routes by running through the one providers. The provider that has the first type matched
will be run across all the resources
Parameters
Expand All @@ -64,12 +63,12 @@ def _extract_apis(self, resources):
The dictionary containing the different resources within the template
Returns
---------
list of Apis extracted from the resources
An Api from the parsed template
"""
collector = ApiCollector()
provider = self.find_api_provider(resources)
apis = provider.extract_resource_api(resources, collector, cwd=self.cwd)
return self.normalize_apis(apis)
provider.extract_resources(resources, collector, cwd=self.cwd)
return collector.get_api()

@staticmethod
def find_api_provider(resources):
Expand Down
Loading

0 comments on commit 6e083a9

Please sign in to comment.