Cloudformation - Create ApiGateway resources (#3659)
* Cloudformation - Create ApiGateway resources * Cleanup * Linting
This commit is contained in:
parent
2f50f9cb24
commit
f64532ed40
@ -16,7 +16,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import responses
|
import responses
|
||||||
from moto.core import ACCOUNT_ID, BaseBackend, BaseModel
|
from moto.core import ACCOUNT_ID, BaseBackend, BaseModel, CloudFormationModel
|
||||||
from .utils import create_id
|
from .utils import create_id
|
||||||
from moto.core.utils import path_url
|
from moto.core.utils import path_url
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
@ -49,7 +49,7 @@ from ..core.models import responses_mock
|
|||||||
STAGE_URL = "https://{api_id}.execute-api.{region_name}.amazonaws.com/{stage_name}"
|
STAGE_URL = "https://{api_id}.execute-api.{region_name}.amazonaws.com/{stage_name}"
|
||||||
|
|
||||||
|
|
||||||
class Deployment(BaseModel, dict):
|
class Deployment(CloudFormationModel, dict):
|
||||||
def __init__(self, deployment_id, name, description=""):
|
def __init__(self, deployment_id, name, description=""):
|
||||||
super(Deployment, self).__init__()
|
super(Deployment, self).__init__()
|
||||||
self["id"] = deployment_id
|
self["id"] = deployment_id
|
||||||
@ -57,6 +57,27 @@ class Deployment(BaseModel, dict):
|
|||||||
self["description"] = description
|
self["description"] = description
|
||||||
self["createdDate"] = int(time.time())
|
self["createdDate"] = int(time.time())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_name_type():
|
||||||
|
return "Deployment"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_type():
|
||||||
|
return "AWS::ApiGateway::Deployment"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_cloudformation_json(
|
||||||
|
cls, resource_name, cloudformation_json, region_name
|
||||||
|
):
|
||||||
|
properties = cloudformation_json["Properties"]
|
||||||
|
rest_api_id = properties["RestApiId"]
|
||||||
|
name = properties["StageName"]
|
||||||
|
desc = properties.get("Description", "")
|
||||||
|
backend = apigateway_backends[region_name]
|
||||||
|
return backend.create_deployment(
|
||||||
|
function_id=rest_api_id, name=name, description=desc
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IntegrationResponse(BaseModel, dict):
|
class IntegrationResponse(BaseModel, dict):
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -109,7 +130,7 @@ class MethodResponse(BaseModel, dict):
|
|||||||
self["statusCode"] = status_code
|
self["statusCode"] = status_code
|
||||||
|
|
||||||
|
|
||||||
class Method(BaseModel, dict):
|
class Method(CloudFormationModel, dict):
|
||||||
def __init__(self, method_type, authorization_type, **kwargs):
|
def __init__(self, method_type, authorization_type, **kwargs):
|
||||||
super(Method, self).__init__()
|
super(Method, self).__init__()
|
||||||
self.update(
|
self.update(
|
||||||
@ -125,6 +146,45 @@ class Method(BaseModel, dict):
|
|||||||
)
|
)
|
||||||
self.method_responses = {}
|
self.method_responses = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_name_type():
|
||||||
|
return "Method"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_type():
|
||||||
|
return "AWS::ApiGateway::Method"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_cloudformation_json(
|
||||||
|
cls, resource_name, cloudformation_json, region_name
|
||||||
|
):
|
||||||
|
properties = cloudformation_json["Properties"]
|
||||||
|
rest_api_id = properties["RestApiId"]
|
||||||
|
resource_id = properties["ResourceId"]
|
||||||
|
method_type = properties["HttpMethod"]
|
||||||
|
auth_type = properties["AuthorizationType"]
|
||||||
|
key_req = properties["ApiKeyRequired"]
|
||||||
|
backend = apigateway_backends[region_name]
|
||||||
|
m = backend.create_method(
|
||||||
|
function_id=rest_api_id,
|
||||||
|
resource_id=resource_id,
|
||||||
|
method_type=method_type,
|
||||||
|
authorization_type=auth_type,
|
||||||
|
api_key_required=key_req,
|
||||||
|
)
|
||||||
|
int_method = properties["Integration"]["IntegrationHttpMethod"]
|
||||||
|
int_type = properties["Integration"]["Type"]
|
||||||
|
int_uri = properties["Integration"]["Uri"]
|
||||||
|
backend.create_integration(
|
||||||
|
function_id=rest_api_id,
|
||||||
|
resource_id=resource_id,
|
||||||
|
method_type=method_type,
|
||||||
|
integration_type=int_type,
|
||||||
|
uri=int_uri,
|
||||||
|
integration_method=int_method,
|
||||||
|
)
|
||||||
|
return m
|
||||||
|
|
||||||
def create_response(self, response_code):
|
def create_response(self, response_code):
|
||||||
method_response = MethodResponse(response_code)
|
method_response = MethodResponse(response_code)
|
||||||
self.method_responses[response_code] = method_response
|
self.method_responses[response_code] = method_response
|
||||||
@ -137,8 +197,9 @@ class Method(BaseModel, dict):
|
|||||||
return self.method_responses.pop(response_code)
|
return self.method_responses.pop(response_code)
|
||||||
|
|
||||||
|
|
||||||
class Resource(BaseModel):
|
class Resource(CloudFormationModel):
|
||||||
def __init__(self, id, region_name, api_id, path_part, parent_id):
|
def __init__(self, id, region_name, api_id, path_part, parent_id):
|
||||||
|
super(Resource, self).__init__()
|
||||||
self.id = id
|
self.id = id
|
||||||
self.region_name = region_name
|
self.region_name = region_name
|
||||||
self.api_id = api_id
|
self.api_id = api_id
|
||||||
@ -158,6 +219,39 @@ class Resource(BaseModel):
|
|||||||
response["pathPart"] = self.path_part
|
response["pathPart"] = self.path_part
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@property
|
||||||
|
def physical_resource_id(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_name_type():
|
||||||
|
return "Resource"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_type():
|
||||||
|
return "AWS::ApiGateway::Resource"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_cloudformation_json(
|
||||||
|
cls, resource_name, cloudformation_json, region_name
|
||||||
|
):
|
||||||
|
properties = cloudformation_json["Properties"]
|
||||||
|
api_id = properties["RestApiId"]
|
||||||
|
parent = properties["ParentId"]
|
||||||
|
path = properties["PathPart"]
|
||||||
|
|
||||||
|
backend = apigateway_backends[region_name]
|
||||||
|
if parent == api_id:
|
||||||
|
# A Root path (/) is automatically created. Any new paths should use this as their parent
|
||||||
|
resources = backend.list_resources(function_id=api_id)
|
||||||
|
root_id = [resource for resource in resources if resource.path_part == "/"][
|
||||||
|
0
|
||||||
|
].id
|
||||||
|
parent = root_id
|
||||||
|
return backend.create_resource(
|
||||||
|
function_id=api_id, parent_resource_id=parent, path_part=path
|
||||||
|
)
|
||||||
|
|
||||||
def get_path(self):
|
def get_path(self):
|
||||||
return self.get_parent_path() + self.path_part
|
return self.get_parent_path() + self.path_part
|
||||||
|
|
||||||
@ -473,8 +567,9 @@ class UsagePlanKey(BaseModel, dict):
|
|||||||
self["value"] = value
|
self["value"] = value
|
||||||
|
|
||||||
|
|
||||||
class RestAPI(BaseModel):
|
class RestAPI(CloudFormationModel):
|
||||||
def __init__(self, id, region_name, name, description, **kwargs):
|
def __init__(self, id, region_name, name, description, **kwargs):
|
||||||
|
super(RestAPI, self).__init__()
|
||||||
self.id = id
|
self.id = id
|
||||||
self.region_name = region_name
|
self.region_name = region_name
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -509,6 +604,38 @@ class RestAPI(BaseModel):
|
|||||||
"policy": self.policy,
|
"policy": self.policy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_cfn_attribute(self, attribute_name):
|
||||||
|
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||||
|
|
||||||
|
if attribute_name == "RootResourceId":
|
||||||
|
return self.id
|
||||||
|
raise UnformattedGetAttTemplateException()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def physical_resource_id(self):
|
||||||
|
return self.id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_name_type():
|
||||||
|
return "RestApi"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_type():
|
||||||
|
return "AWS::ApiGateway::RestApi"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_cloudformation_json(
|
||||||
|
cls, resource_name, cloudformation_json, region_name
|
||||||
|
):
|
||||||
|
properties = cloudformation_json["Properties"]
|
||||||
|
name = properties["Name"]
|
||||||
|
desc = properties.get("Description", "")
|
||||||
|
config = properties.get("EndpointConfiguration", None)
|
||||||
|
backend = apigateway_backends[region_name]
|
||||||
|
return backend.create_rest_api(
|
||||||
|
name=name, description=desc, endpoint_configuration=config
|
||||||
|
)
|
||||||
|
|
||||||
def add_child(self, path, parent_id=None):
|
def add_child(self, path, parent_id=None):
|
||||||
child_id = create_id()
|
child_id = create_id()
|
||||||
child = Resource(
|
child = Resource(
|
||||||
@ -1003,7 +1130,8 @@ class APIGatewayBackend(BaseBackend):
|
|||||||
methods = [
|
methods = [
|
||||||
list(res.resource_methods.values())
|
list(res.resource_methods.values())
|
||||||
for res in self.list_resources(function_id)
|
for res in self.list_resources(function_id)
|
||||||
][0]
|
]
|
||||||
|
methods = [m for sublist in methods for m in sublist]
|
||||||
if not any(methods):
|
if not any(methods):
|
||||||
raise NoMethodDefined()
|
raise NoMethodDefined()
|
||||||
method_integrations = [
|
method_integrations = [
|
||||||
|
@ -1,31 +1,33 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from .responses import APIGatewayResponse
|
from .responses import APIGatewayResponse
|
||||||
|
|
||||||
|
response = APIGatewayResponse()
|
||||||
|
|
||||||
url_bases = ["https?://apigateway.(.+).amazonaws.com"]
|
url_bases = ["https?://apigateway.(.+).amazonaws.com"]
|
||||||
|
|
||||||
url_paths = {
|
url_paths = {
|
||||||
"{0}/restapis$": APIGatewayResponse().restapis,
|
"{0}/restapis$": response.restapis,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/?$": APIGatewayResponse().restapis_individual,
|
"{0}/restapis/(?P<function_id>[^/]+)/?$": response.restapis_individual,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/resources$": APIGatewayResponse().resources,
|
"{0}/restapis/(?P<function_id>[^/]+)/resources$": response.resources,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/authorizers$": APIGatewayResponse().restapis_authorizers,
|
"{0}/restapis/(?P<function_id>[^/]+)/authorizers$": response.restapis_authorizers,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/authorizers/(?P<authorizer_id>[^/]+)/?$": APIGatewayResponse().authorizers,
|
"{0}/restapis/(?P<function_id>[^/]+)/authorizers/(?P<authorizer_id>[^/]+)/?$": response.authorizers,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/stages$": APIGatewayResponse().restapis_stages,
|
"{0}/restapis/(?P<function_id>[^/]+)/stages$": response.restapis_stages,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/stages/(?P<stage_name>[^/]+)/?$": APIGatewayResponse().stages,
|
"{0}/restapis/(?P<function_id>[^/]+)/stages/(?P<stage_name>[^/]+)/?$": response.stages,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/deployments$": APIGatewayResponse().deployments,
|
"{0}/restapis/(?P<function_id>[^/]+)/deployments$": response.deployments,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/deployments/(?P<deployment_id>[^/]+)/?$": APIGatewayResponse().individual_deployment,
|
"{0}/restapis/(?P<function_id>[^/]+)/deployments/(?P<deployment_id>[^/]+)/?$": response.individual_deployment,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/?$": APIGatewayResponse().resource_individual,
|
"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/?$": response.resource_individual,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/?$": APIGatewayResponse().resource_methods,
|
"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/?$": response.resource_methods,
|
||||||
r"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/responses/(?P<status_code>\d+)$": APIGatewayResponse().resource_method_responses,
|
r"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/responses/(?P<status_code>\d+)$": response.resource_method_responses,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/integration/?$": APIGatewayResponse().integrations,
|
"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/integration/?$": response.integrations,
|
||||||
r"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/integration/responses/(?P<status_code>\d+)/?$": APIGatewayResponse().integration_responses,
|
r"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/integration/responses/(?P<status_code>\d+)/?$": response.integration_responses,
|
||||||
"{0}/apikeys$": APIGatewayResponse().apikeys,
|
"{0}/apikeys$": response.apikeys,
|
||||||
"{0}/apikeys/(?P<apikey>[^/]+)": APIGatewayResponse().apikey_individual,
|
"{0}/apikeys/(?P<apikey>[^/]+)": response.apikey_individual,
|
||||||
"{0}/usageplans$": APIGatewayResponse().usage_plans,
|
"{0}/usageplans$": response.usage_plans,
|
||||||
"{0}/domainnames$": APIGatewayResponse().domain_names,
|
"{0}/domainnames$": response.domain_names,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/models$": APIGatewayResponse().models,
|
"{0}/restapis/(?P<function_id>[^/]+)/models$": response.models,
|
||||||
"{0}/restapis/(?P<function_id>[^/]+)/models/(?P<model_name>[^/]+)/?$": APIGatewayResponse().model_induvidual,
|
"{0}/restapis/(?P<function_id>[^/]+)/models/(?P<model_name>[^/]+)/?$": response.model_induvidual,
|
||||||
"{0}/domainnames/(?P<domain_name>[^/]+)/?$": APIGatewayResponse().domain_name_induvidual,
|
"{0}/domainnames/(?P<domain_name>[^/]+)/?$": response.domain_name_induvidual,
|
||||||
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/?$": APIGatewayResponse().usage_plan_individual,
|
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/?$": response.usage_plan_individual,
|
||||||
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/keys$": APIGatewayResponse().usage_plan_keys,
|
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/keys$": response.usage_plan_keys,
|
||||||
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/keys/(?P<api_key_id>[^/]+)/?$": APIGatewayResponse().usage_plan_key_individual,
|
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/keys/(?P<api_key_id>[^/]+)/?$": response.usage_plan_key_individual,
|
||||||
}
|
}
|
||||||
|
@ -185,6 +185,29 @@ def _validate_s3_bucket_and_key(data):
|
|||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(CloudFormationModel):
|
||||||
|
def __init__(self, region):
|
||||||
|
self.region = region
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_name_type():
|
||||||
|
return "Permission"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cloudformation_type():
|
||||||
|
return "AWS::Lambda::Permission"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_from_cloudformation_json(
|
||||||
|
cls, resource_name, cloudformation_json, region_name
|
||||||
|
):
|
||||||
|
properties = cloudformation_json["Properties"]
|
||||||
|
backend = lambda_backends[region_name]
|
||||||
|
fn = backend.get_function(properties["FunctionName"])
|
||||||
|
fn.policy.add_statement(raw=json.dumps(properties))
|
||||||
|
return Permission(region=region_name)
|
||||||
|
|
||||||
|
|
||||||
class LayerVersion(CloudFormationModel):
|
class LayerVersion(CloudFormationModel):
|
||||||
def __init__(self, spec, region):
|
def __init__(self, spec, region):
|
||||||
# required
|
# required
|
||||||
@ -316,7 +339,6 @@ class LambdaFunction(CloudFormationModel, DockerModel):
|
|||||||
self.layers = self._get_layers_data(spec.get("Layers", []))
|
self.layers = self._get_layers_data(spec.get("Layers", []))
|
||||||
|
|
||||||
self.logs_group_name = "/aws/lambda/{}".format(self.function_name)
|
self.logs_group_name = "/aws/lambda/{}".format(self.function_name)
|
||||||
self.logs_backend.ensure_log_group(self.logs_group_name, [])
|
|
||||||
|
|
||||||
# this isn't finished yet. it needs to find out the VpcId value
|
# this isn't finished yet. it needs to find out the VpcId value
|
||||||
self._vpc_config = spec.get(
|
self._vpc_config = spec.get(
|
||||||
@ -341,6 +363,10 @@ class LambdaFunction(CloudFormationModel, DockerModel):
|
|||||||
self.code_bytes = key.value
|
self.code_bytes = key.value
|
||||||
self.code_size = key.size
|
self.code_size = key.size
|
||||||
self.code_sha_256 = hashlib.sha256(key.value).hexdigest()
|
self.code_sha_256 = hashlib.sha256(key.value).hexdigest()
|
||||||
|
else:
|
||||||
|
self.code_bytes = ""
|
||||||
|
self.code_size = 0
|
||||||
|
self.code_sha_256 = ""
|
||||||
|
|
||||||
self.function_arn = make_function_arn(
|
self.function_arn = make_function_arn(
|
||||||
self.region, ACCOUNT_ID, self.function_name
|
self.region, ACCOUNT_ID, self.function_name
|
||||||
@ -510,6 +536,8 @@ class LambdaFunction(CloudFormationModel, DockerModel):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
def _invoke_lambda(self, code, event=None, context=None):
|
def _invoke_lambda(self, code, event=None, context=None):
|
||||||
|
# Create the LogGroup if necessary, to write the result to
|
||||||
|
self.logs_backend.ensure_log_group(self.logs_group_name, [])
|
||||||
# TODO: context not yet implemented
|
# TODO: context not yet implemented
|
||||||
if event is None:
|
if event is None:
|
||||||
event = dict()
|
event = dict()
|
||||||
|
@ -15,6 +15,7 @@ from moto.compat import collections_abc
|
|||||||
# the subclass's module hasn't been imported yet - then that subclass
|
# the subclass's module hasn't been imported yet - then that subclass
|
||||||
# doesn't exist yet, and __subclasses__ won't find it.
|
# doesn't exist yet, and __subclasses__ won't find it.
|
||||||
# So we import here to populate the list of subclasses.
|
# So we import here to populate the list of subclasses.
|
||||||
|
from moto.apigateway import models as apigateway_models # noqa
|
||||||
from moto.autoscaling import models as autoscaling_models # noqa
|
from moto.autoscaling import models as autoscaling_models # noqa
|
||||||
from moto.awslambda import models as awslambda_models # noqa
|
from moto.awslambda import models as awslambda_models # noqa
|
||||||
from moto.batch import models as batch_models # noqa
|
from moto.batch import models as batch_models # noqa
|
||||||
|
350
tests/test_apigateway/test_apigateway_cloudformation.py
Normal file
350
tests/test_apigateway/test_apigateway_cloudformation.py
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
import boto3
|
||||||
|
import json
|
||||||
|
import sure # noqa
|
||||||
|
|
||||||
|
from moto import mock_lambda, mock_cloudformation, mock_apigateway, mock_iam, mock_logs
|
||||||
|
from string import Template
|
||||||
|
|
||||||
|
template = """{
|
||||||
|
"AWSTemplateFormatVersion": "2010-09-09",
|
||||||
|
"Description": "The AWS CloudFormation template for this Serverless application",
|
||||||
|
"Resources": {
|
||||||
|
"ServerlessDeploymentBucket": {
|
||||||
|
"Type": "AWS::S3::Bucket"
|
||||||
|
},
|
||||||
|
"HelloLogGroup": {
|
||||||
|
"Type": "AWS::Logs::LogGroup",
|
||||||
|
"Properties": {
|
||||||
|
"LogGroupName": "/aws/lambda/timeseries-service-dev-hello"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IamRoleLambdaExecution": {
|
||||||
|
"Type": "AWS::IAM::Role",
|
||||||
|
"Properties": {
|
||||||
|
"AssumeRolePolicyDocument": {
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Principal": {
|
||||||
|
"Service": [
|
||||||
|
"lambda.amazonaws.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Action": [
|
||||||
|
"sts:AssumeRole"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Policies": [
|
||||||
|
{
|
||||||
|
"PolicyName": {
|
||||||
|
"Fn::Join": [
|
||||||
|
"-",
|
||||||
|
[
|
||||||
|
"dev",
|
||||||
|
"timeseries-service",
|
||||||
|
"lambda"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PolicyDocument": {
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"logs:CreateLogStream"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
{
|
||||||
|
"Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/timeseries-service-dev-hello:*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"logs:PutLogEvents"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
{
|
||||||
|
"Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/timeseries-service-dev-hello:*:*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Path": "/",
|
||||||
|
"RoleName": {
|
||||||
|
"Fn::Join": [
|
||||||
|
"-",
|
||||||
|
[
|
||||||
|
"timeseries-service",
|
||||||
|
"dev",
|
||||||
|
"us-east-1",
|
||||||
|
"lambdaRole"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HelloLambdaFunction": {
|
||||||
|
"Type": "AWS::Lambda::Function",
|
||||||
|
"Properties": {
|
||||||
|
"Code": {
|
||||||
|
"S3Bucket": {
|
||||||
|
"Ref": "ServerlessDeploymentBucket"
|
||||||
|
},
|
||||||
|
"S3Key": "serverless/timeseries-service/dev/1542744572309-2018-11-20T20:09:32.309Z/timeseries-service.zip"
|
||||||
|
},
|
||||||
|
"FunctionName": "timeseries-service-dev-hello",
|
||||||
|
"Handler": "handler.hello",
|
||||||
|
"MemorySize": 1024,
|
||||||
|
"Role": {
|
||||||
|
"Fn::GetAtt": [
|
||||||
|
"IamRoleLambdaExecution",
|
||||||
|
"Arn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Runtime": "python2.7",
|
||||||
|
"Timeout": 6
|
||||||
|
},
|
||||||
|
"DependsOn": [
|
||||||
|
"HelloLogGroup",
|
||||||
|
"IamRoleLambdaExecution"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"HelloLambdaVersionU88Ag36tX5K6Yuze3R8jedH2g7q2TTGuafWQxEnUmo": {
|
||||||
|
"Type": "AWS::Lambda::Version",
|
||||||
|
"DeletionPolicy": "Retain",
|
||||||
|
"Properties": {
|
||||||
|
"FunctionName": {
|
||||||
|
"Ref": "HelloLambdaFunction"
|
||||||
|
},
|
||||||
|
"CodeSha256": "+pq+8RveA979z1DNF8UKnFGZfgE07blNyJGust5VJnU="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ApiGatewayRestApi": {
|
||||||
|
"Type": "AWS::ApiGateway::RestApi",
|
||||||
|
"Properties": {
|
||||||
|
"Name": "dev-timeseries-service",
|
||||||
|
"EndpointConfiguration": {
|
||||||
|
"Types": [
|
||||||
|
"EDGE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ApiGatewayResourceHello": {
|
||||||
|
"Type": "AWS::ApiGateway::Resource",
|
||||||
|
"Properties": {
|
||||||
|
"ParentId": {
|
||||||
|
"Fn::GetAtt": [
|
||||||
|
"ApiGatewayRestApi",
|
||||||
|
"RootResourceId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"PathPart": "hello",
|
||||||
|
"RestApiId": {
|
||||||
|
"Ref": "ApiGatewayRestApi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ApiGatewayMethodHelloGet": {
|
||||||
|
"Type": "AWS::ApiGateway::Method",
|
||||||
|
"Properties": {
|
||||||
|
"HttpMethod": "GET",
|
||||||
|
"RequestParameters": {},
|
||||||
|
"ResourceId": {
|
||||||
|
"Ref": "ApiGatewayResourceHello"
|
||||||
|
},
|
||||||
|
"RestApiId": {
|
||||||
|
"Ref": "ApiGatewayRestApi"
|
||||||
|
},
|
||||||
|
"ApiKeyRequired": false,
|
||||||
|
"AuthorizationType": "NONE",
|
||||||
|
"Integration": {
|
||||||
|
"IntegrationHttpMethod": "POST",
|
||||||
|
"Type": "AWS_PROXY",
|
||||||
|
"Uri": {
|
||||||
|
"Fn::Join": [
|
||||||
|
"",
|
||||||
|
[
|
||||||
|
"arn:",
|
||||||
|
{
|
||||||
|
"Ref": "AWS::Partition"
|
||||||
|
},
|
||||||
|
":apigateway:",
|
||||||
|
{
|
||||||
|
"Ref": "AWS::Region"
|
||||||
|
},
|
||||||
|
":lambda:path/2015-03-31/functions/",
|
||||||
|
{
|
||||||
|
"Fn::GetAtt": [
|
||||||
|
"HelloLambdaFunction",
|
||||||
|
"Arn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"/invocations"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"MethodResponses": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ApiGatewayDeployment1542744572805": {
|
||||||
|
"Type": "AWS::ApiGateway::Deployment",
|
||||||
|
"Properties": {
|
||||||
|
"RestApiId": {
|
||||||
|
"Ref": "ApiGatewayRestApi"
|
||||||
|
},
|
||||||
|
"StageName": "dev"
|
||||||
|
},
|
||||||
|
"DependsOn": [
|
||||||
|
"ApiGatewayMethodHelloGet"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"HelloLambdaPermissionApiGateway": {
|
||||||
|
"Type": "AWS::Lambda::Permission",
|
||||||
|
"Properties": {
|
||||||
|
"FunctionName": {
|
||||||
|
"Fn::GetAtt": [
|
||||||
|
"HelloLambdaFunction",
|
||||||
|
"Arn"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Action": "lambda:InvokeFunction",
|
||||||
|
"Principal": {
|
||||||
|
"Fn::Join": [
|
||||||
|
"",
|
||||||
|
[
|
||||||
|
"apigateway.",
|
||||||
|
{
|
||||||
|
"Ref": "AWS::URLSuffix"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"SourceArn": {
|
||||||
|
"Fn::Join": [
|
||||||
|
"",
|
||||||
|
[
|
||||||
|
"arn:",
|
||||||
|
{
|
||||||
|
"Ref": "AWS::Partition"
|
||||||
|
},
|
||||||
|
":execute-api:",
|
||||||
|
{
|
||||||
|
"Ref": "AWS::Region"
|
||||||
|
},
|
||||||
|
":",
|
||||||
|
{
|
||||||
|
"Ref": "AWS::AccountId"
|
||||||
|
},
|
||||||
|
":",
|
||||||
|
{
|
||||||
|
"Ref": "ApiGatewayRestApi"
|
||||||
|
},
|
||||||
|
"/*/*"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Outputs": {
|
||||||
|
"ServerlessDeploymentBucketName": {
|
||||||
|
"Value": {
|
||||||
|
"Ref": "ServerlessDeploymentBucket"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HelloLambdaFunctionQualifiedArn": {
|
||||||
|
"Description": "Current Lambda function version",
|
||||||
|
"Value": {
|
||||||
|
"Ref": "HelloLambdaVersionU88Ag36tX5K6Yuze3R8jedH2g7q2TTGuafWQxEnUmo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ServiceEndpoint": {
|
||||||
|
"Description": "URL of the service endpoint",
|
||||||
|
"Value": {
|
||||||
|
"Fn::Join": [
|
||||||
|
"",
|
||||||
|
[
|
||||||
|
"https://",
|
||||||
|
{
|
||||||
|
"Ref": "ApiGatewayRestApi"
|
||||||
|
},
|
||||||
|
".execute-api.us-east-1.",
|
||||||
|
{
|
||||||
|
"Ref": "AWS::URLSuffix"
|
||||||
|
},
|
||||||
|
"/dev"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
|
||||||
|
|
||||||
|
@mock_cloudformation
|
||||||
|
@mock_lambda
|
||||||
|
@mock_iam
|
||||||
|
@mock_logs
|
||||||
|
@mock_apigateway
|
||||||
|
def test_simple_apigateway_with_lambda_proxy():
|
||||||
|
region = "us-east-1"
|
||||||
|
apigw = boto3.client("apigateway", region_name=region)
|
||||||
|
cf = boto3.client("cloudformation", region_name=region)
|
||||||
|
awslambda = boto3.client("lambda", region_name=region)
|
||||||
|
cf.create_stack(StackName="teststack", TemplateBody=template)
|
||||||
|
#
|
||||||
|
cf.describe_stacks(StackName="teststack")["Stacks"]
|
||||||
|
resources = cf.describe_stack_resources(StackName="teststack")["StackResources"]
|
||||||
|
api_id = [
|
||||||
|
r["PhysicalResourceId"]
|
||||||
|
for r in resources
|
||||||
|
if r["ResourceType"] == "AWS::ApiGateway::RestApi"
|
||||||
|
][0]
|
||||||
|
fn_name = [
|
||||||
|
r["PhysicalResourceId"]
|
||||||
|
for r in resources
|
||||||
|
if r["LogicalResourceId"] == "HelloLambdaFunction"
|
||||||
|
][0]
|
||||||
|
#
|
||||||
|
# Verify Rest API was created
|
||||||
|
api = apigw.get_rest_apis()["items"][0]
|
||||||
|
api["id"].should.equal(api_id)
|
||||||
|
api["name"].should.equal("dev-timeseries-service")
|
||||||
|
#
|
||||||
|
# Verify Gateway Resource was created
|
||||||
|
paths = apigw.get_resources(restApiId=api_id)["items"]
|
||||||
|
root_path = [p for p in paths if p["path"] == "/"][0]
|
||||||
|
hello_path = [p for p in paths if p["path"] == "/hello"][0]
|
||||||
|
hello_path["parentId"].should.equal(root_path["id"])
|
||||||
|
#
|
||||||
|
# Verify Gateway Method was created
|
||||||
|
m = apigw.get_method(
|
||||||
|
restApiId=api_id, resourceId=hello_path["id"], httpMethod="GET"
|
||||||
|
)
|
||||||
|
m["httpMethod"].should.equal("GET")
|
||||||
|
#
|
||||||
|
# Verify a Gateway Deployment was created
|
||||||
|
d = apigw.get_deployments(restApiId=api_id)["items"]
|
||||||
|
d.should.have.length_of(1)
|
||||||
|
#
|
||||||
|
# Verify Lambda function was created
|
||||||
|
awslambda.get_function(FunctionName=fn_name) # Will throw 404 if it doesn't exist
|
||||||
|
#
|
||||||
|
# Verify Lambda Permission was created
|
||||||
|
policy = json.loads(awslambda.get_policy(FunctionName=fn_name)["Policy"])
|
||||||
|
statement = policy["Statement"][0]
|
||||||
|
statement["FunctionName"].should.contain(fn_name)
|
||||||
|
statement["Condition"]["ArnLike"]["AWS:SourceArn"].should.equal(
|
||||||
|
"arn:aws:execute-api:us-east-1:123456789012:{}/*/*".format(api_id)
|
||||||
|
)
|
@ -14,7 +14,7 @@ def test_list_apis():
|
|||||||
test_client = backend.test_client()
|
test_client = backend.test_client()
|
||||||
|
|
||||||
res = test_client.get("/restapis")
|
res = test_client.get("/restapis")
|
||||||
res.data.should.equal(b'{"item": []}')
|
json.loads(res.data).should.contain("item")
|
||||||
|
|
||||||
|
|
||||||
def test_usage_plans_apis():
|
def test_usage_plans_apis():
|
||||||
|
Loading…
Reference in New Issue
Block a user