Skip to content

Commit b302f29

Browse files
authored
apigateway - refactor store (#7750)
1 parent 1d93107 commit b302f29

File tree

7 files changed

+457
-318
lines changed

7 files changed

+457
-318
lines changed

localstack/services/apigateway/helpers.py

Lines changed: 34 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
from botocore.utils import InvalidArnException
1212
from jsonpatch import apply_patch
1313
from jsonpointer import JsonPointerException
14-
from moto.apigateway.models import Authorizer, Integration, Resource, RestAPI, apigateway_backends
14+
from moto.apigateway.models import Integration, Resource, RestAPI, apigateway_backends
1515
from moto.apigateway.utils import create_id as create_resource_id
1616
from requests.models import Response
1717

1818
from localstack import config
1919
from localstack.aws.accounts import get_aws_account_id
20+
from localstack.aws.api.apigateway import Authorizer
2021
from localstack.constants import (
2122
APPLICATION_JSON,
2223
HEADER_LOCALSTACK_EDGE_URL,
@@ -279,127 +280,11 @@ def get_stage_variables(context: ApiInvocationContext) -> Optional[Dict[str, str
279280
return {}
280281

281282

282-
# -----------------------
283-
# GATEWAY RESPONSES APIs
284-
# -----------------------
285-
286-
287-
# TODO: merge with to_response_json(..) above
288-
def gateway_response_to_response_json(item, api_id):
289-
base_path = "/restapis/%s/gatewayresponses" % api_id
290-
item["_links"] = {
291-
"self": {"href": "%s/%s" % (base_path, item["responseType"])},
292-
"gatewayresponse:put": {
293-
"href": "%s/{response_type}" % base_path,
294-
"templated": True,
295-
},
296-
"gatewayresponse:update": {"href": "%s/%s" % (base_path, item["responseType"])},
297-
}
298-
item["responseParameters"] = item.get("responseParameters", {})
299-
item["responseTemplates"] = item.get("responseTemplates", {})
300-
return item
301-
302-
303-
def get_gateway_responses(api_id):
304-
region_details = get_apigateway_store()
305-
result = region_details.gateway_responses.get(api_id, [])
306-
307-
href = "http://docs.aws.amazon.com/apigateway/latest/developerguide/restapi-gatewayresponse-{rel}.html"
308-
base_path = "/restapis/%s/gatewayresponses" % api_id
309-
310-
result = {
311-
"_links": {
312-
"curies": {"href": href, "name": "gatewayresponse", "templated": True},
313-
"self": {"href": base_path},
314-
"first": {"href": base_path},
315-
"gatewayresponse:by-type": {
316-
"href": "%s/{response_type}" % base_path,
317-
"templated": True,
318-
},
319-
"item": [{"href": "%s/%s" % (base_path, r["responseType"])} for r in result],
320-
},
321-
"_embedded": {"item": [gateway_response_to_response_json(i, api_id) for i in result]},
322-
# Note: Looks like the format required by aws CLI ("item" at top level) differs from the docs:
323-
# https://docs.aws.amazon.com/apigateway/api-reference/resource/gateway-responses/
324-
"item": [gateway_response_to_response_json(i, api_id) for i in result],
325-
}
326-
return result
327-
328-
329-
def get_gateway_response(api_id, response_type):
330-
region_details = get_apigateway_store()
331-
responses = region_details.gateway_responses.get(api_id, [])
332-
if result := [r for r in responses if r["responseType"] == response_type]:
333-
return result[0]
334-
return make_error_response(
335-
"Gateway response %s for API Gateway %s not found" % (response_type, api_id),
336-
code=404,
337-
)
338-
339-
340-
def put_gateway_response(api_id, response_type, data):
341-
region_details = get_apigateway_store()
342-
responses = region_details.gateway_responses.setdefault(api_id, [])
343-
if existing := ([r for r in responses if r["responseType"] == response_type] or [None])[0]:
344-
existing.update(data)
345-
else:
346-
data["responseType"] = response_type
347-
responses.append(data)
348-
return data
349-
350-
351-
def delete_gateway_response(api_id, response_type):
352-
region_details = get_apigateway_store()
353-
responses = region_details.gateway_responses.get(api_id) or []
354-
region_details.gateway_responses[api_id] = [
355-
r for r in responses if r["responseType"] != response_type
356-
]
357-
return make_accepted_response()
358-
359-
360-
def update_gateway_response(api_id, response_type, data):
361-
region_details = get_apigateway_store()
362-
responses = region_details.gateway_responses.setdefault(api_id, [])
363-
364-
existing = ([r for r in responses if r["responseType"] == response_type] or [None])[0]
365-
if existing is None:
366-
return make_error_response(
367-
"Gateway response %s for API Gateway %s not found" % (response_type, api_id),
368-
code=404,
369-
)
370-
return apply_json_patch_safe(existing, data["patchOperations"])
371-
372-
373-
def handle_gateway_responses(method, path, data, headers):
374-
search_match = re.search(PATH_REGEX_RESPONSES, path)
375-
api_id = search_match.group(1)
376-
response_type = (search_match.group(2) or "").lstrip("/")
377-
if method == "GET":
378-
if response_type:
379-
return get_gateway_response(api_id, response_type)
380-
return get_gateway_responses(api_id)
381-
if method == "PUT":
382-
return put_gateway_response(api_id, response_type, data)
383-
if method == "PATCH":
384-
return update_gateway_response(api_id, response_type, data)
385-
if method == "DELETE":
386-
return delete_gateway_response(api_id, response_type)
387-
return make_error_response(
388-
"Not implemented for API Gateway gateway responses: %s" % method, code=404
389-
)
390-
391-
392283
# ---------------
393284
# UTIL FUNCTIONS
394285
# ---------------
395286

396287

397-
def find_api_subentity_by_id(api_id, entity_id, map_name):
398-
region_details = get_apigateway_store()
399-
auth_list = getattr(region_details, map_name).get(api_id) or []
400-
return ([a for a in auth_list if a["id"] == entity_id] or [None])[0]
401-
402-
403288
def path_based_url(api_id: str, stage_name: str, path: str) -> str:
404289
"""Return URL for inbound API gateway for given API ID, stage name, and path"""
405290
pattern = "%s/restapis/{api_id}/{stage_name}/%s{path}" % (
@@ -636,7 +521,9 @@ def apply_json_patch_safe(subject, patch_operations, in_place=True, return_list=
636521
return (results or [subject])[-1]
637522

638523

639-
def import_api_from_openapi_spec(rest_api: RestAPI, body: Dict, query_params: Dict) -> RestAPI:
524+
def import_api_from_openapi_spec(
525+
rest_api: RestAPI, body: Dict, query_params: Dict, account_id: str = None, region: str = None
526+
) -> Optional[RestAPI]:
640527
"""Import an API from an OpenAPI spec document"""
641528

642529
resolved_schema = resolve_references(body)
@@ -657,9 +544,10 @@ def import_api_from_openapi_spec(rest_api: RestAPI, body: Dict, query_params: Di
657544
# authorizers map to avoid duplication
658545
authorizers = {}
659546

660-
region_details = get_apigateway_store()
547+
store = get_apigateway_store(account_id=account_id, region=region)
548+
rest_api_container = store.rest_apis[rest_api.id]
661549

662-
def create_authorizer(path_payload: dict) -> Authorizer:
550+
def create_authorizer(path_payload: dict) -> Optional[Authorizer]:
663551
if "security" not in path_payload:
664552
return None
665553

@@ -678,29 +566,34 @@ def create_authorizer(path_payload: dict) -> Authorizer:
678566
if authorizers.get(security_scheme_name):
679567
return authorizers.get(security_scheme_name)
680568

569+
# TODO: do we need validation of resources here?
681570
authorizer = Authorizer(
682-
authorizer_id=create_resource_id(),
571+
id=create_resource_id(),
683572
name=security_scheme_name,
684-
authorizer_type=aws_apigateway_authorizer.get("type", "").upper(),
685-
provider_arns=aws_apigateway_authorizer.get("providerARNs"),
686-
auth_type=security_config.get("x-amazon-apigateway-authtype"),
687-
authorizer_uri=aws_apigateway_authorizer.get("authorizerUri"),
688-
authorizer_credentials=aws_apigateway_authorizer.get(
689-
"authorizerCredentials"
690-
),
691-
identity_source=aws_apigateway_authorizer.get("identitySource"),
692-
identiy_validation_expression=aws_apigateway_authorizer.get(
693-
"identityValidationExpression"
573+
type=aws_apigateway_authorizer.get("type", "").upper(),
574+
authorizerResultTtlInSeconds=aws_apigateway_authorizer.get(
575+
"authorizerResultTtlInSeconds", 300
694576
),
695-
authorizer_result_ttl=aws_apigateway_authorizer.get(
696-
"authorizerResultTtlInSeconds"
697-
)
698-
or 300,
699577
)
578+
if provider_arns := aws_apigateway_authorizer.get("providerARNs"):
579+
authorizer["providerARNs"] = provider_arns
580+
if auth_type := security_config.get("x-amazon-apigateway-authtype"):
581+
authorizer["authType"] = auth_type
582+
if authorizer_uri := aws_apigateway_authorizer.get("authorizerUri"):
583+
authorizer["authorizerUri"] = authorizer_uri
584+
if authorizer_credentials := aws_apigateway_authorizer.get(
585+
"authorizerCredentials"
586+
):
587+
authorizer["authorizerCredentials"] = authorizer_credentials
588+
if identity_source := aws_apigateway_authorizer.get("identitySource"):
589+
authorizer["identitySource"] = identity_source
590+
if identity_validation_expression := aws_apigateway_authorizer.get(
591+
"identityValidationExpression"
592+
):
593+
authorizer["identityValidationExpression"] = identity_validation_expression
594+
595+
rest_api_container.authorizers[authorizer["id"]] = authorizer
700596

701-
region_details.authorizers.setdefault(rest_api.id, []).append(
702-
authorizer.to_json()
703-
)
704597
authorizers[security_scheme_name] = authorizer
705598
return authorizer
706599

@@ -791,15 +684,16 @@ def add_path_methods(rel_path: str, parts: List[str], parent_id=""):
791684
resource.resource_methods[field].method_integration = integration
792685

793686
rest_api.resources[child_id] = resource
687+
rest_api_container.resource_children.setdefault(parent_id, []).append(child_id)
794688
return resource
795689

796690
def create_method_resource(child, method, method_schema):
797691
return (
798692
child.add_method(
799693
method,
800-
authorization_type=authorizer.type,
694+
authorization_type=authorizer["type"],
801695
api_key_required=None,
802-
authorizer_id=authorizer.id,
696+
authorizer_id=authorizer["id"],
803697
)
804698
if (authorizer := create_authorizer(method_schema))
805699
else child.add_method(method, None, None)

localstack/services/apigateway/models.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
from typing import Any, Dict, List
22

3+
from requests.structures import CaseInsensitiveDict
4+
5+
from localstack.aws.api.apigateway import (
6+
Authorizer,
7+
DocumentationPart,
8+
GatewayResponse,
9+
Model,
10+
RequestValidator,
11+
RestApi,
12+
)
313
from localstack.services.stores import (
414
AccountRegionBundle,
515
BaseStore,
@@ -9,22 +19,36 @@
919
from localstack.utils.aws import arns
1020

1121

12-
class ApiGatewayStore(BaseStore):
13-
# TODO: introduce a RestAPI class to encapsulate the variables below
14-
# maps (API id) -> [authorizers]
15-
authorizers: Dict[str, List[Dict]] = LocalAttribute(default=dict)
16-
17-
# maps (API id) -> [validators]
18-
validators: Dict[str, List[Dict]] = LocalAttribute(default=dict)
22+
class RestApiContainer:
23+
# contains the RestApi dictionary. We're not making use of it yet, still using moto data.
24+
rest_api: RestApi
25+
# maps AuthorizerId -> Authorizer
26+
authorizers: Dict[str, Authorizer]
27+
# maps RequestValidatorId -> RequestValidator
28+
validators: Dict[str, RequestValidator]
29+
# map DocumentationPartId -> DocumentationPart
30+
documentation_parts: Dict[str, DocumentationPart]
31+
# not used yet, still in moto
32+
gateway_responses: Dict[str, GatewayResponse]
33+
# not used yet, still in moto
34+
models: Dict[str, Model]
35+
# maps ResourceId of a Resource to its children ResourceIds
36+
resource_children: Dict[str, List[str]]
37+
38+
def __init__(self, rest_api: RestApi):
39+
self.rest_api = rest_api
40+
self.authorizers = {}
41+
self.validators = {}
42+
self.documentation_parts = {}
43+
self.gateway_responses = {}
44+
self.models = {}
45+
self.resource_children = {}
1946

20-
# maps (API id) -> [documentation_parts]
21-
documentation_parts: Dict[str, List[Dict]] = LocalAttribute(default=dict)
2247

23-
# maps (API id) -> [gateway_responses]
24-
gateway_responses: Dict[str, List[Dict]] = LocalAttribute(default=dict)
25-
26-
# maps (API id) -> Resource id -> its children Resource id
27-
resources_children: Dict[str, Dict[str, List[str]]] = LocalAttribute(default=dict)
48+
class ApiGatewayStore(BaseStore):
49+
# maps (API id) -> RestApiContainer
50+
# TODO: remove CaseInsensitiveDict, and lower the value of the ID when getting it from the tags
51+
rest_apis: Dict[str, RestApiContainer] = LocalAttribute(default=CaseInsensitiveDict)
2852

2953
# account details
3054
account: Dict[str, Any] = LocalAttribute(default=dict)

localstack/services/apigateway/patches.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ def create_rest_api(fn, self, *args, tags=None, **kwargs):
332332
"""
333333
tags = tags or {}
334334
result = fn(self, *args, tags=tags, **kwargs)
335+
# TODO: lower the custom_id when getting it from the tags, as AWS is case insensitive
335336
if custom_id := tags.get(TAG_KEY_CUSTOM_ID):
336337
self.apis.pop(result.id)
337338
result.id = custom_id

0 commit comments

Comments
 (0)