Cloudformation - Create ApiGateway resources (#3659)

* Cloudformation - Create ApiGateway resources

* Cleanup

* Linting
This commit is contained in:
Bert Blommers 2021-02-15 10:31:33 +00:00 committed by GitHub
commit f64532ed40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 541 additions and 32 deletions

View file

@ -16,7 +16,7 @@ try:
except ImportError:
from urllib.parse import urlparse
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 moto.core.utils import path_url
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}"
class Deployment(BaseModel, dict):
class Deployment(CloudFormationModel, dict):
def __init__(self, deployment_id, name, description=""):
super(Deployment, self).__init__()
self["id"] = deployment_id
@ -57,6 +57,27 @@ class Deployment(BaseModel, dict):
self["description"] = description
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):
def __init__(
@ -109,7 +130,7 @@ class MethodResponse(BaseModel, dict):
self["statusCode"] = status_code
class Method(BaseModel, dict):
class Method(CloudFormationModel, dict):
def __init__(self, method_type, authorization_type, **kwargs):
super(Method, self).__init__()
self.update(
@ -125,6 +146,45 @@ class Method(BaseModel, dict):
)
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):
method_response = MethodResponse(response_code)
self.method_responses[response_code] = method_response
@ -137,8 +197,9 @@ class Method(BaseModel, dict):
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):
super(Resource, self).__init__()
self.id = id
self.region_name = region_name
self.api_id = api_id
@ -158,6 +219,39 @@ class Resource(BaseModel):
response["pathPart"] = self.path_part
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):
return self.get_parent_path() + self.path_part
@ -473,8 +567,9 @@ class UsagePlanKey(BaseModel, dict):
self["value"] = value
class RestAPI(BaseModel):
class RestAPI(CloudFormationModel):
def __init__(self, id, region_name, name, description, **kwargs):
super(RestAPI, self).__init__()
self.id = id
self.region_name = region_name
self.name = name
@ -509,6 +604,38 @@ class RestAPI(BaseModel):
"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):
child_id = create_id()
child = Resource(
@ -1003,7 +1130,8 @@ class APIGatewayBackend(BaseBackend):
methods = [
list(res.resource_methods.values())
for res in self.list_resources(function_id)
][0]
]
methods = [m for sublist in methods for m in sublist]
if not any(methods):
raise NoMethodDefined()
method_integrations = [

View file

@ -1,31 +1,33 @@
from __future__ import unicode_literals
from .responses import APIGatewayResponse
response = APIGatewayResponse()
url_bases = ["https?://apigateway.(.+).amazonaws.com"]
url_paths = {
"{0}/restapis$": APIGatewayResponse().restapis,
"{0}/restapis/(?P<function_id>[^/]+)/?$": APIGatewayResponse().restapis_individual,
"{0}/restapis/(?P<function_id>[^/]+)/resources$": APIGatewayResponse().resources,
"{0}/restapis/(?P<function_id>[^/]+)/authorizers$": APIGatewayResponse().restapis_authorizers,
"{0}/restapis/(?P<function_id>[^/]+)/authorizers/(?P<authorizer_id>[^/]+)/?$": APIGatewayResponse().authorizers,
"{0}/restapis/(?P<function_id>[^/]+)/stages$": APIGatewayResponse().restapis_stages,
"{0}/restapis/(?P<function_id>[^/]+)/stages/(?P<stage_name>[^/]+)/?$": APIGatewayResponse().stages,
"{0}/restapis/(?P<function_id>[^/]+)/deployments$": APIGatewayResponse().deployments,
"{0}/restapis/(?P<function_id>[^/]+)/deployments/(?P<deployment_id>[^/]+)/?$": APIGatewayResponse().individual_deployment,
"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/?$": APIGatewayResponse().resource_individual,
"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/?$": APIGatewayResponse().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,
"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/integration/?$": APIGatewayResponse().integrations,
r"{0}/restapis/(?P<function_id>[^/]+)/resources/(?P<resource_id>[^/]+)/methods/(?P<method_name>[^/]+)/integration/responses/(?P<status_code>\d+)/?$": APIGatewayResponse().integration_responses,
"{0}/apikeys$": APIGatewayResponse().apikeys,
"{0}/apikeys/(?P<apikey>[^/]+)": APIGatewayResponse().apikey_individual,
"{0}/usageplans$": APIGatewayResponse().usage_plans,
"{0}/domainnames$": APIGatewayResponse().domain_names,
"{0}/restapis/(?P<function_id>[^/]+)/models$": APIGatewayResponse().models,
"{0}/restapis/(?P<function_id>[^/]+)/models/(?P<model_name>[^/]+)/?$": APIGatewayResponse().model_induvidual,
"{0}/domainnames/(?P<domain_name>[^/]+)/?$": APIGatewayResponse().domain_name_induvidual,
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/?$": APIGatewayResponse().usage_plan_individual,
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/keys$": APIGatewayResponse().usage_plan_keys,
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/keys/(?P<api_key_id>[^/]+)/?$": APIGatewayResponse().usage_plan_key_individual,
"{0}/restapis$": response.restapis,
"{0}/restapis/(?P<function_id>[^/]+)/?$": response.restapis_individual,
"{0}/restapis/(?P<function_id>[^/]+)/resources$": response.resources,
"{0}/restapis/(?P<function_id>[^/]+)/authorizers$": response.restapis_authorizers,
"{0}/restapis/(?P<function_id>[^/]+)/authorizers/(?P<authorizer_id>[^/]+)/?$": response.authorizers,
"{0}/restapis/(?P<function_id>[^/]+)/stages$": response.restapis_stages,
"{0}/restapis/(?P<function_id>[^/]+)/stages/(?P<stage_name>[^/]+)/?$": response.stages,
"{0}/restapis/(?P<function_id>[^/]+)/deployments$": response.deployments,
"{0}/restapis/(?P<function_id>[^/]+)/deployments/(?P<deployment_id>[^/]+)/?$": response.individual_deployment,
"{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>[^/]+)/?$": response.resource_methods,
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/?$": response.integrations,
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$": response.apikeys,
"{0}/apikeys/(?P<apikey>[^/]+)": response.apikey_individual,
"{0}/usageplans$": response.usage_plans,
"{0}/domainnames$": response.domain_names,
"{0}/restapis/(?P<function_id>[^/]+)/models$": response.models,
"{0}/restapis/(?P<function_id>[^/]+)/models/(?P<model_name>[^/]+)/?$": response.model_induvidual,
"{0}/domainnames/(?P<domain_name>[^/]+)/?$": response.domain_name_induvidual,
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/?$": response.usage_plan_individual,
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/keys$": response.usage_plan_keys,
"{0}/usageplans/(?P<usage_plan_id>[^/]+)/keys/(?P<api_key_id>[^/]+)/?$": response.usage_plan_key_individual,
}

View file

@ -185,6 +185,29 @@ def _validate_s3_bucket_and_key(data):
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):
def __init__(self, spec, region):
# required
@ -316,7 +339,6 @@ class LambdaFunction(CloudFormationModel, DockerModel):
self.layers = self._get_layers_data(spec.get("Layers", []))
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
self._vpc_config = spec.get(
@ -341,6 +363,10 @@ class LambdaFunction(CloudFormationModel, DockerModel):
self.code_bytes = key.value
self.code_size = key.size
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.region, ACCOUNT_ID, self.function_name
@ -510,6 +536,8 @@ class LambdaFunction(CloudFormationModel, DockerModel):
return s
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
if event is None:
event = dict()

View file

@ -15,6 +15,7 @@ from moto.compat import collections_abc
# the subclass's module hasn't been imported yet - then that subclass
# doesn't exist yet, and __subclasses__ won't find it.
# 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.awslambda import models as awslambda_models # noqa
from moto.batch import models as batch_models # noqa