APIGatewayV2: Stages (#6456)
This commit is contained in:
parent
3f528f5428
commit
f1c72814d8
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
)
|
||||
|
@ -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")
|
||||
|
@ -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]})
|
||||
|
@ -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,
|
||||
|
@ -30,6 +30,9 @@ apigatewayv2:
|
||||
- TestAccAPIGatewayV2IntegrationResponse
|
||||
- TestAccAPIGatewayV2Model
|
||||
- TestAccAPIGatewayV2Route
|
||||
- TestAccAPIGatewayV2Stage_basicHTTP
|
||||
- TestAccAPIGatewayV2Stage_basicWebSocket
|
||||
- TestAccAPIGatewayV2Stage_disappears
|
||||
- TestAccAPIGatewayV2VPCLink
|
||||
appconfig:
|
||||
- TestAccAppConfigConfigurationProfileDataSource_basic
|
||||
|
95
tests/test_apigatewayv2/test_apigatewayv2_stages.py
Normal file
95
tests/test_apigatewayv2/test_apigatewayv2_stages.py
Normal 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"}
|
Loading…
Reference in New Issue
Block a user