@bowtie/sls
Version:
Serverless helpers & utilities
531 lines (434 loc) • 22.4 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>parser.js - Documentation</title>
<script src="scripts/prettify/prettify.js"></script>
<script src="scripts/prettify/lang-css.js"></script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
<label for="nav-trigger" class="navicon-button x">
<div class="navicon"></div>
</label>
<label for="nav-trigger" class="overlay"></label>
<nav>
<li class="nav-link nav-home-link"><a href="index.html">Home</a></li><li class="nav-heading"><a href="global.html">Globals</a></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#actionFailureNotifySlack">actionFailureNotifySlack</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#bitbucketWebhook">bitbucketWebhook</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#buildChange">buildChange</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#buildChangeNotifyBitbucket">buildChangeNotifyBitbucket</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#buildChangeNotifyGithub">buildChangeNotifyGithub</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#buildChangeNotifySlack">buildChangeNotifySlack</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#decodeBody">decodeBody</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#deployBuild">deployBuild</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#deployEcr">deployEcr</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#deploymentNotifyAirbrake">deploymentNotifyAirbrake</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#deployments">deployments</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#deployS3">deployS3</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#describeStack">describeStack</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#findClusterName">findClusterName</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#findClusterStack">findClusterStack</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#findMigrationTask">findMigrationTask</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#getStatusColor">getStatusColor</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#githubWebhook">githubWebhook</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#init">init</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#initMigration">initMigration</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#parseBody">parseBody</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#parsePayload">parsePayload</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#prepareBuild">prepareBuild</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#runMigration">runMigration</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#slackCommand">slackCommand</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#slackResponse">slackResponse</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#stackChange">stackChange</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#startBuild">startBuild</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#trackBuild">trackBuild</a></span></li>
</nav>
<div id="main">
<h1 class="page-title">parser.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>// 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)
}
)
}
</code></pre>
</article>
</section>
</div>
<br class="clear">
<footer>
Generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.10</a> on Wed Apr 10 2024 18:08:07 GMT-0600 (Mountain Daylight Time) using the Minami theme.
</footer>
<script>prettyPrint();</script>
<script src="scripts/linenumber.js"></script>
</body>
</html>