Skip to content

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

Merged
merged 12 commits into from
Jun 12, 2024

Conversation

bentsku
Copy link
Contributor

@bentsku bentsku commented Jun 7, 2024

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 and PluginManager to register the different AWS integrations transparently, and the IntegrationHandler 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 a Deployment, 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 a Stage. 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

  • create a new opt-in provider called next_gen in order to separate the new code and registrations of routes from the current implementation
  • create a new edge router handler, registered on the global APIGW routes. This handler instantiates a subclass or the rolo.Gateway in order to pass the incoming invocation request through a HandlerChain. This handler can be subclassed.
  • the RestApiGateway contains a first iteration of the handler list to be executed while invoking a REST API
  • introduce a new subclass of the RequestContext which will be extended by future PRs, to contain all the necessary data for a successful REST API invocation.
  • some new fields in the APIGW store in order to store the deployments and active deployments for the edge router handler to properly understand what API is being invoked and populate the context
  • a few fixes to existing only_localstack or needs_fixing tests that did not deploy the REST API before invoking it.

TODO

What's left to do:

  • remove the forced usage of this new prototype in config.py before merging

@bentsku bentsku added aws:apigateway Amazon API Gateway semver: minor Non-breaking changes which can be included in minor releases, but not in patch releases labels Jun 7, 2024
@bentsku bentsku self-assigned this Jun 7, 2024
@bentsku bentsku added this to the Playground milestone Jun 7, 2024
Copy link

github-actions bot commented Jun 7, 2024

S3 Image Test Results (AMD64 / ARM64)

  2 files    2 suites   3m 11s ⏱️
404 tests 352 ✅  52 💤 0 ❌
808 runs  704 ✅ 104 💤 0 ❌

Results for commit e03db85.

♻️ This comment has been updated with latest results.

Copy link

github-actions bot commented Jun 7, 2024

LocalStack Community integration with Pro

    2 files  ±0      2 suites  ±0   1h 36m 36s ⏱️ + 1m 13s
3 074 tests +2  2 729 ✅ +2  345 💤 ±0  0 ❌ ±0 
3 076 runs  +2  2 729 ✅ +2  347 💤 ±0  0 ❌ ±0 

Results for commit e03db85. ± Comparison against base commit 8e5b9ff.

♻️ This comment has been updated with latest results.

@bentsku bentsku force-pushed the next-gen-apigw-v1 branch 2 times, most recently from bbed56c to 1eb9126 Compare June 9, 2024 18:17
@bentsku bentsku changed the title WIP: next gen apigw v1 introduce handler chain (next gen) in apigateway v1 invocation Jun 10, 2024
LOG = logging.getLogger(__name__)


class InvocationRequestParser(RestApiGatewayHandler):
Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

@bentsku bentsku Jun 11, 2024

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 😄

@alexrashed alexrashed self-requested a review June 11, 2024 07:00
@alexrashed alexrashed modified the milestones: Playground, 3.6 Jun 11, 2024
@bentsku bentsku marked this pull request as ready for review June 11, 2024 13:45

else:
# TODO: return right response
print("here?")
Copy link
Contributor Author

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

Copy link
Member

@alexrashed alexrashed left a 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! 🥳

Comment on lines 1035 to 1038
# 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"
Copy link
Member

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,
Copy link
Member

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):
Copy link
Member

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.

Comment on lines 13 to 14
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
Copy link
Member

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 😄

Copy link
Contributor Author

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):
Copy link
Member

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?

Copy link
Member

Choose a reason for hiding this comment

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

Beautiful 😍

@@ -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)
Copy link
Member

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)?

Copy link
Contributor Author

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?

Copy link
Member

@alexrashed alexrashed Jun 12, 2024

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:
Copy link
Member

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")
Copy link
Member

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")
Copy link
Member

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...

Copy link
Contributor Author

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

Copy link
Contributor Author

@bentsku bentsku left a 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 🚀

Comment on lines 13 to 14
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
Copy link
Contributor Author

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):
Copy link
Contributor Author

@bentsku bentsku Jun 11, 2024

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")
Copy link
Contributor Author

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

@@ -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)
Copy link
Contributor Author

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?

@bentsku bentsku force-pushed the next-gen-apigw-v1 branch from ff18519 to 5687faa Compare June 11, 2024 16:54
@bentsku bentsku modified the milestones: 3.6, 3.5 Jun 12, 2024
@bentsku bentsku merged commit 7133027 into master Jun 12, 2024
37 checks passed
@bentsku bentsku deleted the next-gen-apigw-v1 branch June 12, 2024 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
aws:apigateway Amazon API Gateway semver: minor Non-breaking changes which can be included in minor releases, but not in patch releases
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants