Last active
April 22, 2019 20:24
-
-
Save ryanblock/205c57fff6c8b0fb4e9bbfdf9cb62761 to your computer and use it in GitHub Desktop.
Arc API Gateway upgrade script
This file contains 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
// I like using dotenv locally but that's up to you | |
require('dotenv').config() | |
const aws = require('aws-sdk') | |
const path = require('path') | |
const fs = require('fs') | |
const series = require('run-series') | |
const waterfall = require('run-waterfall') | |
const requestTemplate = fs.readFileSync(path.join(__dirname, 'node_modules', '@architect', 'architect', 'src', 'create', 'aws', 'create-http-route', 'create-route', '_request.vtl')).toString() | |
const requestFormPostTemplate = fs.readFileSync(path.join(__dirname, 'node_modules', '@architect', 'architect', 'src', 'create', 'aws', 'create-http-route', 'create-route', '_request-form-post.vtl')).toString() | |
const requestBinary = fs.readFileSync(path.join(__dirname, 'node_modules', '@architect', 'architect', 'src', 'create', 'aws', 'create-http-route', 'create-route', '_request-binary.vtl')).toString() | |
const responseTemplate = fs.readFileSync(path.join(__dirname, 'node_modules', '@architect', 'architect', 'src', 'create', 'aws', 'create-http-route', 'create-route', '_response.vtl')).toString() | |
// TODO ↓ add API Gateway ID! ↓ | |
const restApiId = '' | |
const apig = new aws.APIGateway() | |
let resourceQueue = [] | |
let counter = 0 | |
waterfall([ | |
// Get the API | |
(callback) => { | |
apig.getRestApi({ | |
restApiId | |
}, callback) | |
}, | |
// Update binaryMediaTypes if necessary | |
(api, callback) => { | |
console.log(`Updating API ${api.name} (ID: ${api.id})`) | |
// Skip if we're good | |
if (api.binaryMediaTypes && api.binaryMediaTypes.length === 1 && api.binaryMediaTypes[0] === '*/*') { | |
console.log('No binary media types to update') | |
callback() | |
} | |
else { | |
let patchOperations = [] | |
const encode = t => t.replace('/', '~1') | |
if (api.binaryMediaTypes && api.binaryMediaTypes.length) { | |
api.binaryMediaTypes.forEach(t => { | |
console.log('Removing binary media type:', t) | |
patchOperations.push({ | |
op: 'remove', | |
path: `/binaryMediaTypes/${encode(t)}`, | |
}) | |
}) | |
} | |
let add = `*/*` | |
console.log('Adding binary media type:', add) | |
patchOperations.push({ | |
op: 'add', | |
path: `/binaryMediaTypes/${encode(add)}`, | |
}) | |
apig.updateRestApi({ | |
restApiId, | |
patchOperations, | |
}, (err) => { | |
if (err) callback(err) | |
else callback() | |
}) | |
} | |
}, | |
// Get all the resource IDs within an API | |
(callback) => { | |
console.log('Updating integrations on:', restApiId) | |
apig.getResources({ | |
restApiId, | |
limit: 500, | |
}, (err, data) => { | |
if (err) callback(err) | |
else { | |
/** | |
* Build the queue of resources to operate on, i.e. | |
* [ | |
* [ 'abc123', 'GET' ], | |
* [ 'abc123', 'POST' ], | |
* [ 'def456', 'GET' ], | |
* ] | |
*/ | |
data.items.forEach(r => { | |
if (r.pathPart === '{proxy+}') return | |
else if (r.resourceMethods) { | |
Object.keys(r.resourceMethods).forEach(m => resourceQueue.push([r.id, m, r.path])) | |
} | |
else return | |
}) | |
callback() | |
} | |
}) | |
}, | |
// Process integration request templates and content settings | |
(callback) => { | |
let ops = resourceQueue.map(r => { | |
return (callback) => { | |
counter += 1 | |
let params = { | |
restApiId, | |
resourceId: r[0], | |
httpMethod: r[1], | |
} | |
// Get the integration state for each resource | |
apig.getIntegration(params, (err, data) => { | |
if (err) callback(err) | |
else { | |
let patchOperations = [] | |
let pusher = (op, path, value) => { | |
patchOperations.push({op, path, value}) | |
} | |
// Deal with passthroughBehavior | |
if (!data.passthroughBehavior) { | |
pusher('add','/passthroughBehavior','WHEN_NO_MATCH') | |
} | |
else if (data.passthroughBehavior !== 'WHEN_NO_MATCH') { | |
pusher('replace','/passthroughBehavior','WHEN_NO_MATCH') | |
} | |
// Deal with contentHandling | |
// Apparently APIG always has contentHandling set under the hood, even if it's not returned, so always replace it ¯\_(ツ)_/¯ | |
pusher('replace','/contentHandling','CONVERT_TO_TEXT') | |
// Update request templates | |
const responses = [ | |
'application~1json', | |
'application~1vnd.api+json', | |
'application~1xml', | |
'text~1css', | |
'text~1html', | |
'text~1javascript', | |
'text~1plain', | |
] | |
responses.forEach(r => { | |
pusher('replace', `/requestTemplates/${r}`, requestTemplate) | |
}) | |
// Look out, we got some special cases here | |
pusher('replace', `/requestTemplates/application~1x-www-form-urlencoded`, requestFormPostTemplate) | |
pusher('add', `/requestTemplates/application~1octet-stream`, requestBinary) | |
pusher('replace', `/requestTemplates/multipart~1form-data`, requestBinary) | |
// Wrap it up | |
setTimeout(() => { | |
Object.assign(params, {patchOperations}) | |
apig.updateIntegration(params, (err) => { | |
if (err) { | |
console.log('Failed on', r) | |
console.log('Got back', data) | |
console.log('Params', params) | |
callback(err) | |
} | |
else { | |
console.log('✓ Integration request update completed:', r[1], r[2]) | |
callback() | |
} | |
}) | |
}, 100) // Buffer a minimum of 100ms | |
} | |
}) | |
} | |
}) | |
series(ops, (err) => { | |
if (err) callback(err) | |
else callback() | |
}) | |
}, | |
// Process integration response templates | |
(callback) => { | |
console.log(`Completed ${counter} integration request updates`) | |
let timeout = 0 | |
counter = 0 | |
resourceQueue.forEach(r => { | |
timeout += 100 | |
let params = { | |
restApiId, | |
resourceId: r[0], | |
httpMethod: r[1], | |
statusCode: '200', // Arc only provisions 200 responses | |
responseTemplates: { | |
'text/html': responseTemplate, // 'text/html' is the default response template | |
}, | |
contentHandling: 'CONVERT_TO_TEXT' | |
} | |
setTimeout(() => { | |
apig.putIntegrationResponse(params, (err) => { | |
if (err) callback(err) | |
else { | |
counter += 1 | |
console.log('✓ Integration response update completed:', r[1], r[2]) | |
if (counter === resourceQueue.length) { | |
console.log(`Completed ${counter} integration response updates`) | |
callback() | |
} | |
} | |
}) | |
}, timeout) | |
}) | |
}, | |
], (err) => { | |
if (err) console.log(err) | |
else console.log(`🌈 Updated API ${restApiId} with all the new hotness!`) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Ah, that
Content-Type specified was not found
is probably the potential upsert I was mentioning, let's tryadd
instead ofreplace
for that one line, will update!