Skip to content

Instantly share code, notes, and snippets.

@ryanblock
Last active April 22, 2019 20:24
Show Gist options
  • Save ryanblock/205c57fff6c8b0fb4e9bbfdf9cb62761 to your computer and use it in GitHub Desktop.
Save ryanblock/205c57fff6c8b0fb4e9bbfdf9cb62761 to your computer and use it in GitHub Desktop.
Arc API Gateway upgrade script
// 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!`)
})
@mikeal
Copy link

mikeal commented Apr 18, 2019

A few problems:

  1. ’__dirname’ should be __dirname
  2. the script is throwing with the following error:
{ NotFoundException: Content-Type specified was not found
    at Object.extractError (/root/ipld.earth/node_modules/aws-sdk/lib/protocol/json.js:51:27)
    at Request.extractError (/root/ipld.earth/node_modules/aws-sdk/lib/protocol/rest_json.js:55:8)
    at Request.callListeners (/root/ipld.earth/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/root/ipld.earth/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/root/ipld.earth/node_modules/aws-sdk/lib/request.js:683:14)
    at Request.transition (/root/ipld.earth/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/root/ipld.earth/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /root/ipld.earth/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/root/ipld.earth/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/root/ipld.earth/node_modules/aws-sdk/lib/request.js:685:12)
  message: 'Content-Type specified was not found',
  code: 'NotFoundException',
  time: 2019-04-18T23:47:01.238Z,
  requestId: '42ed4cd4-6234-11e9-893a-5196d2a48ff8',
  statusCode: 404,
  retryable: false,
  retryDelay: 15.006495640518347 }

@ryanblock
Copy link
Author

Ah, that Content-Type specified was not found is probably the potential upsert I was mentioning, let's try add instead of replace for that one line, will update!

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