@bowtie/sls
Version:
Serverless helpers & utilities
378 lines (341 loc) • 14.2 kB
JavaScript
/**
* Include utilities from @bowtie/sls
*/
const AWS = require('aws-sdk');
const qs = require('qs');
const fs = require('fs-extra')
const path = require('path')
const axios = require('axios')
const { action, parser, builder, migrator, deployer, notifier, validator, models, controllers, oauth, utils } = require('./src')
const { BaseController, BuildsController, DeploysController, DocumentsController, SubmissionsController } = controllers
const { Build, Deploy } = models
const { parseServiceConfig } = utils
const handlers = {}
const CORS_HEADERS = {
'Access-Control-Allow-Origin': '*', // Required for CORS support to work
'Access-Control-Allow-Credentials': true, // Required for cookies, authorization headers with HTTPS
};
// const s3 = new AWS.S3();
/**
* Generate MVC-Style REST API routes using controllers:
*/
// baseController = new BaseController
// buildsController = new BuildsController()
// deploysController = new DeploysController()
// TODO: Hookup actual build/deploy logic for create/update/destroy?
const methods = [ 'index', 'show', 'create', 'update', 'destroy', 'logs', 'deploy', 'tags', 'stacks', 'download', 'audits' ]
// const methods = [ 'index', 'show', 'logs' ]
const activeControllers = {
builds: new BuildsController(),
deploys: new DeploysController(),
documents: new DocumentsController(),
submissions: new SubmissionsController()
}
Object.keys(activeControllers).forEach(model => {
const ctrl = activeControllers[model]
methods.forEach(method => {
if (typeof ctrl[method] === 'function') {
handlers[`${model}_${method}`] = ctrl[method].bind(ctrl)
}
})
})
/**
* Add info handler to report service info
*/
if (typeof BaseController.info === 'function') {
handlers.info = BaseController.info
}
// handlers.spec = async (event, context) => {
// return {
// statusCode: 200,
// headers: BaseController.REQUIRED_RESPONSE_HEADERS,
// body: fs.readFileSync('spec.yml').toString()
// }
// }
handlers.oauthBitbucketAuthorize = (event, context, callback) => {
action.init(event).then(event => {
oauth.bitbucket.authorize(event, context, callback)
}).catch(callback)
}
/**
* Handle CloudFormation stack change events
* @param {object} event
* @param {object} context
* @param {function} callback
*/
handlers.stackChange = (event, context, callback) => {
action.init(event) // Initialize the action promise chain
.then(parser.stackChange) // Parse SNS Records into slack messages
.then(deployer.stackChange) // Handle stack change deployments
.then(notifier.stackChange) // Send parsed messages to slack
.then(migrator.stackChange) // Find & run DB migration on successfull stack change
.then(() => callback(null)) // Callback with null error successful
.catch(callback) // Callback with error when a promise in the chain is rejected
}
/**
* Handle Bitbucket webhook events (HTTP POST request on push)
* @param {object} event
* @param {object} context
* @param {function} callback
*/
handlers.bitbucketWebhook = (event, context, callback) => {
action.init(event) // Initialize the action promise chain
.then(validator.bitbucketWebhook) // Validate webhook source is bitbucket
.then(parser.parseBody) // Parse payload body (from JSON string => object)
.then(builder.prepareBuild) // Prepare next build (if applicable)
.then(builder.startBuild) // Start prepared build (if exists)
.then(builder.trackBuild) // Track started build (if started)
.then(parser.deployments) // Parse deployments (if any, defined in service yaml file)
.then(deployer.deployBuild) // Deploy build(s) (if any, determined by previous parse step)
.then(notifier.deploymentNotifyAirbrake) // Notify Airbrake of deployment (if any)
.then(e => {
// Successful webhook, return null error with response code 200
callback(null, {
statusCode: '200'
})
})
.catch(err => {
// Something failed, notify error using slack integration
notifier.actionFailureNotifySlack(err)
// Return callback with null error to avoid lambda reporting errors
.then(() => callback(null, { statusCode: '422' }))
.catch(callback)
})
}
/**
* Handle GitHub webhook events (HTTP POST request on push)
* @param {object} event
* @param {object} context
* @param {function} callback
*/
handlers.githubWebhook = (event, context, callback) => {
/**
* Use headers
* X-GitHub-Event = [ "push", "create", "pull_request" ]
*/
action.init(event)
.then(validator.githubWebhook)
.then(parser.parseBody)
.then(builder.prepareBuild)
.then(builder.startBuild)
.then(builder.trackBuild)
.then(parser.deployments)
.then(deployer.deployBuild)
.then(notifier.deploymentNotifyAirbrake)
.then(e => {
// Successful webhook, return null error with response code 200
callback(null, {
statusCode: '200'
})
})
.catch(err => {
// Something failed, notify error using slack integration
notifier.actionFailureNotifySlack(err)
// Return callback with null error to avoid lambda reporting errors
.then(() => callback(null, { statusCode: '422' }))
.catch(callback)
})
}
/**
* Handle CodeBuild status change events
* @param {object} event
* @param {object} context
* @param {function} callback
*/
handlers.buildChange = (event, context, callback) => {
action.init(event) // Initialize the action promise chain
.then(parser.buildChange) // Parse build change message details
.then(notifier.buildChange) // Update build from image details
.then(notifier.buildChangeNotifyGithub) // Notify github of changes / commit statuses
.then(notifier.buildChangeNotifyBitbucket) // Notify bitbucket of changes / commit statuses
.then(notifier.buildChangeNotifySlack) // Notify slack of build changes
.then(parser.deployments) // Parse deployments (from successful builds)
.then(deployer.deployBuild) // Deploy build(s) (if any, determined by previous parse step)
.then(notifier.deploymentNotifyAirbrake) // Notify airbrake of deployments (if any & airbrake is configured)
.then(e => callback(null)) // Successful handler, return callback with null error
.catch(err => {
// Something failed, notify error using slack integration
notifier.actionFailureNotifySlack(err)
// Return callback with null error to avoid lambda reporting errors
.then(() => callback(null))
.catch(callback)
})
}
// handlers.s3Download = (event, context, callback) => {
// action.init(event)
// .then(event => {
// const Expires = 5;
// const Bucket = process.env.ASSET_BUCKET_NAME;
// const { Key } = event.queryStringParameters;
// AWS.config.update({region: 'us-east-1'});
// const s3 = new AWS.S3({
// signatureVersion: 'v4'
// });
// const headers= {
// 'Location': s3.getSignedUrl('getObject', { Bucket, Key, Expires })
// };
// console.log('s3Download() headers', headers);
// return callback(null, {
// statusCode: '302',
// headers
// })
// })
// .catch(err => {
// // Something failed, notify error using slack integration
// notifier.actionFailureNotifySlack(err)
// // Return callback with null error to avoid lambda reporting errors
// .then(() => callback(null))
// .catch(callback)
// })
// }
handlers.s3Upload = (event, context, callback) => {
action.init(event)
.then(parser.parseBody)
.then(event => {
const queryParams = event.queryStringParameters || {};
const { ctx } = queryParams;
const useSecureBucket = (process.env.CTX_SECURE && ctx && ctx === process.env.CTX_SECURE);
const Bucket = useSecureBucket ? process.env.SECURE_BUCKET_NAME : process.env.ASSET_BUCKET_NAME;
const { Key, ContentType, Data } = event.parsed.body;
// const { Bucket = 'svig-testing-uploads', Key, ContentType } = event.queryStringParameters;
if (!Bucket || !Key || !ContentType) {
return callback(null, {
statusCode: '422',
headers: CORS_HEADERS,
body: JSON.stringify({ message: 'Invalid parameter(s)' })
})
}
// [HIGH] TODO: Validate incoming data with form submission(s)
// if (useSecureBucket && !Data) {
// return callback(null, {
// statusCode: '422',
// headers,
// body: JSON.stringify({ message: 'Invalid parameter(s)' })
// })
// }
// AWS.config.update({region: 'us-east-1'});
const s3 = new AWS.S3({
signatureVersion: 'v4'
});
// var params = {Bucket, Key};
// s3.getSignedUrl('putObject', params, function (err, url) {
// console.log('The URL is', url);
// });
s3.getSignedUrlPromise('putObject', { Bucket, Key, ContentType })
.then(signedUrl => {
let publicUrl;
console.log("Signed URL IS: ", signedUrl)
if (!useSecureBucket) {
publicUrl = `https://${Bucket}.s3.amazonaws.com/${Key}`;
console.log("Public URL IS: ", publicUrl)
}
callback(null, {
statusCode: '200',
headers: CORS_HEADERS,
body: JSON.stringify({ signedUrl, publicUrl, location: publicUrl })
})
})
.catch(err => {
console.error(err)
callback(null, {
statusCode: '400',
headers: CORS_HEADERS,
body: JSON.stringify({ message: err.message })
})
})
})
.catch(err => {
// Something failed, notify error using slack integration
notifier.actionFailureNotifySlack(err)
// Return callback with null error to avoid lambda reporting errors
.then(() => callback(null))
.catch(callback)
})
}
/**
* Verify recaptcha v3 token
* @param {ApiEvent} event - Request event
* @param {object} context - Handler execution context
*/
handlers.verifyRecaptcha = async (event, context) => {
if (event.queryStringParameters.token) {
const resp = await axios.post('https://www.google.com/recaptcha/api/siteverify', qs.stringify({
secret: process.env.RECAPTCHA_SECRET_KEY,
response: event.queryStringParameters.token
}));
return {
headers: CORS_HEADERS,
statusCode: '200',
body: JSON.stringify(resp.data)
};
} else {
return {
headers: CORS_HEADERS,
statusCode: '400',
message: 'Missing or invalid token'
};
}
};
// handlers.sendEmail = (event, context, callback) => {
// action.init(event) // Initialize the action promise chain
// .then(parser.parseBody) // Parse payload body (from JSON string => object)
// .then(notifier.sendEmail) // Send email (using SES)
// .then(response => {
// // Successful webhook, return null error with response code 200
// callback(null, {
// statusCode: '200',
// body: JSON.stringify(response)
// })
// })
// .catch(err => {
// // Something failed, notify error using slack integration
// notifier.actionFailureNotifySlack(err)
// // Return callback with null error to avoid lambda reporting errors
// .then(() => callback(null, { statusCode: '422', body: JSON.stringify({ message: err.message || 'Something failed' }) }))
// .catch(callback)
// })
// };
// /**
// * Show build logs
// * [HIGH] TODO: Authenticate?
// * @param {object} event
// * @param {object} context
// * @param {function} callback
// */
// handlers.buildLogs = (event, context, callback) => {
// action.init(event) // Initialize the action promise chain
// .then(builder.buildLogs) // Notify airbrake of deployments (if any & airbrake is configured)
// .then(e => callback(null, e.response)) // Successful handler, return callback with null error
// .catch(err => {
// callback(null, {
// statusCode: '422',
// body: JSON.stringify({ message: err.message || err })
// })
// })
// }
// /**
// * Handle Slack slash command events
// */
// handlers.slackCommand = (event, context, callback) => {
// action.init(event) // Initialize the action promise chain
// .then(parser.decodeBody) // Decode the urlencoded event.body into event.parsed.body
// .then(validator.slackCommand) // Validate the request token from Slack
// .then(parser.slackCommand) // Build command info from the parsed body
// .then(manager.slackCommand) // Handle the command built in the previous step
// .then((e) => callback(null, e.response)) // Callback with null error and constructed response if successful
// .catch(callback); // Callback with error when a promise in the chain is rejected
// }
// /**
// * Handle Slack response events
// */
// handlers.slackResponse = (event, context, callback) => {
// action.init(event) // Initialize the action promise chain
// .then(parser.decodeBody) // Decode the urlencoded event.body into event.parsed.body
// .then(parser.parsePayload) // Parse payload as JSON from the decoded body
// .then(validator.slackResponse) // Validate the request token from Slack
// .then(parser.slackResponse) // Build response info from the parsed body
// .then(manager.slackResponse) // Handle the response built in the previous step
// .then((e) => callback(null, e.response)) // Callback with null error and constructed response if successful
// .catch(callback); // Callback with error when a promise in the chain is rejected
// }
module.exports = handlers;