UNPKG

wavefront-serverless-rollback-plugin

Version:
1,197 lines (1,059 loc) 36.5 kB
// // Copyright (c) 2017 Wavefront. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. 'use strict'; const BbPromise = require('bluebird'); const util = require('util'); const archiver = require('archiver'); const path = require('path'); const fs = require('fs'); const glob = require('glob-all'); const findParentDir = require('find-parent-dir'); const jsonfile = require('jsonfile'); const _ = require('lodash'); const uuidV4 = require('uuid/v4'); const copydir = require('copy-dir'); const rimraf = require('rimraf'); var wildstring = require('wildstring'); const request = BbPromise.promisify(require("request"), {multiArgs: true}); BbPromise.promisifyAll(request, {multiArgs: true}) class ServerlessWavefrontRollback { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.provider = this.serverless.getProvider('aws'); this.hooks = { 'before:deploy:deploy': () => BbPromise.bind(this) .then(this.init) .then(this.checkMandatoryParams) .then(this.getPreviousTemplate), 'after:deploy:deploy': () => BbPromise.bind(this) // .then(this.debugParams) .then(this.checkDeployment) .then(this.getResources) .then(this.createApiGateway) .then(this.getApiGatewayParams) .then(this.setInitialApiGatewayParams) .then(this.setIamRole) .then(this.deployRollbackFunction) .then(this.setRollbackFunctionApiGatewayParams) .then(this.deployWavefrontAlert), 'before:remove:remove': () => BbPromise.bind(this) .then(this.init) .then(this.getResources) .then(this.removeApiGateway) .then(this.removeRollbackFunction) .then(this.removeIAMRole) .then(this.removeWavefrontAlert) }; // this.serverless.cli.log(util.inspect(this.options, false, null)); } debugParams() { this.serverless.cli.log(util.inspect(Object.keys(this.serverless.service.functions), false, null)); } init() { this.options.stage = this.options.stage || (this.serverless.service.provider && this.serverless.service.provider.stage) || 'dev'; this.options.region = this.options.region || (this.serverless.service.provider && this.serverless.service.provider.region) || 'us-east-1'; this.previousTemplate; this.cloudFormationResources = []; this.apiGatewayResources = []; this.artifactFilePath = ''; this.deployedFunctionParams = {}; this.restApiCFParams = {}; this.restApiId = {}; this.apiGatewayResource = {}; this.iamRole = {}; this.maxRetries = 5; this.pluginName = 'wavefront-serverless-rollback-plugin'; this.pluginShortName = 'wf-rollback'; this.functionName = [this.pluginShortName, this.serverless.service.service, this.options.stage].join('-'); this.roleName = [this.pluginShortName, this.serverless.service.service, this.options.stage, 'role'].join('-'); this.policyName = [this.pluginShortName, this.serverless.service.service, this.options.stage, 'policy'].join('-'); this.srcDir = ['src', this.pluginName].join('-'); this.tempDir = ['.', this.pluginName].join(''); this.paramsFile = 'params.json'; this.localParamsFile = 'localParams.json'; this.restApiPath = 'rollback'; this.wildcardFunctionName = '<function_name>'; this.restApiOwnName = [this.options.stage, this.serverless.service.service, this.pluginShortName].join('-');; this.wavefrontApiKey = this.serverless.service.custom.wavefrontApiKey; this.srcParentDir; try { this.srcParentDir = findParentDir.sync(__dirname, this.srcDir); } catch(err) { throw new this.serverless.classes .Error('Plugin src folder not found. Make sure the plugin is installed.'); } if (!this.serverless.service.custom.wavefrontDebugMode) { this.restApiPath = uuidV4(); } const servicePath = this.serverless.config.servicePath; const zipFileName = 'wavefront-serverless-rollback-plugin.zip'; this.artifactFilePath = path.join( servicePath, this.tempDir, zipFileName ); this.localParamsFilePath = path.resolve( servicePath, this.tempDir, this.localParamsFile ); const dir = path.resolve( servicePath, this.tempDir ); if (!fs.existsSync(dir)){ fs.mkdirSync(dir); } try { this.prevLocalParams = jsonfile.readFileSync(this.localParamsFilePath); } catch(err) { } jsonfile.writeFileSync(this.localParamsFilePath, { APIGatewayResource: { path: this.restApiPath } }); } checkMandatoryParams() { if (!this.wavefrontApiKey) { throw new this.serverless.classes .Error('Please provide Wavefront API Key using custom wavefrontApiKey option.'); } if (!this.serverless.service.custom.wavefrontApiInstanceUrl) { throw new this.serverless.classes .Error('Please provide Wavefront instance URL using custom wavefrontApiInstanceUrl option.'); } if (!/^(f|ht)tps?:\/\//i.test(this.serverless.service.custom.wavefrontApiInstanceUrl)) { throw new this.serverless.classes .Error('Please provide complete Wavefront instance URL in wavefrontApiInstanceUrl option.'); } if (!this.serverless.service.custom.wavefrontRollbackAlertCondition) { throw new this.serverless.classes .Error('Please provide Wavefront alert trigger condition using custom wavefrontRollbackAlertCondition option.'); } } getPreviousTemplate() { this.serverless.cli.log('Getting previous CloudFormation template...'); const stackName = this.provider.naming.getStackName(this.options.stage); return this.provider.request('CloudFormation', 'getTemplate', { StackName: stackName }, this.options.stage, this.options.region) .catch((err) => { this.serverless.cli.log('First deployment detected'); }) .then((result) => { if (result) { this.previousTemplate = result.TemplateBody; this.serverless.cli.log('Template acquired'); // this.serverless.cli.log(util.inspect(this.previousTemplate, false, null)); } return BbPromise.resolve(); }); } checkDeployment() { if (!this.previousTemplate){ return BbPromise.reject('First deployment detected, will not upload rollback function.'); } else { return BbPromise.resolve(); } } getResources() { this.serverless.cli.log('Getting CloudFormation resource...'); const stackName = this.provider.naming.getStackName(this.options.stage); var params = { StackName: stackName }; return this.provider.request('CloudFormation', 'describeStackResources', params, this.options.stage, this.options.region) .then((result) => { if (result) { this.serverless.cli.log('CloudFormation resources acquired'); this.cloudFormationResources = result.StackResources; // this.serverless.cli.log(util.inspect(result, false, null)); this.restApiCFParams = _.find(this.cloudFormationResources, function(res) { wildstring.wildcard = '*'; return wildstring.match('ApiGatewayRestApi*', res.LogicalResourceId); }); } else this.serverless.cli.log('Failed getting resource'); return BbPromise.resolve(result); }); } createApiGateway() { if (!this.restApiCFParams || !this.restApiCFParams.PhysicalResourceId) { this.serverless.cli.log('No API Gateway resource from user...'); return this.provider.request( 'APIGateway', 'getRestApis', { }, this.options.stage, this.options.region) .then((result) => { let restApi = _.find(result.items, {name: this.restApiOwnName}); if (restApi) { this.serverless.cli.log('Reusing plugin owned API Gateway'); this.restApiId = restApi.id; return true; } return false; }) .then((result) => { if (!result) { // Need to create own API this.serverless.cli.log('Creating API Gateway param...'); return this.provider.request( 'APIGateway', 'createRestApi', { name: this.restApiOwnName }, this.options.stage, this.options.region) .catch((err) => { BbPromise.reject(err); }) .then((result) => { this.restApiId = result.id; return BbPromise.resolve(); }) } return BbPromise.resolve(); }); } this.restApiId = this.restApiCFParams.PhysicalResourceId; // Deleting plugin's API gateway if any return this.provider.request( 'APIGateway', 'getRestApis', { }, this.options.stage, this.options.region) .then((result) => { let restApi = _.find(result.items, {name: this.restApiOwnName}); if (restApi) { this.serverless.cli.log('Deleting unused plugin owned API Gateway...'); return this.provider.request( 'APIGateway', 'deleteRestApi', { restApiId: restApi.id }, this.options.stage, this.options.region); } return BbPromise.resolve(); }) } getApiGatewayParams() { this.serverless.cli.log('Getting API Gateway param...'); return this.provider.request('APIGateway', 'getResources', { restApiId: this.restApiId }, this.options.stage, this.options.region) .then((result) => { if (result) { this.serverless.cli.log('API Gateway resources acquired'); this.apiGatewayResources = result.items; // this.serverless.cli.log(util.inspect(result, false, null)); } else this.serverless.cli.log('Failed getting resource'); return BbPromise.resolve(result); }); } setInitialApiGatewayParams() { this.serverless.cli.log('Setup API Gateway...'); const apiGatewayResourceRoot = _.find(this.apiGatewayResources, {path: '/'}); const previousApiGatewayResource = this.prevLocalParams ? _.find(this.apiGatewayResources, {pathPart: this.prevLocalParams.APIGatewayResource.path}) : undefined; if (previousApiGatewayResource) { this.serverless.cli.log('API Gateway resource already exists'); if (!this.serverless.service.custom.wavefrontForceDeploy) { return BbPromise.reject('A rollback function API gateway already exists. Use wavefrontForceDeploy custom option to force update the rollback function.'); } this.serverless.cli.log('Deleting existing API Gateway resource...'); } return this.provider.request( 'APIGateway', 'deleteResource', { restApiId: this.restApiId, resourceId: previousApiGatewayResource ? previousApiGatewayResource.id : null }, this.options.stage, this.options.region ) .catch(() => {}) .then(() => { this.serverless.cli.log('Creating API Gateway resource...'); return this.provider.request( 'APIGateway', 'createResource', { restApiId: this.restApiId, parentId: apiGatewayResourceRoot.id, pathPart: this.restApiPath }, this.options.stage, this.options.region); }) .catch((err) => { BbPromise.reject(err); }) .then((result) => { this.apiGatewayResource = result; return BbPromise.resolve(); }); } setRollbackFunctionApiGatewayParams() { var functionArn; if (this.deployedFunctionParams.Version == '$LATEST') { functionArn = this.deployedFunctionParams.FunctionArn; } else { let indexToRemove = this.deployedFunctionParams.FunctionArn.lastIndexOf(':'); functionArn = this.deployedFunctionParams.FunctionArn.substring(0, indexToRemove); } this.serverless.cli.log('Creating API Gateway GET method...'); return this.provider.request( 'APIGateway', 'putMethod', { authorizationType: 'NONE', httpMethod: 'GET', resourceId: this.apiGatewayResource.id, restApiId: this.restApiId, apiKeyRequired: false }, this.options.stage, this.options.region) .catch((err) => { BbPromise.reject(err); }) .then((result) => { this.serverless.cli.log('Creating API Gateway GET integration request...'); return this.provider.request( 'APIGateway', 'putIntegration', { httpMethod: 'GET', resourceId: this.apiGatewayResource.id, restApiId: this.restApiId, type: 'AWS_PROXY', integrationHttpMethod: 'POST', uri: 'arn:aws:apigateway:' + this.options.region + ':lambda:path/2015-03-31/functions/' + functionArn + '/invocations' }, this.options.stage, this.options.region); }) .catch((err) => { BbPromise.reject(err); }) .then((result) => { this.serverless.cli.log('Creating API Gateway POST method...'); return this.provider.request( 'APIGateway', 'putMethod', { authorizationType: 'NONE', httpMethod: 'POST', resourceId: this.apiGatewayResource.id, restApiId: this.restApiId, apiKeyRequired: false }, this.options.stage, this.options.region); }) .catch((err) => { BbPromise.reject(err); }) .then((result) => { this.serverless.cli.log('Creating API Gateway POST integration request...'); return this.provider.request( 'APIGateway', 'putIntegration', { httpMethod: 'POST', resourceId: this.apiGatewayResource.id, restApiId: this.restApiId, type: 'AWS_PROXY', integrationHttpMethod: 'POST', uri: 'arn:aws:apigateway:' + this.options.region + ':lambda:path/2015-03-31/functions/' + functionArn + '/invocations' }, this.options.stage, this.options.region); }) .catch((err) => { BbPromise.reject(err); }) .then((result) => { this.serverless.cli.log('Creating API Gateway deployment...'); return this.provider.request( 'APIGateway', 'createDeployment', { restApiId: this.restApiId, stageName: this.options.stage, }, this.options.stage, this.options.region); }) .catch((err) => { BbPromise.reject(err); }) .then((result) => { this.serverless.cli.log('Removing Lambda permission...'); return this.provider.request( 'Lambda', 'removePermission', { FunctionName: this.deployedFunctionParams.FunctionName, StatementId: this.functionName + '-statementID', }, this.options.stage, this.options.region); }) .catch((err) => { }) .then((result) => { this.serverless.cli.log('Adding Lambda permission...'); return this.provider.request( 'Lambda', 'addPermission', { Action: 'lambda:InvokeFunction', FunctionName: this.deployedFunctionParams.FunctionName, Principal: 'apigateway.amazonaws.com', StatementId: this.functionName + '-statementID', }, this.options.stage, this.options.region); }) .catch((err) => { BbPromise.reject(err); }); } setIamRole() { return this.removeIAMRole() .then(() => { this.serverless.cli.log('Creating new IAM role...'); return this.provider.request( 'IAM', 'createRole', { AssumeRolePolicyDocument: JSON.stringify({ Version: '2012-10-17', Statement: [{ Effect: 'Allow', Principal: { Service: [ 'lambda.amazonaws.com' ] }, Action: [ 'sts:AssumeRole' ] }] }), Path: "/", RoleName: this.roleName }, this.options.stage, this.options.region); }) .catch((err) => { BbPromise.reject(err); }) .then((result) => { this.iamRole = result.Role; this.serverless.cli.log('Creating IAM role policy...'); return this.provider.request( 'IAM', 'putRolePolicy', { PolicyDocument: JSON.stringify({ Version: '2012-10-17', Statement: [ { Effect: 'Allow', Action: [ 'cloudformation:Describe*', 'cloudformation:Get*', 'cloudformation:Create*', 'cloudformation:Update*', 'apigateway:Get*', 'apigateway:POST', 'apigateway:DELETE', 'logs:Describe*', 'logs:Create*', 'logs:Delete*', 'iam:Put*', 'iam:Get*', 'events:*', 'lambda:*', 's3:Get*' ], Resource: '*' } ] }), PolicyName: this.policyName, RoleName: this.roleName }, this.options.stage, this.options.region); }) .catch((err) => { BbPromise.reject(err); }); } deployRollbackFunction() { this.serverless.cli.log(`Checking for rollback function ${this.functionName}...`); return this.provider.request( 'Lambda', 'getFunction', { FunctionName: this.functionName }, this.options.stage, this.options.region ) .catch(() => { this.serverless.cli.log('Rollback function does not exist yet'); return false; }) .then((isExist) => { if (isExist && !this.serverless.service.custom.wavefrontForceDeploy) { return BbPromise.reject('A rollback Lambda function already exists. Use wavefrontForceDeploy custom option to force update the rollback function.'); } this.generateJsonParams(); return this.zipDirectory() .then((result) => { var that = this; // Create new Lambda function if (!isExist) { this.serverless.cli.log('Creating new rollback function...'); this.serverless.cli.log('Deploying rollback lambda function...'); return this.retry(this.maxRetries, function(){ const data = fs.readFileSync(that.artifactFilePath); const createParams = { FunctionName: that.functionName, Handler: 'src-wavefront-serverless-rollback-plugin/rollback.rollback', Role: that.iamRole.Arn, Runtime: 'nodejs4.3', Code: { ZipFile: data } }; return that.provider.request( 'Lambda', 'createFunction', createParams, that.options.stage, that.options.region ); }) .then((res) => { this.deployedFunctionParams = res; this.serverless.cli.log(`Successfully deployed rollback function ${this.functionName}`); }); } // Update existing Lambda function else { this.serverless.cli.log('Rollback function already exists'); this.serverless.cli.log('Updating rollback function...'); this.serverless.cli.log('Deploying rollback lambda function...'); return this.retry(this.maxRetries, function(){ const data = fs.readFileSync(that.artifactFilePath); const createParams = { FunctionName: that.functionName, Publish: true, ZipFile: data }; return that.provider.request( 'Lambda', 'updateFunctionCode', createParams, that.options.stage, that.options.region ) }) .then((res) => { this.deployedFunctionParams = res; this.serverless.cli.log(`Successfully deployed rollback function ${this.functionName}`); }); } }); }); return BbPromise.resolve(); } deployWavefrontAlert() { this.serverless.cli.log('Deploying Wavefront rollback trigger...'); const wavefrontHost = this.serverless.service.custom.wavefrontApiInstanceUrl; const rollbackFunctionPath = 'https://' + this.restApiId + '.execute-api.' + this.options.region + '.amazonaws.com/' + this.options.stage + '/'; const rollbackFunctionEndpoint = rollbackFunctionPath + this.restApiPath; const userFunctionResources = _.filter(this.cloudFormationResources, {ResourceType: 'AWS::Lambda::Function'}); const alertCondition = this.serverless.service.custom.wavefrontRollbackAlertCondition; const alertMinutes = this.serverless.service.custom.wavefrontRollbackAlertTriggerThreshold ? this.serverless.service.custom.wavefrontRollbackAlertTriggerThreshold : 2; var currentWebhook = {}; this.serverless.cli.log('Getting existing Wavefront webhooks...'); return request({ url: wavefrontHost + '/api/v2/webhook', headers: { 'Authorization': 'Bearer ' + this.wavefrontApiKey } }) .spread((response, body, error) => { const bodyJson = JSON.parse(body); if (error) { return BbPromise.reject(error); } wildstring.wildcard = '*'; const previousWebhook = _.find(bodyJson.response.items, function(item) { return wildstring.match(rollbackFunctionPath + '*', item.recipient); }); const webhookParams = { description: 'Call rollback lambda for ' + this.provider.naming.getStackName(this.options.stage), template: '{"alertId": "{{{alertId}}}", "notificationId": "{{{notificationId}}}", "reason": "{{{reason}}}", "name": "{{#jsonEscape}}{{{name}}}{{/jsonEscape}}", "severity": "{{{severity}}}", "condition": "{{#jsonEscape}}{{{condition}}}{{/jsonEscape}}", "url": "{{{url}}}", "createdTime": "{{{createdTime}}}", "startedTime": "{{{startedTime}}}", "sinceTime": "{{{sinceTime}}}", "endedTime": "{{{endedTime}}}", "subject": "{{#jsonEscape}}{{{subject}}}{{/jsonEscape}}", "hostsFailingMessage": "{{#jsonEscape}}{{{hostsFailingMessage}}}{{/jsonEscape}}", "errorMessage": "{{#jsonEscape}}{{{errorMessage}}}{{/jsonEscape}}", "additionalInformation": "{{#jsonEscape}}{{{additionalInformation}}}{{/jsonEscape}}"}', title: 'Trigger Rollback Lambda', triggers: [ 'ALERT_OPENED' ], recipient: rollbackFunctionEndpoint, customHttpHeaders: {}, contentType: 'application/json' }; if (previousWebhook) { this.serverless.cli.log('Reusing existing Wavefront webhook...'); return request( { url: wavefrontHost + '/api/v2/webhook/' + previousWebhook.id, method: 'PUT', headers: { 'Authorization': 'Bearer ' + this.wavefrontApiKey }, json: webhookParams }) .spread((response, body, error) => { if (error) { return BbPromise.reject(error); } return BbPromise.resolve(body.response); }); } else { this.serverless.cli.log('Creating new Wavefront webhook...'); return request( { url: wavefrontHost + '/api/v2/webhook', method: 'POST', headers: { 'Authorization': 'Bearer ' + this.wavefrontApiKey }, json: webhookParams }) .spread((response, body, error) => { if (error) { return BbPromise.reject(error); } return BbPromise.resolve(body.response); }); } }) .then((webhook) => { // this.serverless.cli.log(util.inspect(webhook, false, null)); currentWebhook = webhook; this.serverless.cli.log('Getting existing Wavefront alerts...'); return request({ url: wavefrontHost + '/api/v2/alert', headers: { 'Authorization': 'Bearer ' + this.wavefrontApiKey } }); }) .spread((response, body, error) => { const bodyJson = JSON.parse(body); // this.serverless.cli.log(util.inspect(bodyJson, false, null)); if (error) { return BbPromise.reject(error); } var alertTarget = 'webhook:' + currentWebhook.id; if (this.serverless.service.custom.wavefrontAlertAdditionalTarget && this.serverless.service.custom.wavefrontAlertAdditionalTarget.length > 0) { alertTarget = [alertTarget] .concat(this.serverless.service.custom.alertAdditionalTarget) .join(', '); } BbPromise.map(userFunctionResources, (res) => { let functionName = res.PhysicalResourceId; let alertName = 'Alert for ' + functionName; const previousAlert = _.find(bodyJson.response.items, function(alert){ return alert.name == alertName; }); wildstring.wildcard = this.wildcardFunctionName; let condition = wildstring.replace(alertCondition, functionName); const alertParams = { name: alertName, target: alertTarget, condition: condition, minutes: alertMinutes, severity: 'SEVERE' }; if (previousAlert) { this.serverless.cli.log('Reusing existing Wavefront alert...'); alertParams.id = previousAlert.id; return request({ url: wavefrontHost + '/api/v2/alert/' + previousAlert.id, method: 'PUT', headers: { 'Authorization': 'Bearer ' + this.wavefrontApiKey }, json: alertParams }); } else { this.serverless.cli.log('Creating new Wavefront alert...'); return request({ url: wavefrontHost + '/api/v2/alert', method: 'POST', headers: { 'Authorization': 'Bearer ' + this.wavefrontApiKey }, json: alertParams }); } }) .catch((err) => { return BbPromise.reject(err); }); }); } removeApiGateway() { if (!this.restApiCFParams || !this.restApiCFParams.PhysicalResourceId) { this.serverless.cli.log('API Gateway does not exists'); return BbPromise.resolve(); } this.restApiId = this.restApiCFParams.PhysicalResourceId; return this.getApiGatewayParams() .then(() => { const previousApiGatewayResource = this.prevLocalParams ? _.find(this.apiGatewayResources, {pathPart: this.prevLocalParams.APIGatewayResource.path}) : undefined; this.serverless.cli.log('Removing API Gateway...'); return this.provider.request( 'APIGateway', 'deleteResource', { restApiId: this.restApiId, resourceId: previousApiGatewayResource ? previousApiGatewayResource.id : null }, this.options.stage, this.options.region); }) .catch((err)=>{ // this.serverless.cli.log(util.inspect(err, false, null)); }) .then((result) => { return BbPromise.resolve(); }); } removeIAMRole() { this.serverless.cli.log('Removing IAM Role...'); return this.provider.request( 'IAM', 'deleteRolePolicy', { RoleName: this.roleName, PolicyName: this.policyName }, this.options.stage, this.options.region) .catch((err) => { // this.serverless.cli.log(util.inspect(err, false, null)); }) .then(() => { return this.provider.request( 'IAM', 'deleteRole', { RoleName: this.roleName }, this.options.stage, this.options.region); }) .catch((err) => { // this.serverless.cli.log(util.inspect(err, false, null)); }) .then(() => { return BbPromise.resolve(); }); } removeRollbackFunction() { this.serverless.cli.log('Removing rollback function...'); return this.provider.request( 'Lambda', 'deleteFunction', { FunctionName: this.functionName }, this.options.stage, this.options.region) .catch((err) => { // this.serverless.cli.log(util.inspect(err, false, null)); }) .then(() => { return BbPromise.resolve(); }); } removeWavefrontAlert() { const wavefrontHost = this.serverless.service.custom.wavefrontApiInstanceUrl; const rollbackFunctionPath = 'https://' + this.restApiId + '.execute-api.' + this.options.region + '.amazonaws.com/' + this.options.stage + '/'; const userFunctionResources = _.filter(this.cloudFormationResources, {ResourceType: 'AWS::Lambda::Function'}); this.serverless.cli.log('Removing Wavefront alert...'); return request({ url: wavefrontHost + '/api/v2/webhook', headers: { 'Authorization': 'Bearer ' + this.wavefrontApiKey } }) .spread((response, body, error) => { const bodyJson = JSON.parse(body); if (error) { return BbPromise.resolve(); } wildstring.wildcard = '*'; const previousWebhook = _.find(bodyJson.response.items, function(item) { return wildstring.match(rollbackFunctionPath + '*', item.recipient); }); if (previousWebhook) { return request( { url: wavefrontHost + '/api/v2/webhook/' + previousWebhook.id, method: 'DELETE', headers: { 'Authorization': 'Bearer ' + this.wavefrontApiKey } }) .spread((response, body, error) => { return BbPromise.resolve(); }); } }) .then(() => { // this.serverless.cli.log(util.inspect(webhook, false, null)); return request({ url: wavefrontHost + '/api/v2/alert', headers: { 'Authorization': 'Bearer ' + this.wavefrontApiKey } }); }) .spread((response, body, error) => { const bodyJson = JSON.parse(body); // this.serverless.cli.log(util.inspect(bodyJson, false, null)); if (error) { return BbPromise.resolve(); } BbPromise.map(userFunctionResources, (res) => { let functionName = res.PhysicalResourceId; let alertName = 'Alert for ' + functionName; const previousAlert = _.find(bodyJson.response.items, function(alert){ return alert.name == alertName; }); if (previousAlert) { return request({ url: wavefrontHost + '/api/v2/alert/' + previousAlert.id, method: 'DELETE', headers: { 'Authorization': 'Bearer ' + this.wavefrontApiKey } }); } }) .catch((err) => {}); }) .then(() => { return BbPromise.resolve(); }); } generateJsonParams() { this.serverless.cli.log('Generating function params...'); const fullPath = path.resolve( this.srcParentDir, this.srcDir, this.paramsFile ); const wavefrontHost = this.serverless.service.custom.wavefrontApiInstanceUrl.replace(/^https?\:\/\//i, ""); const jsonObj = { CloudFormationRollbackParam: { StackName: this.provider.naming.getStackName(this.options.stage), TemplateBody: this.previousTemplate, Capabilities: ['CAPABILITY_NAMED_IAM'] }, WavefrontApiParam: { Authorization: 'Bearer ' + this.wavefrontApiKey, WavefrontHost: wavefrontHost }, LambdaFunctionParam: { FunctionName: this.functionName }, ApiGatewayParam: { resourceId: this.apiGatewayResource.id, restApiId: this.restApiId }, RolePolicyParam: { RoleName: this.roleName, PolicyName: this.policyName } }; jsonfile.writeFileSync(fullPath, jsonObj); return BbPromise.resolve(); } zipDirectory() { this.serverless.cli.log('Zipping rollback function files...'); const patterns = [ '**/' + this.srcDir + '/**', '!**/' + this.tempDir + '/**', ]; /* * Function taken from serverless/lib/plugins/package with modification * Might want to implement custom include/exclude pattern * in case user want to customize the plugin with their own code * exclude.forEach((pattern) => { if (pattern.charAt(0) !== '!') { patterns.push(`!${pattern}`); } else { patterns.push(pattern.substring(1)); } }); push the include globs to the end of the array (files and folders will be re-added again even if they were excluded beforehand) include.forEach((pattern) => { patterns.push(pattern); }); */ const localModulesDir = path.resolve( this.srcParentDir, this.srcDir, 'node_modules' ); const localModulesPath = path.resolve( this.srcParentDir, this.srcDir, 'rollbackPackage.json' ); const modulesDir = path.resolve( this.srcParentDir, 'node_modules' ); if (fs.existsSync(localModulesDir)) rimraf.sync(localModulesDir); fs.mkdirSync(localModulesDir); let localModules; try { localModules = jsonfile.readFileSync(localModulesPath); this.serverless.cli.log(util.inspect(localModules, false, null)); if (localModules) { this.copyModules(Object.keys(localModules.dependencies), modulesDir, localModulesDir); } } catch(err) { this.serverless.cli.log(util.inspect(err, false, null)); } const zip = archiver.create('zip'); const output = fs.createWriteStream(this.artifactFilePath); output.on('open', () => { zip.pipe(output); const files = glob.sync(patterns, { cwd: this.srcParentDir, dot: true, silent: true, follow: true, }); // this.serverless.cli.log(util.inspect(files, false, null)); files.forEach((filePath) => { const fullPath = path.resolve( this.srcParentDir, filePath ); const stats = fs.statSync(fullPath); // this.serverless.cli.log(util.inspect(fullPath, false, null)); if (!stats.isDirectory(fullPath)) { zip.append(fs.readFileSync(fullPath), { name: filePath, mode: stats.mode, }); } }); zip.finalize(); }); return new BbPromise((resolve, reject) => { output.on('close', () => resolve(this.artifactFilePath)); zip.on('error', (err) => reject(err)); }); } copyModules(depArr, from, to) { var that = this; _.each(depArr, function(dep){ let modulePathFrom = path.resolve(from, dep); let modulePathTo = path.resolve(to, dep); if (fs.existsSync(modulePathFrom)){ fs.mkdirSync(modulePathTo); that.serverless.cli.log(`Copying ${modulePathFrom}`); copydir.sync(modulePathFrom, modulePathTo); let packageFile = path.resolve(modulePathFrom, 'package.json'); try { let pack = jsonfile.readFileSync(packageFile); if (pack && pack.dependencies) { that.copyModules(Object.keys(pack.dependencies), from , to); } } catch(err){} } }); } retry(maxRetries, fn) { var that = this; return fn().catch(function(err) { if (maxRetries <= 0) { throw err; } return BbPromise.delay(1000).then(() => { return that.retry(maxRetries - 1, fn); }); }); } } module.exports = ServerlessWavefrontRollback;