Skip to content

Commit be9149c

Browse files
authored
add context variables and stage variables to API NG context (#11111)
1 parent 05a4db1 commit be9149c

File tree

8 files changed

+367
-72
lines changed

8 files changed

+367
-72
lines changed

localstack-core/localstack/services/apigateway/next_gen/execute_api/context.py

Lines changed: 30 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from localstack.aws.api.apigateway import Method, Resource
99
from localstack.services.apigateway.models import RestApiDeployment
1010

11+
from .variables import ContextVariables, LoggingContextVariables
12+
1113

1214
class InvocationRequest(TypedDict, total=False):
1315
http_method: Optional[HTTPMethod]
@@ -33,55 +35,21 @@ class InvocationRequest(TypedDict, total=False):
3335
"""Body content of the request"""
3436

3537

36-
class AuthorizerContext(TypedDict, total=False):
37-
# https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
38-
claims: Optional[dict[str, str]]
39-
"""Claims returned from the Amazon Cognito user pool after the method caller is successfully authenticated"""
40-
principal_id: Optional[str]
41-
"""The principal user identification associated with the token sent by the client and returned from an API Gateway Lambda authorizer"""
42-
context: Optional[dict[str, str]]
43-
"""The stringified value of the specified key-value pair of the context map returned from an API Gateway Lambda authorizer function"""
44-
45-
46-
class IdentityContext(TypedDict, total=False):
47-
# https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
48-
accountId: Optional[str]
49-
"""The AWS account ID associated with the request."""
50-
apiKey: Optional[str]
51-
"""For API methods that require an API key, this variable is the API key associated with the method request."""
52-
apiKeyId: Optional[str]
53-
"""The API key ID associated with an API request that requires an API key."""
54-
caller: Optional[str]
55-
"""The principal identifier of the caller that signed the request. Supported for resources that use IAM authorization."""
56-
cognitoAuthenticationProvider: Optional[str]
57-
"""A comma-separated list of the Amazon Cognito authentication providers used by the caller making the request"""
58-
cognitoAuthenticationType: Optional[str]
59-
"""The Amazon Cognito authentication type of the caller making the request"""
60-
cognitoIdentityId: Optional[str]
61-
"""The Amazon Cognito identity ID of the caller making the request"""
62-
cognitoIdentityPoolId: Optional[str]
63-
"""The Amazon Cognito identity pool ID of the caller making the request"""
64-
principalOrgId: Optional[str]
65-
"""The AWS organization ID."""
66-
sourceIp: Optional[str]
67-
"""The source IP address of the immediate TCP connection making the request to the API Gateway endpoint"""
68-
clientCert: Optional[dict[str, str]]
69-
"""Certificate that a client presents. Present only in access logs if mutual TLS authentication fails."""
70-
vpcId: Optional[str]
71-
"""The VPC ID of the VPC making the request to the API Gateway endpoint."""
72-
vpceId: Optional[str]
73-
"""The VPC endpoint ID of the VPC endpoint making the request to the API Gateway endpoint."""
74-
user: Optional[str]
75-
"""The principal identifier of the user that will be authorized against resource access for resources that use IAM authorization."""
76-
userAgent: Optional[str]
77-
"""The User-Agent header of the API caller."""
78-
userArn: Optional[str]
79-
"""The Amazon Resource Name (ARN) of the effective user identified after authentication."""
80-
81-
82-
class ContextVariables(TypedDict, total=False):
83-
authorizer: AuthorizerContext
84-
identity: IdentityContext
38+
class IntegrationRequest(TypedDict, total=False):
39+
http_method: Optional[HTTPMethod]
40+
"""HTTP Method of the incoming request"""
41+
uri: Optional[str]
42+
"""URI of the integration"""
43+
query_string_parameters: Optional[dict[str, str]]
44+
"""Query string parameters of the request"""
45+
headers: Optional[dict[str, str]]
46+
"""Headers of the request"""
47+
multi_value_query_string_parameters: Optional[dict[str, list[str]]]
48+
"""Multi value query string parameters of the request"""
49+
multi_value_headers: Optional[dict[str, list[str]]]
50+
"""Multi value headers of the request"""
51+
body: Optional[bytes]
52+
"""Body content of the request"""
8553

8654

8755
class RestApiInvocationContext(RequestContext):
@@ -97,6 +65,8 @@ class RestApiInvocationContext(RequestContext):
9765
"""The REST API identifier of the invoked API"""
9866
stage: Optional[str]
9967
"""The REST API stage linked to this invocation"""
68+
deployment_id: Optional[str]
69+
"""The REST API deployment linked to this invocation"""
10070
region: Optional[str]
10171
"""The region the REST API is living in."""
10272
account_id: Optional[str]
@@ -105,17 +75,27 @@ class RestApiInvocationContext(RequestContext):
10575
"""The resource the invocation matched""" # TODO: verify if needed through the invocation
10676
resource_method: Optional[Method]
10777
"""The method of the resource the invocation matched"""
78+
stage_variables: Optional[dict[str, str]]
79+
"""The Stage variables, also used in parameters mapping and mapping templates"""
10880
context_variables: Optional[ContextVariables]
109-
"""Variables can be used in data models, authorizers, mapping templates, and CloudWatch access logging."""
81+
"""The $context used in data models, authorizers, mapping templates, and CloudWatch access logging"""
82+
logging_context_variables: Optional[LoggingContextVariables]
83+
"""Additional $context variables available only for access logging, not yet implemented"""
84+
integration_request: Optional[IntegrationRequest]
85+
"""Contains the data needed to construct an HTTP request to an Integration"""
11086

11187
def __init__(self, request: Request):
11288
super().__init__(request)
11389
self.deployment = None
11490
self.api_id = None
11591
self.stage = None
92+
self.deployment_id = None
11693
self.account_id = None
11794
self.region = None
11895
self.invocation_request = None
11996
self.resource = None
12097
self.resource_method = None
98+
self.stage_variables = None
12199
self.context_variables = None
100+
self.logging_context_variables = None
101+
self.integration_request = None

localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/integration_request.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22

3+
from localstack.aws.api.apigateway import Integration, IntegrationType
34
from localstack.http import Response
45

56
from ..api import RestApiGatewayHandler, RestApiGatewayHandlerChain
@@ -20,4 +21,20 @@ def __call__(
2021
context: RestApiInvocationContext,
2122
response: Response,
2223
):
24+
integration: Integration = context.resource_method["methodIntegration"]
25+
integration_type = integration["type"]
26+
27+
if integration_type in (IntegrationType.AWS_PROXY, IntegrationType.HTTP_PROXY):
28+
# `PROXY` types cannot use integration mapping templates
29+
# TODO: check if PROXY can still parameters mapping and substitution in URI for example?
30+
# See
31+
return
32+
33+
if integration_type == IntegrationType.MOCK:
34+
# TODO: only apply partial rendering of the VTL context
35+
return
36+
37+
# TODO: apply rendering, and attach the Integration Request needed for the Integration to construct its HTTP
38+
# request to send
39+
2340
return

localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/parse.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1+
import datetime
12
import logging
23
from collections import defaultdict
4+
from typing import Optional
35
from urllib.parse import urlparse
46

57
from rolo.request import Request, restore_payload
68
from werkzeug.datastructures import Headers, MultiDict
79

810
from localstack.http import Response
11+
from localstack.services.apigateway.helpers import REQUEST_TIME_DATE_FORMAT
12+
from localstack.utils.strings import short_uid
13+
from localstack.utils.time import timestamp
914

1015
from ..api import RestApiGatewayHandler, RestApiGatewayHandlerChain
1116
from ..context import InvocationRequest, RestApiInvocationContext
17+
from ..moto_helpers import get_stage_variables
18+
from ..variables import ContextVariables
1219

1320
LOG = logging.getLogger(__name__)
1421

@@ -27,6 +34,14 @@ def __call__(
2734
def parse_and_enrich(self, context: RestApiInvocationContext):
2835
# first, create the InvocationRequest with the incoming request
2936
context.invocation_request = self.create_invocation_request(context.request)
37+
# then we can create the ContextVariables, used throughout the invocation as payload and to render authorizer
38+
# payload, mapping templates and such.
39+
context.context_variables = self.create_context_variables(context)
40+
# TODO: maybe adjust the logging
41+
LOG.debug("Initializing $context='%s'", context.context_variables)
42+
# then populate the stage variables
43+
context.stage_variables = self.fetch_stage_variables(context)
44+
LOG.debug("Initializing $stageVariables='%s'", context.stage_variables)
3045

3146
def create_invocation_request(self, request: Request) -> InvocationRequest:
3247
params, multi_value_params = self._get_single_and_multi_values_from_multidict(request.args)
@@ -99,3 +114,44 @@ def _get_single_and_multi_values_from_headers(
99114
multi_values[key] = headers.getlist(key)
100115

101116
return single_values, multi_values
117+
118+
@staticmethod
119+
def create_context_variables(context: RestApiInvocationContext) -> ContextVariables:
120+
invocation_request: InvocationRequest = context.invocation_request
121+
domain_name = invocation_request["raw_headers"].get("Host", "")
122+
domain_prefix = domain_name.split(".")[0]
123+
now = datetime.datetime.now()
124+
125+
# TODO: verify which values needs to explicitly have None set
126+
context_variables = ContextVariables(
127+
accountId=context.account_id,
128+
apiId=context.api_id,
129+
deploymentId=context.deployment_id,
130+
domainName=domain_name,
131+
domainPrefix=domain_prefix,
132+
extendedRequestId=short_uid(), # TODO: use snapshot tests to verify format
133+
httpMethod=invocation_request["http_method"],
134+
path=invocation_request[
135+
"path"
136+
], # TODO: check if we need the raw path? with forward slashes
137+
protocol="HTTP/1.1",
138+
requestId=short_uid(), # TODO: use snapshot tests to verify format
139+
requestTime=timestamp(time=now, format=REQUEST_TIME_DATE_FORMAT),
140+
requestTimeEpoch=int(now.timestamp() * 1000),
141+
stage=context.stage,
142+
)
143+
return context_variables
144+
145+
@staticmethod
146+
def fetch_stage_variables(context: RestApiInvocationContext) -> Optional[dict[str, str]]:
147+
stage_variables = get_stage_variables(
148+
account_id=context.account_id,
149+
region=context.region,
150+
api_id=context.api_id,
151+
stage_name=context.stage,
152+
)
153+
if not stage_variables:
154+
# we need to set the stage variables to None in the context if we don't have at least one
155+
return None
156+
157+
return stage_variables

localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/resource_router.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from ..api import RestApiGatewayHandler, RestApiGatewayHandlerChain
2020
from ..context import RestApiInvocationContext
21+
from ..variables import ContextVariables
2122

2223
LOG = logging.getLogger(__name__)
2324

@@ -84,6 +85,7 @@ def route_and_enrich(self, context: RestApiInvocationContext):
8485
router = self.get_router_for_deployment(context.deployment)
8586

8687
resource, path_parameters = router.match(context)
88+
resource: Resource
8789

8890
context.invocation_request["path_parameters"] = path_parameters
8991
context.resource = resource
@@ -94,6 +96,17 @@ def route_and_enrich(self, context: RestApiInvocationContext):
9496
)
9597
context.resource_method = method
9698

99+
self.update_context_variables_with_resource(context.context_variables, resource)
100+
101+
@staticmethod
102+
def update_context_variables_with_resource(
103+
context_variables: ContextVariables, resource: Resource
104+
):
105+
LOG.debug("Updating $context.resourcePath='%s'", resource["path"])
106+
context_variables["resourcePath"] = resource["path"]
107+
LOG.debug("Updating $context.resourceId='%s'", resource["id"])
108+
context_variables["resourceId"] = resource["id"]
109+
97110
@staticmethod
98111
@cache
99112
def get_router_for_deployment(deployment: RestApiDeployment) -> RestAPIResourceRouter:

localstack-core/localstack/services/apigateway/next_gen/execute_api/moto_helpers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from moto.apigateway.models import APIGatewayBackend, apigateway_backends
12
from moto.apigateway.models import RestAPI as MotoRestAPI
23

34
from localstack.aws.api.apigateway import Resource
@@ -28,3 +29,12 @@ def get_resources_from_moto_rest_api(moto_rest_api: MotoRestAPI) -> dict[str, Re
2829
resources[moto_resource.id] = resource
2930

3031
return resources
32+
33+
34+
def get_stage_variables(
35+
account_id: str, region: str, api_id: str, stage_name: str
36+
) -> dict[str, str]:
37+
apigateway_backend: APIGatewayBackend = apigateway_backends[account_id][region]
38+
moto_rest_api = apigateway_backend.apis[api_id]
39+
stage = moto_rest_api.stages[stage_name]
40+
return stage.variables

localstack-core/localstack/services/apigateway/next_gen/execute_api/router.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def populate_rest_api_invocation_context(
6464
context.deployment = frozen_deployment
6565
context.api_id = api_id
6666
context.stage = stage
67+
context.deployment_id = deployment_id
6768

6869

6970
class ApiGatewayRouter:

0 commit comments

Comments
 (0)