Skip to content

Commit

Permalink
fix APIGW: add rootResourceId to REST API creation (#10665)
Browse files Browse the repository at this point in the history
  • Loading branch information
bentsku committed Apr 17, 2024
1 parent 2dd0475 commit 0c8f87b
Show file tree
Hide file tree
Showing 22 changed files with 1,583 additions and 1,423 deletions.
12 changes: 8 additions & 4 deletions localstack/services/apigateway/helpers.py
Expand Up @@ -937,9 +937,6 @@ def import_api_from_openapi_spec(
# rest_api.name = resolved_schema.get("info", {}).get("title")
rest_api.description = resolved_schema.get("info", {}).get("description")

# Remove default root, then add paths from API spec
# TODO: the default mode is now `merge`, not `overwrite` if using `PutRestApi`
rest_api.resources = {}
# authorizers map to avoid duplication
authorizers = {}

Expand Down Expand Up @@ -1355,7 +1352,14 @@ def create_method_resource(child, method, method_schema):
base_path = base_path.strip("/").partition("/")[-1]
base_path = f"/{base_path}" if base_path else ""

for path in resolved_schema.get("paths", {}):
api_paths = resolved_schema.get("paths", {})
if api_paths:
# Remove default root, then add paths from API spec
# TODO: the default mode is now `merge`, not `overwrite` if using `PutRestApi`
# TODO: quick hack for now, but do not remove the rootResource if the OpenAPI file is empty
rest_api.resources = {}

for path in api_paths:
get_or_create_path(base_path + path, base_path=base_path)

# binary types
Expand Down
44 changes: 42 additions & 2 deletions localstack/services/apigateway/provider.py
Expand Up @@ -246,6 +246,9 @@ def create_rest_api(self, context: RequestContext, request: CreateRestApiRequest
rest_api = get_moto_rest_api(context, rest_api_id=result["id"])
rest_api.version = request.get("version")
response: RestApi = rest_api.to_dict()
# TODO: remove once this is fixed upstream
if "rootResourceId" not in response:
response["rootResourceId"] = get_moto_rest_api_root_resource(rest_api)
remove_empty_attributes_from_rest_api(response)
store = get_apigateway_store(context=context)
rest_api_container = RestApiContainer(rest_api=response)
Expand Down Expand Up @@ -281,6 +284,10 @@ def create_api_key(
def get_rest_api(self, context: RequestContext, rest_api_id: String, **kwargs) -> RestApi:
rest_api: RestApi = call_moto(context)
remove_empty_attributes_from_rest_api(rest_api)
# TODO: remove once this is fixed upstream
if "rootResourceId" not in rest_api:
moto_rest_api = get_moto_rest_api(context, rest_api_id=rest_api_id)
rest_api["rootResourceId"] = get_moto_rest_api_root_resource(moto_rest_api)
return rest_api

def update_rest_api(
Expand Down Expand Up @@ -326,7 +333,7 @@ def update_rest_api(
elif patch_op_path == "/minimumCompressionSize":
if patch_op["op"] != "replace":
raise BadRequestException(
"Invalid patch operation specified. Must be 'add'|'remove'|'replace'"
"Invalid patch operation specified. Must be one of: [replace]"
)

try:
Expand Down Expand Up @@ -358,6 +365,9 @@ def update_rest_api(
rest_api.minimum_compression_size = None

response = rest_api.to_dict()
if "rootResourceId" not in response:
response["rootResourceId"] = get_moto_rest_api_root_resource(rest_api)

remove_empty_attributes_from_rest_api(response, remove_tags=False)
store = get_apigateway_store(context=context)
store.rest_apis[rest_api_id].rest_api = response
Expand All @@ -376,6 +386,9 @@ def put_rest_api(self, context: RequestContext, request: PutRestApiRequest) -> R
remove_empty_attributes_from_rest_api(response)
store = get_apigateway_store(context=context)
store.rest_apis[request["restApiId"]].rest_api = response
# TODO: remove once this is fixed upstream
if "rootResourceId" not in response:
response["rootResourceId"] = get_moto_rest_api_root_resource(rest_api)
# TODO: verify this
response = to_rest_api_response_json(response)
response.setdefault("tags", {})
Expand Down Expand Up @@ -475,6 +488,9 @@ def get_rest_apis(
response: RestApis = call_moto(context)
for rest_api in response["items"]:
remove_empty_attributes_from_rest_api(rest_api)
if "rootResourceId" not in rest_api:
moto_rest_api = get_moto_rest_api(context, rest_api_id=rest_api["id"])
rest_api["rootResourceId"] = get_moto_rest_api_root_resource(moto_rest_api)
return response

# resources
Expand Down Expand Up @@ -807,7 +823,24 @@ def update_method(
# if the path is not supported by the operation, ignore it and skip
op_supported_path = UPDATE_METHOD_PATCH_PATHS.get(op, [])
if not any(path.startswith(s_path) for s_path in op_supported_path):
continue
available_ops = [
available_op
for available_op in ("add", "replace", "delete")
if available_op != op
]
supported_ops = ", ".join(
[
supported_op
for supported_op in available_ops
if any(
path.startswith(s_path)
for s_path in UPDATE_METHOD_PATCH_PATHS.get(supported_op, [])
)
]
)
raise BadRequestException(
f"Invalid patch operation specified. Must be one of: [{supported_ops}]"
)

value = patch_operation.get("value")
if op not in ("add", "replace"):
Expand Down Expand Up @@ -2512,6 +2545,13 @@ def validate_model_in_use(moto_rest_api: MotoRestAPI, model_name: str) -> None:
)


def get_moto_rest_api_root_resource(moto_rest_api: MotoRestAPI) -> str:
for res_id, res_obj in moto_rest_api.resources.items():
if res_obj.path_part == "/" and not res_obj.parent_id:
return res_id
raise Exception(f"Unable to find root resource for API {moto_rest_api.id}")


def create_custom_context(
context: RequestContext, action: str, parameters: ServiceRequest
) -> RequestContext:
Expand Down
4 changes: 1 addition & 3 deletions localstack/testing/pytest/fixtures.py
Expand Up @@ -1954,10 +1954,8 @@ def _create_apigateway_function(**kwargs):
response = apigateway_client.create_rest_api(**kwargs)
api_id = response.get("id")
rest_apis.append((api_id, region_name))
resources = apigateway_client.get_resources(restApiId=api_id)
root_id = next(item for item in resources["items"] if item["path"] == "/")["id"]

return api_id, response.get("name"), root_id
return api_id, response.get("name"), response.get("rootResourceId")

yield _create_apigateway_function

Expand Down
1 change: 1 addition & 0 deletions localstack/testing/snapshots/transformer_utility.py
Expand Up @@ -156,6 +156,7 @@ def apigateway_api():
TransformerUtility.key_value("id"),
TransformerUtility.key_value("name"),
TransformerUtility.key_value("parentId"),
TransformerUtility.key_value("rootResourceId"),
]

@staticmethod
Expand Down
22 changes: 19 additions & 3 deletions tests/aws/services/apigateway/conftest.py
@@ -1,6 +1,8 @@
import pytest
from botocore.config import Config

from localstack.constants import APPLICATION_JSON
from localstack.testing.aws.util import is_aws_cloud
from localstack.utils.strings import short_uid
from tests.aws.services.apigateway.apigateway_fixtures import (
create_rest_api_deployment,
Expand Down Expand Up @@ -198,18 +200,32 @@ def _factory(rest_api_id: str, stage_name: str):


@pytest.fixture
def import_apigw(aws_client):
def import_apigw(aws_client, aws_client_factory):
rest_api_ids = []

if is_aws_cloud():
client_config = (
Config(
# Api gateway can throttle requests pretty heavily. Leading to potentially undeleted apis
retries={"max_attempts": 10, "mode": "adaptive"}
)
if is_aws_cloud()
else None
)

apigateway_client = aws_client_factory(config=client_config).apigateway
else:
apigateway_client = aws_client.apigateway

def _import_apigateway_function(*args, **kwargs):
response, root_id = import_rest_api(aws_client.apigateway, **kwargs)
response, root_id = import_rest_api(apigateway_client, **kwargs)
rest_api_ids.append(response.get("id"))
return response, root_id

yield _import_apigateway_function

for rest_api_id in rest_api_ids:
delete_rest_api(aws_client.apigateway, restApiId=rest_api_id)
delete_rest_api(apigateway_client, restApiId=rest_api_id)


@pytest.fixture
Expand Down

0 comments on commit 0c8f87b

Please sign in to comment.