Skip to content

feature: add swagger #784

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 8 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/main-serverless-service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ jobs:
- name: Codecov
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
fail_ci_if_error: false # optional (default = false)
verbose: false # optional (default = false)
fail_ci_if_error: yes # optional (default = false)
verbose: yes # optional (default = false)
- name: Run E2E tests
run: make e2e
env:
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/pr-serverless-service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,10 @@ jobs:
- name: Codecov
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
fail_ci_if_error: false # optional (default = false)
verbose: false # optional (default = false)
fail_ci_if_error: yes # optional (default = false)
verbose: yes # optional (default = false)
- name: Run E2E tests
run: make e2e
- name: Destroy stack
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ repos:
exclude: "^(?!helpers/)"
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.13
rev: v0.1.14
hooks:
# Run the Ruff linter.
- id: ruff
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ integration:
e2e:
poetry run pytest tests/e2e --cov-config=.coveragerc --cov=service --cov-report xml

pr: deps pre-commit complex lint lint-docs unit deploy coverage-tests e2e
pr: deps format pre-commit complex lint lint-docs unit deploy coverage-tests e2e

coverage-tests:
poetry run pytest tests/unit tests/integration --cov-config=.coveragerc --cov=service --cov-report xml
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ This project aims to reduce cognitive load and answer these questions for you by
- REST API protected by WAF with four AWS managed rules in production deployment
- CloudWatch dashboards - High level and low level including CloudWatch alarms
- Unit, infrastructure, security, integration and end to end tests.
- Automatically generated OpenAPI endpoint: /swagger with Pydnatic schemas for both requests and responses


## CDK Deployment
Expand Down
25 changes: 20 additions & 5 deletions cdk/service/api_construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,31 @@ def __init__(self, scope: Construct, id_: str, appconfig_app_name: str, is_produ
self.lambda_role = self._build_lambda_role(self.api_db.db, self.api_db.idempotency_db)
self.common_layer = self._build_common_layer()
self.rest_api = self._build_api_gw()
api_resource: aws_apigateway.Resource = self.rest_api.root.add_resource('api').add_resource(constants.GW_RESOURCE)
api_resource: aws_apigateway.Resource = self.rest_api.root.add_resource('api')
orders_resource = api_resource.add_resource(constants.GW_RESOURCE)
self.create_order_func = self._add_post_lambda_integration(
api_resource, self.lambda_role, self.api_db.db, appconfig_app_name, self.api_db.idempotency_db
orders_resource, self.lambda_role, self.api_db.db, appconfig_app_name, self.api_db.idempotency_db
)
self._build_swagger_endpoints(rest_api=self.rest_api, dest_func=self.create_order_func)
self.monitoring = CrudMonitoring(self, id_, self.rest_api, self.api_db.db, self.api_db.idempotency_db, [self.create_order_func])

if is_production_env:
# add WAF
self.waf = WafToApiGatewayConstruct(self, f'{id_}waf', self.rest_api)

def _build_swagger_endpoints(self, rest_api: aws_apigateway.RestApi, dest_func: _lambda.Function) -> None:
# GET /swagger
swagger_resource: aws_apigateway.Resource = rest_api.root.add_resource(constants.SWAGGER_RESOURCE)
swagger_resource.add_method(http_method='GET', integration=aws_apigateway.LambdaIntegration(handler=dest_func))
# GET /swagger.css
swagger_resource_css = rest_api.root.add_resource(constants.SWAGGER_CSS_RESOURCE)
swagger_resource_css.add_method(http_method='GET', integration=aws_apigateway.LambdaIntegration(handler=dest_func))
# GET /swagger.js
swagger_resource_js = rest_api.root.add_resource(constants.SWAGGER_JS_RESOURCE)
swagger_resource_js.add_method(http_method='GET', integration=aws_apigateway.LambdaIntegration(handler=dest_func))

CfnOutput(self, id=constants.SWAGGER_URL, value=f'{rest_api.url}swagger').override_logical_id(constants.SWAGGER_URL)

def _build_api_gw(self) -> aws_apigateway.RestApi:
rest_api: aws_apigateway.RestApi = aws_apigateway.RestApi(
self,
Expand Down Expand Up @@ -92,7 +107,7 @@ def _build_common_layer(self) -> PythonLayerVersion:
)

def _add_post_lambda_integration(
self, api_name: aws_apigateway.Resource, role: iam.Role, db: dynamodb.Table, appconfig_app_name: str, idempotency_table: dynamodb.Table
self, api_resource: aws_apigateway.Resource, role: iam.Role, db: dynamodb.Table, appconfig_app_name: str, idempotency_table: dynamodb.Table
) -> _lambda.Function:
lambda_function = _lambda.Function(
self,
Expand All @@ -102,7 +117,7 @@ def _add_post_lambda_integration(
handler='service.handlers.handle_create_order.lambda_handler',
environment={
constants.POWERTOOLS_SERVICE_NAME: constants.SERVICE_NAME, # for logger, tracer and metrics
constants.POWER_TOOLS_LOG_LEVEL: 'DEBUG', # for logger
constants.POWER_TOOLS_LOG_LEVEL: 'INFO', # for logger
'CONFIGURATION_APP': appconfig_app_name, # for feature flags
'CONFIGURATION_ENV': constants.ENVIRONMENT, # for feature flags
'CONFIGURATION_NAME': constants.CONFIGURATION_NAME, # for feature flags
Expand All @@ -124,5 +139,5 @@ def _add_post_lambda_integration(
)

# POST /api/orders/
api_name.add_method(http_method='POST', integration=aws_apigateway.LambdaIntegration(handler=lambda_function))
api_resource.add_method(http_method='POST', integration=aws_apigateway.LambdaIntegration(handler=lambda_function))
return lambda_function
4 changes: 2 additions & 2 deletions cdk/service/configuration/configuration_construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ def __init__(self, scope: Construct, id_: str, environment: str, service_name: s
self.config_app = appconfig.Application(
self,
id=self.app_name,
name=self.app_name,
application_name=self.app_name,
)

self.config_env = appconfig.Environment(
self,
id=f'{id_}env',
application=self.config_app,
name=environment,
environment_name=environment,
)

# zero minutes, zero bake, 100 growth all at once
Expand Down
4 changes: 4 additions & 0 deletions cdk/service/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
APIGATEWAY = 'Apigateway'
MONITORING_TOPIC = 'MonitoringTopic'
GW_RESOURCE = 'orders'
SWAGGER_RESOURCE = 'swagger'
SWAGGER_CSS_RESOURCE = 'swagger.css'
SWAGGER_JS_RESOURCE = 'swagger.js'
SWAGGER_URL = 'SwaggerURL'
LAMBDA_LAYER_NAME = 'common'
API_HANDLER_LAMBDA_MEMORY_SIZE = 128 # MB
API_HANDLER_LAMBDA_TIMEOUT = 10 # seconds
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ This project aims to reduce cognitive load and answer these questions for you by
- Idempotent API
- REST API protected by WAF with four AWS managed rules in production deployment
- Unit, infrastructure, security, integration and E2E tests.
- Automatically generated OpenAPI endpoint: /swagger with Pydnatic schemas for both requests and responses

The GitHub template project can be found at [https://github.com/ran-isenberg/aws-lambda-handler-cookbook](https://github.com/ran-isenberg/aws-lambda-handler-cookbook){:target="_blank" rel="noopener"}.

Expand Down
196 changes: 196 additions & 0 deletions docs/swagger/openapi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
{
"openapi": "3.0.0",
"info": {
"title": "AWS Lambda Handler Cookbook - Orders Service",
"version": "1.0.0"
},
"servers": [
{
"url": "/prod"
}
],
"paths": {
"/api/orders": {
"post": {
"tags": [
"CRUD"
],
"summary": "Create an order",
"description": "Create an order identified by the body payload",
"operationId": "handle_create_order_api_orders_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateOrderRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "The created order",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateOrderOutput"
}
}
}
},
"442": {
"description": "Invalid create order request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InvalidRestApiRequest"
}
}
}
},
"501": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InternalServerErrorOutput"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"CreateOrderOutput": {
"properties": {
"name": {
"type": "string",
"maxLength": 20,
"minLength": 1,
"title": "Name",
"description": "Customer name"
},
"item_count": {
"type": "integer",
"exclusiveMinimum": 0.0,
"title": "Item Count",
"description": "Amount of items in order"
},
"id": {
"type": "string",
"maxLength": 36,
"minLength": 36,
"title": "Id",
"description": "Order ID as UUID"
}
},
"type": "object",
"required": [
"name",
"item_count",
"id"
],
"title": "CreateOrderOutput"
},
"CreateOrderRequest": {
"properties": {
"customer_name": {
"type": "string",
"maxLength": 20,
"minLength": 1,
"title": "Customer Name",
"description": "Customer name"
},
"order_item_count": {
"type": "integer",
"exclusiveMinimum": 0.0,
"title": "Order Item Count",
"description": "Amount of items in order"
}
},
"type": "object",
"required": [
"customer_name",
"order_item_count"
],
"title": "CreateOrderRequest"
},
"InternalServerErrorOutput": {
"properties": {
"error": {
"type": "string",
"title": "Error",
"description": "Error description",
"default": "internal server error"
}
},
"type": "object",
"title": "InternalServerErrorOutput"
},
"InvalidRestApiRequest": {
"properties": {
"details": {
"items": {
"$ref": "#/components/schemas/PydanticError"
},
"type": "array",
"title": "Details",
"description": "Error details"
}
},
"type": "object",
"required": [
"details"
],
"title": "InvalidRestApiRequest"
},
"PydanticError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Loc",
"description": "Error location"
},
"type": {
"type": "string",
"title": "Type",
"description": "Error type"
},
"msg": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Msg",
"description": "Error message"
}
},
"type": "object",
"required": [
"loc",
"type",
"msg"
],
"title": "PydanticError"
}
}
}
}
6 changes: 6 additions & 0 deletions docs/swagger/swagger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Swagger
description: Swagger Documentation
---

!!swagger openapi.json!!
27 changes: 15 additions & 12 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ nav:
- Getting Started: getting_started.md
- CDK: cdk.md
- Pipeline: pipeline.md
- Swagger: swagger/swagger.md
- Best Practices:
- best_practices/logger.md
- best_practices/tracer.md
Expand Down Expand Up @@ -80,19 +81,21 @@ copyright: Copyright © 2023 Ran Isenberg
plugins:
- git-revision-date
- search
- render_swagger
- glightbox:
touchNavigation: true
loop: false
effect: zoom
slide_effect: slide
width: 100%
height: auto
zoomable: true
draggable: true
skip_classes:
- custom-skip-class-name
auto_caption: false
caption_position: bottom
touchNavigation: true
loop: false
effect: zoom
slide_effect: slide
width: 100%
height: auto
zoomable: true
draggable: true
skip_classes:
- custom-skip-class-name
auto_caption: false
caption_position: bottom

extra_css:
- stylesheets/extra.css

Expand Down
Loading