UNPKG

@bowtie/sls

Version:

Serverless helpers & utilities

471 lines (394 loc) 16 kB
// Include the query-string package // TODO: Use "qs" package instead? const queryString = require('query-string') const { Build, Deploy } = require('./models') /** * Decode event.body using query-string.parse() * @param {object} event */ module.exports.decodeBody = (event) => { // Return a promise for the promise chain return new Promise( (resolve, reject) => { // Ensure event has a body if (!event.body) { reject(new Error('Event has no body to be parsed.')) return } try { // Set the parsed body event.parsed.body = queryString.parse(event.body) // Resolve this promise resolve(event) } catch (e) { reject(e) } } ) } /** * Parse event.body using JSON.parse() * @param {object} event */ module.exports.parseBody = (event) => { // Return a promise for the promise chain return new Promise( (resolve, reject) => { // Ensure event has a body if (!event.body) { reject(new Error('Event has no body to be parsed.')) return } try { // Set the parsed body event.parsed.body = JSON.parse(event.body) console.log('Parsed request body:', JSON.stringify(event.parsed.body, null, 2)) // Resolve this promise resolve(event) } catch (e) { reject(e) } } ) } /** * Parse decoded body payload * @param {object} event */ module.exports.parsePayload = (event) => { // Return a promise for the promise chain return new Promise( (resolve, reject) => { // Ensure the body has already been parsed and contains a command if (!event.parsed.body || !event.parsed.body.payload) { reject(new Error('Event has no parsed body and/or payload.')) return } try { event.parsed.payload = JSON.parse(event.parsed.body.payload) resolve(event) } catch (e) { reject(e) } } ) } /** * Parse SNS Records into Slack messages * @param {object} event */ module.exports.stackChange = (event) => { // Return a promise for the promise chain return new Promise( (resolve, reject) => { // Ensure the event has Records to be parsed into messages if (!event.Records || event.Records.length === 0) { reject(new Error('Event has no records to be parsed.')) return } // Create empty array of parsed messages event.parsed.messages = [] // Loop through all event Records and build messages event.Records.forEach(r => { // Create empty message object const msg = {} // Split on new line and process each line r.Sns.Message.split('\n').forEach(line => { // Split each line into key/value pairs const parts = line.split('=') // Assign key/value pairs to message object after sanitizing if (parts.length === 2) { const name = parts[0] const value = parts[1].replace(/^\\|'/g, '') try { // Attempt to assign the value as a parsed object msg[name] = JSON.parse(value) } catch (e) { // Gracefully catch failed JSON.parse() and assign raw value msg[name] = value } } else { // Unknown line format, unable to build message with parts if (parts[0] && parts[0].replace(/^\\|'/g, '').trim() !== '') { console.log('Unknown message parts:', parts) } } }) // Add message to the parsed messages array (only if it has been created) if (Object.keys(msg).length > 0) { event.parsed.messages.push(msg) } }) // Resolve this promise resolve(event) } ) } /** * Parse an event from a change in a CodeBuild service build * @param {object} event */ module.exports.buildChange = (event) => { // Return a promise for the promise chain return new Promise( (resolve, reject) => { // Ensure the event has detail with "build-status" if (!event.detail || !event.detail['build-status']) { reject(new Error('No build details and/or status found in event.')) return } const { region } = event let slug = event.service.source.repo if (event.service.source.type === 'GITHUB') { slug = event.detail['additional-information'].source.location.match(/^https?:\/\/github.com\/(.*)\.git$/)[1] } else if (event.service.source.type === 'BITBUCKET') { slug = event.detail['additional-information'].source.location.match(/^https?:\/\/bitbucket.org\/(.*)\.git$/)[1] } console.log('build event details:') console.log(JSON.stringify(event.detail, null, 2)) // Build object for parsing build details const parsedBuild = { arn: event.detail['build-id'], env: event.detail['additional-information'].environment['environment-variables'], // arn:aws:codebuild:us-east-2:442555157363:build/sls-ci-example-app-us-east-2-build-project:d352736e-ec45-4370-9aed-1fb7d066a680 path: event.detail['build-id'].replace(/^arn:aws:codebuild:[^:]+:[0-9]+:/g, ''), info: event.detail['additional-information'], status: event.detail['build-status'], complete: event.detail['additional-information']['build-complete'], project_name: event.detail['project-name'], region, source: { slug, location: event.detail['additional-information'].source.location, version: event.detail['additional-information']['source-version'] }, details: event.detail } console.log('parsed build data') console.log(JSON.stringify(parsedBuild, null, 2)) // Find ENV vars by name const findBuildId = parsedBuild.env.filter(v => v.name === 'BUILD_ID') const findBuildBranch = parsedBuild.env.filter(v => v.name === 'BUILD_BRANCH') const findBuildAuthor = parsedBuild.env.filter(v => v.name === 'BUILD_AUTHOR') const findBuildSender = parsedBuild.env.filter(v => v.name === 'BUILD_SENDER') const findBuildPusher = parsedBuild.env.filter(v => v.name === 'BUILD_PUSHER') const findBuildRelease = parsedBuild.env.filter(v => v.name === 'BUILD_RELEASE') // Set additional parsedBuild fields, default to 'unknown' parsedBuild.id = (findBuildId.length > 0) ? findBuildId[0].value : 'unknown' parsedBuild.author = (findBuildAuthor.length > 0) ? findBuildAuthor[0].value : 'unknown' parsedBuild.sender = (findBuildSender.length > 0) ? findBuildSender[0].value : 'unknown' parsedBuild.pusher = (findBuildPusher.length > 0) ? findBuildPusher[0].value : 'unknown' parsedBuild.release = (findBuildRelease.length > 0) ? findBuildRelease[0].value : false parsedBuild.source.branch = (findBuildBranch.length > 0) ? findBuildBranch[0].value : 'unknown' // If build is complete and contains phases info, calculate total running time if (parsedBuild.complete && parsedBuild.info.phases) { // Reduce (sum) "duration-in-seconds" value from each build phase parsedBuild.duration = parsedBuild.info.phases.map(p => (p['duration-in-seconds'] || 0)).reduce((a, b) => a + b) } // Load link to build logs (if present) // [HIGH] TODO: Better way for devs to see logs? Can they be live? Can this link be created from build info (arn etc)? // if (parsedBuild.info.logs && parsedBuild.info.logs['deep-link']) { // parsedBuild.link = parsedBuild.info.logs['deep-link'] // } if (parsedBuild.arn) { parsedBuild.link = `https://${region}.console.aws.amazon.com/codesuite/codebuild/projects/${parsedBuild.project_name}/${parsedBuild.path}/log?region=${region}` } event.parsed.build = parsedBuild Build.get(parsedBuild.id).then(build => { if (build) { Object.assign(build, { build_status: parsedBuild.status, build_link: parsedBuild.link }) build.saveNotify().then(resp => { console.log('saved build', resp, build) event.parsed.build.model = build event.parsed.build.number = build.build_number resolve(event) }).catch(reject) } else { console.warn('Unable to find build', parsedBuild) resolve(event) } }).catch(reject) // Build.scanAll({ build_arn: { eq: parsedBuild.arn } }).then(builds => { // if (builds.length > 0) { // const build = builds[0] // Object.assign(build, { // build_status: parsedBuild.status, // build_link: parsedBuild.link // }) // build.saveNotify().then(resp => { // console.log('saved build', resp, build) // resolve(event) // }).catch(reject) // } else { // if (builds.length === 0) { // console.warn('Unable to find build', parsedBuild) // } else if (builds.length > 1) { // console.warn('Found multiple builds with arn:', parsedBuild.arn) // } // resolve(event) // } // }).catch(reject) // Set parsed build info // event.parsed.build = parsedBuild // resolve(event) } ) } /** * Parse deployments from event build details * @param {object} event */ module.exports.deployments = (event) => { // Return a promise for the promise chain return new Promise( (resolve, reject) => { // Ensure the build change details have been parsed if (!event.parsed.build) { console.log('No parsed build to parse deployments from') resolve(event) return } // Init parsed deployments to empty array event.parsed.deployments = [] console.log('parsed', event.parsed) // If parsed build has succeeded and service yaml contains deployment definitions, parse deployment details if (event.parsed.build.status === 'SUCCEEDED' && event.service.deployments) { // Tag to be deployed (from ECR repository) (prioritize release, then build number) let tag = event.parsed.build.release || event.parsed.build.number // Default deployKey to the source branch (key for deployments definition in service yaml) let deployKey = event.parsed.build.source.branch // Debug logs console.log('parsed build', event.parsed.build) // console.log('service release deployments', event.service.deployments['release']) console.log(`using deployKey: '${deployKey}'`) Object.keys(event.service.deployments).forEach(deploySource => { // Init deployment as null (not sure if deploying this branch/tag yet...) let deployment = null console.log(`checking deploySource: '${deploySource}'`) const srcRegEx = new RegExp(deploySource) // Test using RegExp (check if branch/ref matches key in deployments config) if (srcRegEx.test(deployKey)) { console.log('deployKey', deployKey, 'matches deploySource exact or as RegEx', srcRegEx) deployment = event.service.deployments[deploySource] console.log('using deployment', deployment) } else { console.log('NO MATCH: deployKey', deployKey, 'does not match deploySource as RegEx', srcRegEx) } // If deployment defined (and not already queued in parsed array), parse into deployment details and add to array if (deployment && !event.parsed.deployments.find(deploy => deploy.name === deployment.name)) { // Ensure targets is used as an array of strings const targetDeployments = Array.isArray(deployment) ? deployment : [ deployment ] console.log('pulling targetDeployments from deployment', targetDeployments) // If at least one target, parse deployment details if (targetDeployments.length > 0) { // Loop through targetDeployments targetDeployments.forEach(deployConfig => { const { name, overrides = {} } = deployConfig const deployInfo = { tag: tag.toString(), env: event.helpers.envGetAlias(name), user: event.parsed.build.sender, repo: event.service.source.repo, rev: event.parsed.build.source.version, stack: name, target: event.service.target, release: event.helpers.tagIsRelease(tag), build_id: event.parsed.build.id, deployment } const deployData = Object.assign({}, deployConfig, deployInfo, overrides) console.log(`Queueing deploy for '${name}' with data:`, deployData) // Push deployment details for current event.parsed.deployments.push(deployData) }) } } }) } else { // Debug logs console.log('Not deploying build:', event.parsed.build) } resolve(event) } ) } /** * Parse info from a Slack slash command event */ module.exports.slackCommand = (event) => { // Return a promise for the promise chain return new Promise( (resolve, reject) => { // Ensure the body has already been parsed and contains a command if (!event.parsed.body || !event.parsed.body.command) { reject(new Error('Event has no parsed body and/or command.')) return } // Ensure the parsed body contains Slack team info if (!event.parsed.body.team_id || !event.parsed.body.team_domain) { reject(new Error('Command event is missing team information')) return } // Ensure the parsed body contains Slack channel info if (!event.parsed.body.channel_id || !event.parsed.body.channel_name) { reject(new Error('Command event is missing channel information.')) return } // Ensure the parsed body contains Slack user info if (!event.parsed.body.user_id || !event.parsed.body.user_name) { reject(new Error('Command event is missing user information.')) return } // Create the parsed Slack command object event.parsed.slackCommand = { // Initialize args as empty array args: [], // Reference the entire parsed body as "request" for this command event request: event.parsed.body, // The actual command (string) being handled command: event.parsed.body.command, // Slack team object team: { id: event.parsed.body.team_id, domain: event.parsed.body.team_domain }, // Slack channel object channel: { id: event.parsed.body.channel_id, name: event.parsed.body.channel_name }, // Slack user object user: { id: event.parsed.body.user_id, name: event.parsed.body.user_name } } // If the command was sent with "text", parse the text into arguments (split on whitespace) if (event.parsed.body.text && event.parsed.body.text.trim() !== '') { event.parsed.slackCommand.args = event.parsed.body.text.replace(/\s\s+/g, ' ').split(' ') } // Resolve this promise resolve(event) } ) } /** * Parse info from a Slack response */ module.exports.slackResponse = (event) => { // Return a promise for the promise chain return new Promise( (resolve, reject) => { // Ensure the payload has already been parsed if (!event.parsed.payload) { reject(new Error('Event has no parsed payload.')) return } event.parsed.slackResponse = event.parsed.payload resolve(event) } ) }