Last active
October 6, 2021 06:54
-
-
Save amnacog/9a24dfcef232d79358d24b5e5754150f to your computer and use it in GitHub Desktop.
OpenAPI template merger utility for aws APIGateway
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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})) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Little script used to generate a complete openapi definition (like swagger) and use it on apigateway
Context:
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.