Skip to content

Instantly share code, notes, and snippets.

@amnacog
Last active October 6, 2021 06:54
Show Gist options
  • Save amnacog/9a24dfcef232d79358d24b5e5754150f to your computer and use it in GitHub Desktop.
Save amnacog/9a24dfcef232d79358d24b5e5754150f to your computer and use it in GitHub Desktop.
OpenAPI template merger utility for aws APIGateway
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const NAME = process.argv[1].split('/')[process.argv[1].split('/').length - 1]
const BASE_IMAGE = 'composer:latest'
const amz_integration = {
"x-amazon-apigateway-integration" : {
"type" : "http_proxy",
"uri" : "{BACKEND_SERVICE}",
"httpMethod" : "{METHOD}",
"requestParameters" : {},
"cacheKeyParameters" : [],
"responses": {
"default": {
"statusCode": "200",
"content": {}
}
}
}
}
const injected_response_headers = {
"Access-Control-Allow-Origin": {"schema": {"type": "string"}},
"Access-Control-Allow-Methods": {"schema": {"type": "string"}},
"Access-Control-Allow-Headers": {"schema": {"type": "string"}},
}
const mergeDefinitions = (config) => {
const template = {
openapi: '3.0.0',
info: {
title: 'openApi template definition merger',
version: '1.0.0'
},
servers: [],
paths: {},
components: {schemas:{}}
}
if (config.external_url) {
template.servers.push({url: config.external_url, description: 'External'})
}
config.services.forEach(service => {
const args = ['run', '--rm', '-v', `${path.join(config.context, service)}:/app`, BASE_IMAGE, 'bash', '-c',
'cd /app;{ composer install --prefer-dist --no-dev --no-progress --no-scripts --no-interaction && composer symfony:dump-env prod; } &>/dev/null && bin/console nelmio:apidoc:dump']
const execRes = spawnSync('docker', args, {cwd: path.join(config.context, service)})
let serviceTemplate;
try {
serviceTemplate = JSON.parse(execRes.stdout.toString())
} catch (e) {
console.log('Stdout:' + execRes.stdout.toString())
console.log('Stderr:' + execRes.stderr.toString())
process.exit(1)
}
Object.keys(serviceTemplate.paths).forEach((uri) => {
if (uri === "/healthcheck") {
delete serviceTemplate.paths[uri]
return ;
}
Object.keys(serviceTemplate.paths[uri]).forEach((method) => {
if (template.paths?.[uri]?.[method] instanceof Object) {
throw new Error(`about to write a duplicate route: ${method}:${uri}`)
}
const amz_integration_route = JSON.parse(
JSON.stringify(amz_integration)
.replace('{BACKEND_SERVICE}', decodeURI(new URL(uri, config.backend_url[service])))
.replace('{METHOD}', method.toUpperCase())
)
delete serviceTemplate.paths[uri][method].operationId
if (serviceTemplate.paths[uri][method].parameters instanceof Object) {
serviceTemplate.paths[uri][method].parameters.forEach((parameter) => {
//default type
if (!parameter.type && !parameter.schema) {
parameter.schema = {type: "string"};
}
//remove array type
if (parameter.name.slice(-2) === "[]") {
parameter.name = parameter.name.substring(0, parameter.name.length - 2);
}
const type = parameter.in == "query" ? "querystring" : parameter.in
amz_integration_route['x-amazon-apigateway-integration'].requestParameters[`integration.request.${type}.${parameter.name}`] =
`method.request.${type}.${parameter.name}`
})
}
if (serviceTemplate.paths[uri][method].responses instanceof Object) {
Object.keys(serviceTemplate.paths[uri][method].responses).forEach((code) => {
serviceTemplate.paths[uri][method].responses[code].headers = {
...injected_response_headers,
...serviceTemplate.paths[uri][method].responses[code].headers
}
})
}
Object.assign(serviceTemplate.paths[uri][method], amz_integration_route)
})
})
Object.assign(template.paths, serviceTemplate.paths)
if (serviceTemplate?.components?.schemas) {
Object.assign(template.components.schemas, serviceTemplate.components.schemas)
}
})
return template;
}
const displayHelp = (exitCode = 0) => {
console.log(`OpenAPI template merger utility:
Usage: ${NAME} --services a,b,c [--template-file,--output-file,--external-url]
Options:
[R] --services \t\t\tSpecify the services to be used
\t\t\tin the merged template (separated by a comma)
[R] --backend-url\t\t\tBackend url to be use against the routes.
\t\t\tCan also be multivalues per service in
\t\t\tshorhand syntax eg: servicea=url,serviceb=url
--template-file\t\t\tJson file to be merged against the routes
\t\t\twith a set of rules
--output-file \t\t\tMerged template will be wrote here
\t\t\totherwise displayed to the console
--external-url\t\t\tSpecifiy a set of external url
\t\t\tto be displayed inside the template
--help \t\t\tDisplay this
* [R] = Required
`)
process.exit(exitCode)
}
const parseArgs = () => {
const args = process.argv.slice(2)
const params = {}
//context
params.context = process.argv[1]
.split('/')
.slice(0, process.argv[1].split('/').length - 2)
.join('/')
let skip_next = 0;
args.forEach((arg, idx) => {
if (skip_next) {
skip_next = 0
return ;
}
let parameter = arg
let value = args[idx + 1]
const equalArg = parameter.match(/(^\-\-[A-z_-]+)=(.*)/)
if (equalArg) {
([ _, parameter, value ] = equalArg)
} else {
skip_next = !0
}
try {
switch(parameter) {
case '--template-file':
const filePath = path.join(process.cwd(), value);
fs.statSync(filePath)
params.templateFile = filePath
break;
case '--services':
const services = value.split(',')
services.forEach((service) => {
try {
fs.statSync(path.join(params.context, service))
} catch(e) {
throw new Error(`service not found: '${service}'`)
}
})
params.services = services
break;
case '--external-url':
params.external_url = value;
break;
case '--backend-url':
params.backend_url = []
if (!value.includes('=') && !value.includes(',')) {
params.services.forEach((service) => {
params.backend_url[service] = value;
})
break;
}
value.split(',').forEach((segment) => {
[service, uri] = segment.split('=')
params.backend_url[service] = uri
})
break;
case '--output-file':
params.outputFile = value;
break;
case '--help':
displayHelp()
}
} catch (e) {
console.error(`Error on '${parameter}': ${e.message}`)
process.exit(1)
}
})
return params
}
process.argv.length == 2 && displayHelp()
const checkDocker = spawnSync('docker', ['stats', '--no-stream'])
if (checkDocker.status) {
console.error('Error: Docker daemon must be running')
process.exit(1)
}
const config = parseArgs();
if (!config.services) {
console.error('Error: You must specify at least one service')
displayHelp(1)
} else if (!config.backend_url) {
console.error('Error: You must specify the backend url')
displayHelp(1)
}
spawnSync('docker', ['pull', BASE_IMAGE])
const final = mergeDefinitions(config)
if (config.outputFile) {
fs.writeFileSync(path.join(process.cwd(), config.outputFile), JSON.stringify(final))
} else {
console.log(require('util').inspect(final, {colors: true, depth: 10}))
}
@amnacog
Copy link
Author

amnacog commented Oct 5, 2021

Little script used to generate a complete openapi definition (like swagger) and use it on apigateway

Context:

root
|
└───serviceA
|
└───serviceB
|
└───tools
    │   openapi-merger

the script will scan each service (currently symfony) then execute a command inside docker (can be modified) to dump the openapi definition from each services, then aws apigateway specs are injected inside the json and finally concatenate the complete object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment