UNPKG

@bowtie/sls

Version:

Serverless helpers & utilities

223 lines (191 loc) 6.85 kB
// Include the AWS SDK const AWS = require('aws-sdk') const async = require('async') const { Deploy } = require('./models') // Create instances for ECR & CloudFormation const ECR = new AWS.ECR() const CloudFormation = new AWS.CloudFormation() /** * Handle a stack change event (as published from SNS topic) * @param {object} event */ module.exports.stackChange = (event) => { // Return a promise for the promise chain return new Promise( (resolve, reject) => { // Ensure the SNS records have been parsed into messages if (!event.parsed.messages || event.parsed.messages.length === 0) { reject(new Error('No messages to be sent.')) return } console.log('Messages:', JSON.stringify(event.parsed.messages, null, 2)) // Loop through all messages (async) async.each(event.parsed.messages, (msg, next) => { // Flag to determine whether this message is for parent stack or a stack resource const isStackMessage = (msg.ResourceType === 'AWS::CloudFormation::Stack' && msg.LogicalResourceId === msg.StackName) if (isStackMessage) { console.log('Tracking deploy status from msg:', msg) console.log('Current event object', JSON.stringify(event, null, 2)) next() } else { console.log('Not tracking deploy status for msg:', msg) next() } }, err => { if (err) { // Reject on error reject(err) } else { // Resolve with event resolve(event) } }) } ) } /** * Deploy S3 * @param {object} event * @param {object} deployment * @param {function} done */ const deployS3 = (event, deployment, done) => { console.log('Deploy to S3:', deployment) if (event.parsed.build.found) { done(new Error(`Skip deploy, build exists. Rebuilding`)); } else { done(null, 'SUCCEEDED'); } } /** * Deploy ECR * @param {object} event * @param {object} deployment * @param {function} done */ const deployEcr = (event, deployment, done) => { // Construct ECR parameters to search for the specified tag const ecrParams = { // Use repo as specified in the parsed service parameters repositoryName: process.env.ECR_REPO_NAME, imageIds: [ { // Search for an image tag as specified in the parsed service parameters imageTag: deployment.tag } ] } // Check if repo & tag exist ECR.batchGetImage(ecrParams, (err, data) => { // Reject on error if (err) { return done(err) } // Skip deployment if tag does not exist in ECR if (data.images.length === 0) { console.log('Unable to find ECR image with params:', JSON.stringify(ecrParams)) return done(null, 'FAILED') } // Ensure at least 1 image, and no failures // if (!data.images || data.images.length == 0 || data.failures.length > 0) { // return done(data) // } // Describe the stack (verifies it exists, and can build parameter list) CloudFormation.describeStacks({ // Get the stack name from the parsed service parameters StackName: deployment.stack }, (err, data) => { // Reject on error if (err) { console.warn(err) console.log('Unable to find CF Stack with name:', deployment.stack) return done(null, 'FAILED') } // This should be impossible, but ensure only a single stack is being returned if (data.Stacks.length > 1) { return done(new Error(`More than 1 stack matching name: ${deployment.stack}`)) } // Filter current stack parameters except for "Tag" const params = data.Stacks[0].Parameters.map(p => { return { ParameterKey: p.ParameterKey, UsePreviousValue: true } }).filter(p => p.ParameterKey !== 'Tag') // Add the "Tag" parameter with the tag as specified by the parsed service parameters params.push({ ParameterKey: 'Tag', ParameterValue: deployment.tag }) // Update the CloudFormation stack with the reconstructed parameters CloudFormation.updateStack({ // Get the stack name from the parsed service parameters StackName: deployment.stack, // Should include all previous parameters, and an updated "Tag" parameter Parameters: params, // Reuse the previous stack template UsePreviousTemplate: true, // Execute this stack update with the given role ARN // - This is passed into the environment by serverless (see "serverless.yml") RoleARN: process.env.UPDATE_ROLE_ARN, // A stack that creates/changes IAM roles/policies/etc MUST provide this capability flag Capabilities: [ 'CAPABILITY_IAM' ], // Send stack change notifications to the SNS topic ARN // - This is passed into the environment by serverless (see "serverless.yml") // - This SNS topic is automatically hooked up to the "stack-change" function to send messages to Slack NotificationARNs: [ process.env.NOTIFY_SNS_ARN ] }, (err) => { if (err) { return done(err) } done(null, 'IN_PROGRESS') }) }) }) } /** * Deploy build (from parsed deployments) * @param {object} event */ module.exports.deployBuild = (event) => { // Return a promise for the promise chain return new Promise( (resolve, reject) => { if (event.builds.prepared) { console.log('Event contains prepared build (new build). Skipping deployments ...') resolve(event) return } // Ensure the deployments have been parsed if (!event.parsed.deployments) { console.log('Event does not contain parsed deployment details.') resolve(event) return } // Debug logs for deployments being handled console.log('Parsed deployments', JSON.stringify(event.parsed.deployments, null, 2)) console.log('Current event object', JSON.stringify(event, null, 2)) // Loop through parsed deployments (async) and update CF stack(s) async.each(event.parsed.deployments, (deployment, next) => { const deployMethod = deployment.target === 's3' ? deployS3 : deployEcr deployMethod(event, deployment, (err, status) => { if (err) return next(err) const newDeploy = new Deploy(Object.assign({}, deployment, { service_name: process.env.SERVICE_NAME, deploy_status: status, deploy_timestamp: Date.now() })) newDeploy.saveNotify().then(data => { console.log('Saved deploy data:', data) next() }).catch(next) }) }, err => { if (err) { console.warn(err) } resolve(event) }) } ) }