feat(swagger): swagger documentation deployment

main
flavien 2023-06-22 17:04:44 +02:00
parent ff6bd7bed9
commit 1f177ddf9a
No known key found for this signature in database
19 changed files with 443 additions and 39 deletions

2
.gitignore vendored
View File

@ -196,3 +196,5 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# go-task cache
.task/

View File

@ -1 +0,0 @@
a4f344a1cb3c3e0b077542f62f288858

View File

@ -1 +0,0 @@
a4f344a1cb3c3e0b077542f62f288858

View File

@ -1 +0,0 @@
d53f07fbdb97d1fbe6dd58682cad241a

View File

@ -1 +0,0 @@
f7efe372f54c13c2418d38d14dd5b970

View File

@ -43,17 +43,18 @@ tasks:
- hello_world.zip
terraform_plan:
deps: [terraform_init, build_services]
deps: [check_jq, terraform_init, build_services]
dir: "{{.TERRAFORM_DIRECTORY}}"
sources:
- '*.tf'
- '../../doc/*'
- '{{.CONFIG_DIRECTORY}}/_default_values/terraform.tfvars'
- '{{.CONFIG_DIRECTORY}}/{{.ENV}}/terraform.tfvars'
generates:
- '{{.PLAN}}'
- '{{.JSON_PLAN_FILE}}'
cmds:
- terraform fmt -check -diff -recursive
- terraform fmt -recursive
- terraform validate
- terraform plan --var-file=${DEFAULT_VARS} --var-file=${TFVARS} -out=${PLAN} -input=false
- terraform show --json ${PLAN} | jq -r '([.resource_changes[]?.change.actions?]|flatten)|{"create":(map(select(.=="create"))|length),"update":(map(select(.=="update"))|length),"delete":(map(select(.=="delete"))|length)}' > ${JSON_PLAN_FILE}

151
doc/api.yml Normal file
View File

@ -0,0 +1,151 @@
{
"openapi" : "3.0.1",
"info" : {
"title" : "terrapi-dev-eu-west-1-api-gateway",
"version" : "2023-06-22T14:37:40Z"
},
"servers" : [ {
"url" : "https://his423v1bb.execute-api.eu-west-1.amazonaws.com/{basePath}",
"variables" : {
"basePath" : {
"default" : "dev"
}
}
} ],
"paths" : {
"/hello_world" : {
"get" : {
"description" : "Return hello if authenticated with cognito",
"responses" : {
"200" : {
"description" : "200 response",
"headers" : {
"Access-Control-Allow-Origin" : {
"schema" : {
"type" : "string"
}
},
"Access-Control-Allow-Methods" : {
"schema" : {
"type" : "string"
}
},
"Access-Control-Allow-Headers" : {
"schema" : {
"type" : "string"
}
}
},
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Empty"
}
}
}
}
},
"security" : [ {
"terrapi-dev-eu-west-1-authorizer" : [ ]
} ],
"x-amazon-apigateway-integration" : {
"httpMethod" : "POST",
"uri" : "arn:aws:apigateway:eu-west-1:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-west-1:675523145650:function:hello_world/invocations",
"passthroughBehavior" : "when_no_match",
"timeoutInMillis" : 29000,
"type" : "aws_proxy"
}
},
"options" : {
"responses" : {
"200" : {
"description" : "200 response",
"headers" : {
"Access-Control-Allow-Origin" : {
"schema" : {
"type" : "string"
}
},
"Access-Control-Allow-Methods" : {
"schema" : {
"type" : "string"
}
},
"Access-Control-Allow-Headers" : {
"schema" : {
"type" : "string"
}
}
},
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Empty"
}
}
}
}
},
"x-amazon-apigateway-integration" : {
"responses" : {
"default" : {
"statusCode" : "200",
"responseParameters" : {
"method.response.header.Access-Control-Allow-Methods" : "'GET,OPTIONS,POST,PUT'",
"method.response.header.Access-Control-Allow-Headers" : "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
"method.response.header.Access-Control-Allow-Origin" : "'*'"
}
}
},
"requestTemplates" : {
"application/json" : "{\"statusCode\":200}"
},
"passthroughBehavior" : "when_no_match",
"timeoutInMillis" : 29000,
"type" : "mock"
}
}
}
},
"components" : {
"schemas" : {
"Empty" : {
"title" : "Empty Schema",
"type" : "object"
}
},
"securitySchemes" : {
"terrapi-dev-eu-west-1-authorizer" : {
"type" : "apiKey",
"name" : "Authorization",
"in" : "header",
"x-amazon-apigateway-authtype" : "cognito_user_pools",
"x-amazon-apigateway-authorizer" : {
"providerARNs" : [ "arn:aws:cognito-idp:eu-west-1:675523145650:userpool/eu-west-1_khgW77BGl" ],
"type" : "cognito_user_pools"
}
}
}
},
"x-amazon-apigateway-documentation" : {
"version" : "latest",
"createdDate" : "2023-06-22T09:23:57Z",
"documentationParts" : [ {
"location" : {
"type" : "API"
},
"properties" : {
"description" : "Test REST API Gateway deployed with terraform"
}
}, {
"location" : {
"type" : "METHOD",
"path" : "/hello_world",
"method" : "GET"
},
"properties" : {
"description" : "Return hello if authenticated with cognito"
}
} ]
}
}

32
doc/index.html Normal file
View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.35.0/swagger-ui-standalone-preset.min.js" integrity="sha512-WI88XrK/8xukiZdnlwlGrcdIyD9qgNXL15LiWbVnq0qpgd/YzRiewFplb5VyRxsbwZf7wRU5BnkCeNP/OV5CEg==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.35.0/swagger-ui-bundle.min.js" integrity="sha512-7aNGLo3pjgERnsRoSSRrr8Xy6lX8QeKJG3sh8qAeKDvRCExTvDxG6IPRNrCoY0EZG9B5BzGWV5l0xK9DqSSu+w==" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.35.0/swagger-ui.min.css" integrity="sha512-jsql70MmFqKJfWGCXmi3GHPP2q2oi3Ad+6PRQWNeo6df+rxKB07IuBvcCXSrpgKPXaikkQgEQVO2YrtgmSJhUw==" crossorigin="anonymous" />
<style>
body {
margin: 0;
padding: 0;
}
</style>
<script>
window.addEventListener("DOMContentLoaded", async () => {
const ui = SwaggerUIBundle({
url: "api.yml",
dom_id: "#main",
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
layout: "StandaloneLayout",
});
});
</script>
</head>
<body>
<div id="main"></div>
</body>
</html>

View File

@ -3,5 +3,10 @@ import json
def lambda_handler(event, context):
return {
'statusCode': 200,
'body': json.dumps('Hello World!')
'body': json.dumps('Hello World!'),
'headers': {
"Access-Control-Allow-Origin" : "*",
"Access-Control-Allow-Methods" : "GET,OPTIONS,POST,PUT",
"Access-Control-Allow-Headers" : "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token"
}
}

Binary file not shown.

View File

@ -23,3 +23,22 @@ provider "registry.terraform.io/hashicorp/aws" {
"zh:df5329c186545d273f9abaeeb39251d6cfcc446bccc44e27d1d7d02ebf145e2f",
]
}
provider "registry.terraform.io/hashicorp/null" {
version = "3.2.1"
hashes = [
"h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=",
"zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840",
"zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb",
"zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5",
"zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238",
"zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc",
"zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970",
"zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2",
"zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5",
"zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f",
"zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694",
]
}

View File

@ -5,3 +5,7 @@ output "invoke_url" {
output "client_id" {
value = aws_cognito_user_pool_client.client.id
}
output "documentation_url" {
value = aws_s3_bucket_website_configuration.documentation.website_endpoint
}

View File

@ -5,23 +5,18 @@ resource "aws_api_gateway_rest_api" "this" {
resource "aws_api_gateway_deployment" "this" {
rest_api_id = aws_api_gateway_rest_api.this.id
triggers = {
redeployment = timestamp()
}
lifecycle {
create_before_destroy = true
replace_triggered_by = [
null_resource.always_run
]
}
depends_on = [
aws_api_gateway_method.hello_world,
aws_api_gateway_integration.hello_world,
# aws_api_gateway_method.method, aws_api_gateway_method.options_method,
# aws_api_gateway_method_response.method_response, aws_api_gateway_method_response.options_200,
# aws_api_gateway_integration.integration, aws_api_gateway_integration.options_integration,
# aws_api_gateway_integration_response.options_integration_response,
# aws_api_gateway_authorizer.api_authorizer,
# aws_iam_role.invocation_role, aws_iam_role_policy.invocation_policy,
aws_api_gateway_method.hello_world_get,
aws_api_gateway_integration.hello_world_get,
aws_api_gateway_method.hello_world_options,
aws_api_gateway_integration.hello_world_options,
# aws_acm_certificate.ssl_certificate, aws_route53_record.cert_validation,
# aws_acm_certificate_validation.ssl_certificate_validation, aws_api_gateway_domain_name.this,
# aws_route53_record.api, aws_api_gateway_gateway_response.unauthorized_response, module.apigateway_resources,
@ -29,9 +24,10 @@ resource "aws_api_gateway_deployment" "this" {
}
resource "aws_api_gateway_stage" "this" {
stage_name = var.environment
rest_api_id = aws_api_gateway_rest_api.this.id
deployment_id = aws_api_gateway_deployment.this.id
stage_name = var.environment
rest_api_id = aws_api_gateway_rest_api.this.id
deployment_id = aws_api_gateway_deployment.this.id
documentation_version = aws_api_gateway_documentation_version.this.version
xray_tracing_enabled = var.xray_tracing_enabled
@ -40,14 +36,11 @@ resource "aws_api_gateway_stage" "this" {
}
depends_on = [
aws_api_gateway_method.hello_world,
aws_api_gateway_integration.hello_world,
# aws_api_gateway_method.method, aws_api_gateway_method.options_method,
# aws_api_gateway_method_response.method_response, aws_api_gateway_method_response.options_200,
# aws_api_gateway_integration.integration, aws_api_gateway_integration.options_integration,
# aws_api_gateway_integration_response.options_integration_response,
# aws_api_gateway_authorizer.api_authorizer,
# aws_iam_role.invocation_role, aws_iam_role_policy.invocation_policy,
aws_api_gateway_method.hello_world_get,
aws_api_gateway_integration.hello_world_get,
aws_api_gateway_method.hello_world_options,
aws_api_gateway_integration.hello_world_options,
aws_api_gateway_documentation_version.this,
# aws_acm_certificate.ssl_certificate, aws_route53_record.cert_validation,
# aws_acm_certificate_validation.ssl_certificate_validation, aws_api_gateway_domain_name.this,
# aws_route53_record.api, aws_api_gateway_gateway_response.unauthorized_response, module.apigateway_resources
@ -125,3 +118,37 @@ resource "aws_api_gateway_authorizer" "this" {
provider_arns = ["${aws_cognito_user_pool.pool.arn}"]
}
resource "aws_api_gateway_documentation_part" "api" {
location {
type = "API"
}
properties = "{\"description\":\"Test REST API Gateway deployed with terraform\"}"
rest_api_id = aws_api_gateway_rest_api.this.id
}
resource "aws_api_gateway_documentation_version" "this" {
version = "latest"
rest_api_id = aws_api_gateway_rest_api.this.id
description = "API documentation"
depends_on = [
aws_api_gateway_documentation_part.api,
aws_api_gateway_documentation_part.hello_world
]
}
resource "null_resource" "openapi_export" {
lifecycle {
replace_triggered_by = [
null_resource.always_run
]
}
depends_on = [
aws_api_gateway_stage.this
]
provisioner "local-exec" {
command = "aws apigateway get-export --parameters extensions='apigateway' --rest-api-id ${aws_api_gateway_rest_api.this.id} --stage-name ${aws_api_gateway_stage.this.stage_name} --export-type oas30 --output yaml ../../doc/api.yml"
}
}

View File

@ -4,20 +4,99 @@ resource "aws_api_gateway_resource" "hello_world" {
path_part = "hello_world"
}
resource "aws_api_gateway_method" "hello_world" {
resource "aws_api_gateway_method" "hello_world_get" {
rest_api_id = aws_api_gateway_rest_api.this.id
resource_id = aws_api_gateway_resource.hello_world.id
http_method = "GET"
authorization = "COGNITO_USER_POOLS"
authorizer_id = aws_api_gateway_authorizer.this.id
# authorization_scopes = ["${aws_cognito_resource_server.resource_server.scope_identifiers}"]
}
resource "aws_api_gateway_integration" "hello_world" {
resource "aws_api_gateway_method_response" "hello_world_get_200" {
rest_api_id = aws_api_gateway_rest_api.this.id
resource_id = aws_api_gateway_resource.hello_world.id
http_method = aws_api_gateway_method.hello_world_get.http_method
status_code = "200"
response_models = {
"application/json" = "Empty"
}
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = true,
"method.response.header.Access-Control-Allow-Methods" = true,
"method.response.header.Access-Control-Allow-Origin" = true
}
depends_on = [aws_api_gateway_method.hello_world_get]
}
resource "aws_api_gateway_integration" "hello_world_get" {
rest_api_id = aws_api_gateway_rest_api.this.id
resource_id = aws_api_gateway_resource.hello_world.id
http_method = aws_api_gateway_method.hello_world.http_method
http_method = aws_api_gateway_method.hello_world_get.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.hello_world.invoke_arn
}
}
resource "aws_api_gateway_method" "hello_world_options" {
rest_api_id = aws_api_gateway_rest_api.this.id
resource_id = aws_api_gateway_resource.hello_world.id
http_method = "OPTIONS"
authorization = "NONE"
}
resource "aws_api_gateway_method_response" "hello_world_options_200" {
rest_api_id = aws_api_gateway_rest_api.this.id
resource_id = aws_api_gateway_resource.hello_world.id
http_method = aws_api_gateway_method.hello_world_options.http_method
status_code = "200"
response_models = {
"application/json" = "Empty"
}
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = true,
"method.response.header.Access-Control-Allow-Methods" = true,
"method.response.header.Access-Control-Allow-Origin" = true
}
depends_on = [aws_api_gateway_method.hello_world_options]
}
resource "aws_api_gateway_integration" "hello_world_options" {
rest_api_id = aws_api_gateway_rest_api.this.id
resource_id = aws_api_gateway_resource.hello_world.id
http_method = aws_api_gateway_method.hello_world_options.http_method
type = "MOCK"
request_templates = {
"application/json" = jsonencode(
{
statusCode = 200
}
)
}
depends_on = [aws_api_gateway_method.hello_world_options]
}
resource "aws_api_gateway_integration_response" "hello_world_options_200" {
rest_api_id = aws_api_gateway_rest_api.this.id
resource_id = aws_api_gateway_resource.hello_world.id
http_method = aws_api_gateway_method.hello_world_options.http_method
status_code = aws_api_gateway_method_response.hello_world_options_200.status_code
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
"method.response.header.Access-Control-Allow-Methods" = "'GET,OPTIONS,POST,PUT'",
"method.response.header.Access-Control-Allow-Origin" = "'*'"
}
depends_on = [aws_api_gateway_method_response.hello_world_options_200]
}
resource "aws_api_gateway_documentation_part" "hello_world" {
location {
type = "METHOD"
method = "GET"
path = "/hello_world"
}
properties = "{\"description\":\"Return hello if authenticated with cognito\"}"
rest_api_id = aws_api_gateway_rest_api.this.id
}

83
terraform/code/buckets.tf Normal file
View File

@ -0,0 +1,83 @@
resource "aws_s3_bucket" "documentation" {
bucket = "${local.resource_prefix}-documentation"
tags = {
Name = local.resource_prefix
}
}
resource "aws_s3_bucket_public_access_block" "documentation" {
bucket = aws_s3_bucket.documentation.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_ownership_controls" "documentation" {
bucket = aws_s3_bucket.documentation.id
rule {
object_ownership = "BucketOwnerPreferred"
}
depends_on = [aws_s3_bucket_public_access_block.documentation]
}
# resource "aws_s3_bucket_acl" "s3_demo_bucket" {
# bucket = aws_s3_bucket.documentation.id
# acl = "public-read"
# }
resource "aws_s3_bucket_website_configuration" "documentation" {
bucket = aws_s3_bucket.documentation.id
index_document {
suffix = "index.html"
}
}
resource "aws_s3_bucket_policy" "documentation" {
bucket = aws_s3_bucket.documentation.id
policy = <<POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": [
"${aws_s3_bucket.documentation.arn}",
"${aws_s3_bucket.documentation.arn}/*"
]
}
]
}
POLICY
depends_on = [aws_s3_bucket_public_access_block.documentation]
}
resource "aws_s3_object" "swagger_index" {
key = "index.html"
source = "../../doc/index.html"
bucket = aws_s3_bucket.documentation.id
content_type = "text/html"
etag = filemd5("../../doc/index.html")
}
resource "aws_s3_object" "api_definition" {
key = "api.yml"
source = "../../doc/api.yml"
bucket = aws_s3_bucket.documentation.id
depends_on = [null_resource.openapi_export]
lifecycle {
replace_triggered_by = [
null_resource.always_run
]
}
}

View File

@ -31,6 +31,6 @@ resource "aws_lambda_permission" "apigw_lambda" {
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.hello_world.function_name
principal = "apigateway.amazonaws.com"
source_arn = "arn:aws:execute-api:${var.region}:${local.current_account_id}:${aws_api_gateway_rest_api.this.id}/*/${aws_api_gateway_method.hello_world.http_method}${aws_api_gateway_resource.hello_world.path}"
source_arn = "arn:aws:execute-api:${var.region}:${local.current_account_id}:${aws_api_gateway_rest_api.this.id}/*/${aws_api_gateway_method.hello_world_get.http_method}${aws_api_gateway_resource.hello_world.path}"
statement_id = "AllowExecutionFromAPIGateway"
}

View File

@ -0,0 +1,5 @@
resource "null_resource" "always_run" {
triggers = {
timestamp = "${timestamp()}"
}
}

Binary file not shown.

View File

@ -1,5 +1,5 @@
{
"create": 16,
"update": 0,
"delete": 0
"create": 4,
"update": 2,
"delete": 4
}