Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

move import_rest_api patch into ASF handler #6048

Merged
merged 4 commits into from May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 0 additions & 20 deletions localstack/services/apigateway/patches.py
@@ -1,7 +1,6 @@
import json
import logging
from typing import Dict, Optional, Tuple
from urllib.parse import parse_qs, urlparse

from moto.apigateway import models as apigateway_models
from moto.apigateway.exceptions import NoIntegrationDefined, UsagePlanNotFoundException
Expand Down Expand Up @@ -478,24 +477,6 @@ def apigateway_models_RestAPI_to_dict(self):

return resp

apigateway_response_restapis_orig = APIGatewayResponse.restapis

# https://github.com/localstack/localstack/issues/171
def apigateway_response_restapis(self, request, full_url, headers):
parsed_qs = parse_qs(urlparse(full_url).query)
modes = parsed_qs.get("mode", [])

status, _, rest_api = apigateway_response_restapis_orig(self, request, full_url, headers)

if "import" not in modes:
return status, _, rest_api

function_id = json.loads(rest_api)["id"]
body = parse_json_or_yaml(request.data.decode("utf-8"))
self.backend.put_rest_api(function_id, body, parsed_qs)

return 200, {}, rest_api

def individual_deployment(self, request, full_url, headers, *args, **kwargs):
result = individual_deployment_orig(self, request, full_url, headers, *args, **kwargs)
if self.method == "PATCH" and len(result) >= 3 and result[2] in ["null", None, str(None)]:
Expand Down Expand Up @@ -541,4 +522,3 @@ def create_rest_api(self, *args, tags={}, **kwargs):
apigateway_models_Integration_init_orig = apigateway_models.Integration.__init__
apigateway_models.Integration.__init__ = apigateway_models_Integration_init
apigateway_models.RestAPI.to_dict = apigateway_models_RestAPI_to_dict
APIGatewayResponse.restapis = apigateway_response_restapis
51 changes: 49 additions & 2 deletions localstack/services/apigateway/provider.py
Expand Up @@ -2,14 +2,15 @@
import re
from copy import deepcopy

from localstack.aws.api import RequestContext, handler
from localstack.aws.api import RequestContext, ServiceRequest, handler
from localstack.aws.api.apigateway import (
Account,
ApigatewayApi,
Authorizer,
Authorizers,
BasePathMapping,
BasePathMappings,
Blob,
Boolean,
ClientCertificate,
ClientCertificates,
Expand All @@ -24,6 +25,7 @@
MapOfStringToString,
NotFoundException,
NullableInteger,
PutRestApiRequest,
RequestValidator,
RequestValidators,
RestApi,
Expand All @@ -32,6 +34,7 @@
VpcLink,
VpcLinks,
)
from localstack.aws.forwarder import create_aws_request_context
from localstack.aws.proxy import AwsApiListener
from localstack.constants import HEADER_LOCALSTACK_EDGE_URL
from localstack.services.apigateway import helpers
Expand All @@ -53,7 +56,7 @@
from localstack.utils.aws.aws_responses import requests_response
from localstack.utils.collections import ensure_list
from localstack.utils.json import parse_json_or_yaml
from localstack.utils.strings import short_uid, to_str
from localstack.utils.strings import short_uid, str_to_bool, to_str
from localstack.utils.time import now_utc


Expand Down Expand Up @@ -638,12 +641,56 @@ def untag_resource(
for key in tag_keys:
resource_tags.pop(key, None)

def import_rest_api(
self,
context: RequestContext,
body: Blob,
fail_on_warnings: Boolean = None,
parameters: MapOfStringToString = None,
) -> RestApi:

openapi_spec = parse_json_or_yaml(to_str(body))
response = _call_moto(
context,
"CreateRestApi",
CreateRestApiRequest(name=openapi_spec.get("info").get("title")),
)

return _call_moto(
context,
"PutRestApi",
PutRestApiRequest(
restApiId=response.get("id"),
failOnWarnings=str_to_bool(fail_on_warnings) or False,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity - can the runtime type of fail_on_warnings be str here, did you actually observe this behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a string and easy observable running the following command.

awslocal apigateway import-rest-api --cli-binary-format raw-in-base64-out --fail-on-warnings --body 'file:///spec.json

parameters=parameters or {},
body=body,
),
)


# ---------------
# UTIL FUNCTIONS
# ---------------


def _call_moto(context: RequestContext, operation_name: str, parameters: ServiceRequest):
"""
Not necessarily the pattern we want to follow in the future, but this makes possible to nest
moto call and still be interface compatible.

Ripped :call_moto_with_request: from moto.py but applicable to any operation (operation_name).
"""
local_context = create_aws_request_context(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! Looks quite useful - since this is generally applicable, I'd probably move this to moto.py. (as a generalization of call_moto_with_request, which can then call this function with operation_name=context.operation.name).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this was result of a conversation with @thrau, we don't necessarilly think this will be the pattern to use, hence kept it close where it's used. Moving it to the moto.py is not problem for me, but might result in being spread unnecessarily?

service_name=context.service.service_name,
action=operation_name,
parameters=parameters,
region=context.region,
)

local_context.request.headers.extend(context.request.headers)
return call_moto(local_context)


def normalize_authorizer(data):
is_list = isinstance(data, list)
entries = ensure_list(data)
Expand Down
2 changes: 1 addition & 1 deletion localstack/utils/strings.py
Expand Up @@ -106,8 +106,8 @@ def first_char_to_upper(s: str) -> str:

def str_to_bool(value):
"""Return the boolean value of the given string, or the verbatim value if it is not a string"""
true_strings = ["true", "True"]
if isinstance(value, str):
true_strings = ["true", "True"]
return value in true_strings
return value

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_apigateway.py
Expand Up @@ -1181,7 +1181,7 @@ def test_import_rest_api(self):

spec_file = load_file(TEST_IMPORT_REST_API_FILE)
rs = client.import_rest_api(body=spec_file)
self.assertEqual(200, rs["ResponseMetadata"]["HTTPStatusCode"])
self.assertEqual(201, rs["ResponseMetadata"]["HTTPStatusCode"])

rest_api_id = rs["id"]

Expand Down