-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
introduce handler chain (next gen) in apigateway v1 invocation #10982
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
Conversation
S3 Image Test Results (AMD64 / ARM64) 2 files 2 suites 3m 11s ⏱️ Results for commit e03db85. ♻️ This comment has been updated with latest results. |
bbed56c
to
1eb9126
Compare
LOG = logging.getLogger(__name__) | ||
|
||
|
||
class InvocationRequestParser(RestApiGatewayHandler): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
naming might be a bit misleading here: we probably think this handler will populate the context from the linked deployment
, by "routing" the request depending on its path to the right APIGW Resource
& Method
. Naming is not set in stone and will be updated in follow up PRs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, this is exactly what I thought this class would do 😄
But what is it intended to do then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this class might still be needed under this name, to parse for example the RAW_URI
from the request and such things.
And we can also have a "router" handler that will properly match the incoming request to the right APIGW resource. Once we start splitting things (or not for this handler, we might handle parsing/routing as one operation), I think naming will become clearer. I still think we might need a parser somewhere 😄
|
||
else: | ||
# TODO: return right response | ||
print("here?") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oopsie, already removed in a not-yet-pushed commit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! This is a great starting point for the upcoming work on API Gateway and you can already see that this will bring way more structure, way less patching, way more extensibility, and way less boilerplate code (f.e. for the request parsing)! 🧹 💯
The (currently dummy) handlers in the chain already show how well structured this is going to look like afterwards 🤩
I only found some nitpicks, nothing blocking a merge. The changes contained (as soon as the provider override switch is removed again) should be completely safe to merge! 🥳
localstack-core/localstack/config.py
Outdated
# Whether to enable the Next Gen APIGW invocation logic (handler chain) | ||
# TODO: remove this before merge! | ||
if not os.environ.get("PROVIDER_OVERRIDE_APIGATEWAY"): | ||
os.environ["PROVIDER_OVERRIDE_APIGATEWAY"] = "next_gen" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO remove before merge (as you already mentioned in the PR description, just another reminder 😛)
handlers.integration_request_handler, | ||
handlers.integration_handler, | ||
# temporary handler which executes everything for now | ||
handlers.global_temporary_handler, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Previously we used to use the term "legacy" for this kind of backwards compatibility / migration logic.
from .context import RestApiInvocationContext | ||
|
||
|
||
class RestApiGateway(Gateway): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Would be great to add a bit of documentation here: This is the main part controlling the API GW invocation handling now. Maybe we can add a few sentences (can also be in upcoming iterations) on what this is and how to extend / plug into this.
This class might not too much, we still need to investigate but from a first look, does not have much impact | ||
on the HTTP response |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This comment is a bit confusing 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sorry, this was mostly internal 😅 we realized with @cloutierMat that the MethodResponse
resource in APIGW might just be for "decorative purposes" and used to create SDKs from the REST API, but we're not 100% sure this handler does nothing. I'll update the comment!
LOG = logging.getLogger(__name__) | ||
|
||
|
||
class InvocationRequestParser(RestApiGatewayHandler): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, this is exactly what I thought this class would do 😄
But what is it intended to do then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Beautiful 😍
tests/integration/test_security.py
Outdated
@@ -178,7 +178,7 @@ def test_cors_apigw_not_applied(self, aws_client): | |||
"Origin": "https://app.localstack.cloud", | |||
}, | |||
) | |||
assert response.status_code == 404 | |||
assert response.status_code in (403, 404) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Maybe we could make this parameterized to already take the proper return code in case of the new provider (same below)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thing is, I'm not too sure yet what is the appropriate status code, I believe it is 403.
How would we parametrize this?
Shouldn't we instead use the config variable to do the assert? if config.APIGW_NEXT_GEN_PROVIDER: assert 403 else assert 404
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, if we don't know it currently then no.
I had the impression that the old code (404) is not in parity with AWS and the new provider will (403).
If this is the case, then we should make sure that the assert for the old provider is properly removed.
So if we don't know this yet, then it's fine for now, if we know it, then yes, a conditional like you mentioned would be perfectly fine (it would remind us to remove the old part from the test when we remove the config at the end of the project). ;)
|
||
|
||
# TODO: subclass from -ext, and this handler will do the dispatching between v1 and v2 Gateways | ||
class ApiGatewayHandler: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: As discussed, we could maybe rename this to Endpoint
or EndpointHandler
to differentiate between HandlerChain
handlers here (since this is what's invoked by the router on a URL match).
|
||
def __call__(self, request: Request, **kwargs) -> Response: | ||
# api_id can be cased because of custom-tag id | ||
api_id, stage = kwargs.get("api_id", "").lower(), kwargs.get("stage") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! This means we are finally using werkzeug
's request parsing directly from the route matching! 🚀 🤩
# api_id can be cased because of custom-tag id | ||
api_id, stage = kwargs.get("api_id", "").lower(), kwargs.get("stage") | ||
if self.is_rest_api(api_id, stage): | ||
LOG.info("Next-gen handler for APIGW v1 called") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Maybe we shouldn't have the term "next gen" leak into the logs...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, yes, APIGW v1 Endpoint or something similar will be better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for the review 🙏 will address the comments shortly!
When this PR is green, I will remove the flag and merge 🚀
This class might not too much, we still need to investigate but from a first look, does not have much impact | ||
on the HTTP response |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, sorry, this was mostly internal 😅 we realized with @cloutierMat that the MethodResponse
resource in APIGW might just be for "decorative purposes" and used to create SDKs from the REST API, but we're not 100% sure this handler does nothing. I'll update the comment!
LOG = logging.getLogger(__name__) | ||
|
||
|
||
class InvocationRequestParser(RestApiGatewayHandler): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this class might still be needed under this name, to parse for example the RAW_URI
from the request and such things.
And we can also have a "router" handler that will properly match the incoming request to the right APIGW resource. Once we start splitting things (or not for this handler, we might handle parsing/routing as one operation), I think naming will become clearer. I still think we might need a parser somewhere 😄
# api_id can be cased because of custom-tag id | ||
api_id, stage = kwargs.get("api_id", "").lower(), kwargs.get("stage") | ||
if self.is_rest_api(api_id, stage): | ||
LOG.info("Next-gen handler for APIGW v1 called") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, yes, APIGW v1 Endpoint or something similar will be better
tests/integration/test_security.py
Outdated
@@ -178,7 +178,7 @@ def test_cors_apigw_not_applied(self, aws_client): | |||
"Origin": "https://app.localstack.cloud", | |||
}, | |||
) | |||
assert response.status_code == 404 | |||
assert response.status_code in (403, 404) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thing is, I'm not too sure yet what is the appropriate status code, I believe it is 403.
How would we parametrize this?
Shouldn't we instead use the config variable to do the assert? if config.APIGW_NEXT_GEN_PROVIDER: assert 403 else assert 404
?
ff18519
to
5687faa
Compare
Motivation
This is the first implementation of the handler chain inside an edge router handler for API Gateway REST APIs invocations.
Currently, APIGW registers an edge router handler which then invokes a function which passes a context around, where the separation of concerns is not very clear (it does a few things globally, like checking for UsagePlans API Key, or body/parameters validation), but then defers most of it to the
BackendIntegration
subclasses, which do things quite differently between themselves.However, when looking at API Gateway and how they trigger/dispatch requests, a lot should be done outside the integrations themselves.
Using the new LocalStack constructs, we will be able to have clear separations of concerns between different steps of the invocation. This PR already introduces a few handlers, but more will come (API Key validation as a separate step). We are also making use of
CompositeHandler
in order for the code to be extendable from outside.As a next step, we will also make use of
Plugin
andPluginManager
to register the different AWS integrations transparently, and theIntegrationHandler
will contain the logic necessary to load them and invoke them.Additionally, a parity issue is present in LocalStack: once we create a REST API, it is available to be invoked under the
execute-api
route. However, this is not the case in AWS. You will need to create aDeployment
, which is a snapshot (also referred as an "executable" of your API by AWS) of your current REST API, which then can be served by linking that deployment to aStage
. Currently, in LocalStack, invoking your API will always hit your "current" REST API, meaning if you do some updates, they will automatically be used while invoking the API. While this might be useful, this is very far from parity (we might re-introduce this mode with a feature flag, something akin to "hot reloading" of your API, but this would be opt-in) and it prevents things like testing canary releases or having different stages pointing to different versions of your API.This new implementation, freezing the REST API and populating the context with it, will simplify greatly how to fetch the data in order to do the work during the invocation steps.
Currently, in order to work in an iterative way, we have introduced a few handlers outlining the future work to split the current code into a few different handlers, but one "temporary" or "legacy" handler holds all the invocation. This PR simply introduces the concept of a handler chain for the invocation.
Changes
next_gen
in order to separate the new code and registrations of routes from the current implementationrolo.Gateway
in order to pass the incoming invocation request through aHandlerChain
. This handler can be subclassed.RestApiGateway
contains a first iteration of the handler list to be executed while invoking a REST APIRequestContext
which will be extended by future PRs, to contain all the necessary data for a successful REST API invocation.only_localstack
orneeds_fixing
tests that did not deploy the REST API before invoking it.TODO
What's left to do:
config.py
before merging