APIGatewayV2: Stages (#6456)

This commit is contained in:
Bert Blommers 2023-06-29 09:09:15 +00:00 committed by GitHub
parent 3f528f5428
commit f1c72814d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 237 additions and 10 deletions

View File

@ -204,7 +204,7 @@
## apigatewayv2
<details>
<summary>69% implemented</summary>
<summary>75% implemented</summary>
- [X] create_api
- [X] create_api_mapping
@ -216,7 +216,7 @@
- [X] create_model
- [X] create_route
- [X] create_route_response
- [ ] create_stage
- [X] create_stage
- [X] create_vpc_link
- [ ] delete_access_log_settings
- [X] delete_api
@ -232,7 +232,7 @@
- [X] delete_route_request_parameter
- [X] delete_route_response
- [ ] delete_route_settings
- [ ] delete_stage
- [X] delete_stage
- [X] delete_vpc_link
- [ ] export_api
- [X] get_api
@ -256,8 +256,8 @@
- [X] get_route_response
- [ ] get_route_responses
- [X] get_routes
- [ ] get_stage
- [ ] get_stages
- [X] get_stage
- [X] get_stages
- [X] get_tags
- [X] get_vpc_link
- [X] get_vpc_links

View File

@ -46,7 +46,7 @@ apigatewayv2
The following parameters are not yet implemented: ResponseModels, ResponseParameters
- [ ] create_stage
- [X] create_stage
- [X] create_vpc_link
- [ ] delete_access_log_settings
- [X] delete_api
@ -62,7 +62,7 @@ apigatewayv2
- [X] delete_route_request_parameter
- [X] delete_route_response
- [ ] delete_route_settings
- [ ] delete_stage
- [X] delete_stage
- [X] delete_vpc_link
- [ ] export_api
- [X] get_api
@ -102,8 +102,8 @@ apigatewayv2
Pagination is not yet implemented
- [ ] get_stage
- [ ] get_stages
- [X] get_stage
- [X] get_stages
- [X] get_tags
- [X] get_vpc_link
- [X] get_vpc_links

View File

@ -123,3 +123,13 @@ class ApiMappingNotFound(APIGatewayV2Error):
"NotFoundException",
"The api mapping resource specified in the request was not found.",
)
class StageNotFound(APIGatewayV2Error):
code = 404
def __init__(self) -> None:
super().__init__(
"NotFoundException",
"Invalid stage identifier specified",
)

View File

@ -22,9 +22,57 @@ from .exceptions import (
VpcLinkNotFound,
DomainNameNotFound,
DomainNameAlreadyExists,
StageNotFound,
)
class Stage(BaseModel):
def __init__(self, api: "Api", config: Dict[str, Any]):
self.config = config
self.name = config["stageName"]
if api.protocol_type == "HTTP":
self.default_route_settings = config.get(
"defaultRouteSettings", {"detailedMetricsEnabled": False}
)
elif api.protocol_type == "WEBSOCKET":
self.default_route_settings = config.get(
"defaultRouteSettings",
{
"dataTraceEnabled": False,
"detailedMetricsEnabled": False,
"loggingLevel": "OFF",
},
)
self.access_log_settings = config.get("accessLogSettings")
self.auto_deploy = config.get("autoDeploy")
self.client_certificate_id = config.get("clientCertificateId")
self.description = config.get("description")
self.route_settings = config.get("routeSettings", {})
self.stage_variables = config.get("stageVariables", {})
self.tags = config.get("tags", {})
self.created = self.updated = unix_time()
def to_json(self) -> Dict[str, Any]:
dct = {
"stageName": self.name,
"defaultRouteSettings": self.default_route_settings,
"createdDate": self.created,
"lastUpdatedDate": self.updated,
"routeSettings": self.route_settings,
"stageVariables": self.stage_variables,
"tags": self.tags,
}
if self.access_log_settings:
dct["accessLogSettings"] = self.access_log_settings
if self.auto_deploy is not None:
dct["autoDeploy"] = self.auto_deploy
if self.client_certificate_id:
dct["clientCertificateId"] = self.client_certificate_id
if self.description:
dct["description"] = self.description
return dct
class Authorizer(BaseModel):
def __init__(
self,
@ -541,6 +589,7 @@ class Api(BaseModel):
self.integrations: Dict[str, Integration] = dict()
self.models: Dict[str, Model] = dict()
self.routes: Dict[str, Route] = dict()
self.stages: Dict[str, Stage] = dict()
self.arn = f"arn:aws:apigateway:{region}::/apis/{self.api_id}"
self.backend.tag_resource(self.arn, tags)
@ -550,6 +599,7 @@ class Api(BaseModel):
self.integrations.clear()
self.models.clear()
self.routes.clear()
self.stages.clear()
def delete_cors_configuration(self) -> None:
self.cors_configuration = None
@ -962,6 +1012,19 @@ class Api(BaseModel):
route = self.get_route(route_id)
return route.get_route_response(route_response_id)
def create_stage(self, config: Dict[str, Any]) -> Stage:
stage = Stage(api=self, config=config)
self.stages[stage.name] = stage
return stage
def get_stage(self, stage_name: str) -> Stage:
if stage_name not in self.stages:
raise StageNotFound
return self.stages[stage_name]
def delete_stage(self, stage_name: str) -> None:
self.stages.pop(stage_name, None)
def to_json(self) -> Dict[str, Any]:
return {
"apiId": self.api_id,
@ -1599,7 +1662,6 @@ class ApiGatewayV2Backend(BaseBackend):
mutual_tls_authentication: Dict[str, str],
tags: Dict[str, str],
) -> DomainName:
if domain_name in self.domain_names.keys():
raise DomainNameAlreadyExists
@ -1701,5 +1763,21 @@ class ApiGatewayV2Backend(BaseBackend):
del self.api_mappings[api_mapping_id]
def create_stage(self, api_id: str, config: Dict[str, Any]) -> Stage:
api = self.get_api(api_id)
return api.create_stage(config)
def get_stage(self, api_id: str, stage_name: str) -> Stage:
api = self.get_api(api_id)
return api.get_stage(stage_name)
def delete_stage(self, api_id: str, stage_name: str) -> None:
api = self.get_api(api_id)
api.delete_stage(stage_name)
def get_stages(self, api_id: str) -> List[Stage]:
api = self.get_api(api_id)
return list(api.stages.values())
apigatewayv2_backends = BackendDict(ApiGatewayV2Backend, "apigatewayv2")

View File

@ -212,6 +212,22 @@ class ApiGatewayV2Response(BaseResponse):
if request.method == "DELETE":
return self.delete_api_mapping()
def stages(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE: # type: ignore[return]
self.setup_class(request, full_url, headers)
if self.method == "POST":
return self.create_stage()
if self.method == "GET":
return self.get_stages()
def stage(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE: # type: ignore[return]
self.setup_class(request, full_url, headers)
if self.method == "GET":
return self.get_stage()
if self.method == "DELETE":
return self.delete_stage()
def create_api(self) -> TYPE_RESPONSE:
params = json.loads(self.body)
@ -854,3 +870,26 @@ class ApiGatewayV2Response(BaseResponse):
domain_name=domain_name,
)
return 204, {}, ""
def create_stage(self) -> TYPE_RESPONSE:
api_id = self.path.split("/")[-2]
config = json.loads(self.body)
stage = self.apigatewayv2_backend.create_stage(api_id, config)
return 200, {}, json.dumps(stage.to_json())
def get_stage(self) -> TYPE_RESPONSE:
api_id = self.path.split("/")[-3]
stage_name = self.path.split("/")[-1]
stage = self.apigatewayv2_backend.get_stage(api_id, stage_name)
return 200, {}, json.dumps(stage.to_json())
def delete_stage(self) -> TYPE_RESPONSE:
api_id = self.path.split("/")[-3]
stage_name = self.path.split("/")[-1]
self.apigatewayv2_backend.delete_stage(api_id, stage_name)
return 200, {}, "{}"
def get_stages(self) -> TYPE_RESPONSE:
api_id = self.path.split("/")[-2]
stages = self.apigatewayv2_backend.get_stages(api_id)
return 200, {}, json.dumps({"items": [st.to_json() for st in stages]})

View File

@ -26,6 +26,8 @@ url_paths = {
"{0}/v2/apis/(?P<api_id>[^/]+)/routes/(?P<route_id>[^/]+)/routeresponses$": response_v2.route_responses,
"{0}/v2/apis/(?P<api_id>[^/]+)/routes/(?P<route_id>[^/]+)/routeresponses/(?P<route_response_id>[^/]+)$": response_v2.route_response,
"{0}/v2/apis/(?P<api_id>[^/]+)/routes/(?P<route_id>[^/]+)/requestparameters/(?P<request_parameter>[^/]+)$": response_v2.route_request_parameter,
"{0}/v2/apis/(?P<api_id>[^/]+)/stages$": response_v2.stages,
"{0}/v2/apis/(?P<api_id>[^/]+)/stages/(?P<stage_name>[^/]+)$": response_v2.stage,
"{0}/v2/tags/(?P<resource_arn>[^/]+)$": response_v2.tags,
"{0}/v2/tags/(?P<resource_arn_pt1>[^/]+)/apis/(?P<resource_arn_pt2>[^/]+)$": response_v2.tags,
"{0}/v2/tags/(?P<resource_arn_pt1>[^/]+)/vpclinks/(?P<resource_arn_pt2>[^/]+)$": response_v2.tags,

View File

@ -30,6 +30,9 @@ apigatewayv2:
- TestAccAPIGatewayV2IntegrationResponse
- TestAccAPIGatewayV2Model
- TestAccAPIGatewayV2Route
- TestAccAPIGatewayV2Stage_basicHTTP
- TestAccAPIGatewayV2Stage_basicWebSocket
- TestAccAPIGatewayV2Stage_disappears
- TestAccAPIGatewayV2VPCLink
appconfig:
- TestAccAppConfigConfigurationProfileDataSource_basic

View File

@ -0,0 +1,95 @@
import boto3
import pytest
from botocore.exceptions import ClientError
from moto import mock_apigatewayv2
@mock_apigatewayv2
def test_create_stage__defaults():
client = boto3.client("apigatewayv2", "us-east-2")
api_id = client.create_api(Name="test-api", ProtocolType="HTTP")["ApiId"]
stages = client.get_stages(ApiId=api_id)
assert stages["Items"] == []
stage = client.create_stage(ApiId=api_id, StageName="my_stage")
assert stage["StageName"] == "my_stage"
resp = client.get_stage(ApiId=api_id, StageName="my_stage")
assert resp["StageName"] == "my_stage"
stages = client.get_stages(ApiId=api_id)
assert len(stages["Items"]) == 1
assert stages["Items"][0]["StageName"] == "my_stage"
assert stages["Items"][0]["DefaultRouteSettings"] == {
"DetailedMetricsEnabled": False
}
assert stages["Items"][0]["CreatedDate"]
assert stages["Items"][0]["LastUpdatedDate"]
assert stages["Items"][0]["RouteSettings"] == {}
assert stages["Items"][0]["StageVariables"] == {}
assert stages["Items"][0]["Tags"] == {}
client.delete_stage(ApiId=api_id, StageName="my_stage")
with pytest.raises(ClientError) as exc:
client.get_stage(ApiId=api_id, StageName="my_stage")
err = exc.value.response["Error"]
assert err["Code"] == "NotFoundException"
assert err["Message"] == "Invalid stage identifier specified"
@mock_apigatewayv2
def test_create_stage__defaults_for_websocket_api():
client = boto3.client("apigatewayv2", "us-east-2")
api_id = client.create_api(Name="test-api", ProtocolType="WEBSOCKET")["ApiId"]
client.create_stage(ApiId=api_id, StageName="my_stage")
stage = client.get_stage(ApiId=api_id, StageName="my_stage")
assert stage["StageName"] == "my_stage"
assert stage["DefaultRouteSettings"] == {
"DataTraceEnabled": False,
"DetailedMetricsEnabled": False,
"LoggingLevel": "OFF",
}
@mock_apigatewayv2
def test_create_stage():
client = boto3.client("apigatewayv2", "us-east-2")
api_id = client.create_api(Name="test-api", ProtocolType="HTTP")["ApiId"]
stage = client.create_stage(
ApiId=api_id,
StageName="my_stage",
AccessLogSettings={"Format": "JSON"},
AutoDeploy=True,
ClientCertificateId="ccid",
DefaultRouteSettings={"LoggingLevel": "INFO", "ThrottlingBurstLimit": 9000},
Description="my shiny stage",
RouteSettings={
"route1": {"DataTraceEnabled": True},
},
StageVariables={"sv1": "val1"},
Tags={"k1": "v1"},
)
assert stage["StageName"] == "my_stage"
assert stage["AccessLogSettings"] == {"Format": "JSON"}
assert stage["AutoDeploy"] is True
assert stage["ClientCertificateId"] == "ccid"
assert stage["DefaultRouteSettings"] == {
"LoggingLevel": "INFO",
"ThrottlingBurstLimit": 9000,
}
assert stage["Description"] == "my shiny stage"
assert stage["CreatedDate"]
assert stage["LastUpdatedDate"]
assert stage["RouteSettings"] == {"route1": {"DataTraceEnabled": True}}
assert stage["StageVariables"] == {"sv1": "val1"}
assert stage["Tags"] == {"k1": "v1"}