Skip to content

Commit

Permalink
feat(AWS Deploy): Ensure existence of S3 bucket if possible
Browse files Browse the repository at this point in the history
  • Loading branch information
pgrzesik committed Dec 8, 2021
1 parent 664d8dc commit f358585
Show file tree
Hide file tree
Showing 9 changed files with 385 additions and 145 deletions.
15 changes: 6 additions & 9 deletions lib/plugins/aws/deploy/index.js
Expand Up @@ -6,12 +6,13 @@ const extendedValidate = require('./lib/extendedValidate');
const setBucketName = require('../lib/setBucketName');
const checkForChanges = require('./lib/checkForChanges');
const monitorStack = require('../lib/monitorStack');
const checkIfBucketExists = require('../lib/check-if-bucket-exists');
const createStack = require('./lib/createStack');
const cleanupS3Bucket = require('./lib/cleanupS3Bucket');
const uploadArtifacts = require('./lib/uploadArtifacts');
const validateTemplate = require('./lib/validateTemplate');
const updateStack = require('../lib/updateStack');
const existsDeploymentBucket = require('./lib/existsDeploymentBucket');
const ensureValidBucketExists = require('./lib/ensure-valid-bucket-exists');
const path = require('path');
const { style, log, progress, writeText, legacy } = require('@serverless/utils/log');
const memoize = require('memoizee');
Expand All @@ -36,11 +37,12 @@ class AwsDeploy {
setBucketName,
checkForChanges,
cleanupS3Bucket,
ensureValidBucketExists,
uploadArtifacts,
validateTemplate,
updateStack,
existsDeploymentBucket,
monitorStack
monitorStack,
checkIfBucketExists
);

this.getFileStats = memoize(this.getFileStats.bind(this), { promise: true });
Expand Down Expand Up @@ -104,11 +106,6 @@ class AwsDeploy {
'before:deploy:deploy': async () => {
await this.serverless.pluginManager.spawn('aws:common:validate');

const bucketName = this.serverless.service.provider.deploymentBucket;
if (bucketName) {
await this.existsDeploymentBucket(bucketName);
}

if (!this.options.package && !this.serverless.service.package.path) {
return this.extendedValidate();
}
Expand All @@ -127,7 +124,7 @@ class AwsDeploy {
'aws:deploy:deploy:createStack': async () => this.createStack(),

'aws:deploy:deploy:checkForChanges': async () => {
await this.setBucketName();
await this.ensureValidBucketExists();
await this.checkForChanges();
},

Expand Down
161 changes: 161 additions & 0 deletions lib/plugins/aws/deploy/lib/ensure-valid-bucket-exists.js
@@ -0,0 +1,161 @@
'use strict';

const ServerlessError = require('../../../../serverless-error');
const { log, legacy, progress } = require('@serverless/utils/log');

const mainProgress = progress.get('main');

module.exports = {
async ensureValidBucketExists() {
legacy.log('Ensuring that deployment bucket exists');

// Ensure to set bucket name if it can be resolved
// Result of this operation will determine how further validation will be performed
try {
await this.setBucketName();
} catch (err) {
// If there is a validation error with expected message, it means that logical resource for
// S3 bucket does not exist and we want to proceed with handling that situation
if (
err.providerError.code !== 'ValidationError' ||
!err.message.includes('does not exist for stack')
) {
throw err;
}
}

// Validate that custom deployment bucket exists and has proper location
if (this.serverless.service.provider.deploymentBucket) {
let result;
try {
result = await this.provider.request('S3', 'getBucketLocation', {
Bucket: this.bucketName,
});
} catch (err) {
throw new ServerlessError(
`Could not locate deployment bucket. Error: ${err.message}`,
'DEPLOYMENT_BUCKET_NOT_FOUND'
);
}

if (result.LocationConstraint === '') result.LocationConstraint = 'us-east-1';
if (result.LocationConstraint === 'EU') result.LocationConstraint = 'eu-west-1';
if (result.LocationConstraint !== this.provider.getRegion()) {
throw new ServerlessError(
'Deployment bucket is not in the same region as the lambda function',
'DEPLOYMENT_BUCKET_INVALID_REGION'
);
}
// If above is satisfied, then custom S3 bucket is valid
return;
}

// If bucket name is set, it means it's defined as a part of CloudFormation template (custom bucket case was handled by logic above)
if (this.bucketName) {
if (!(await this.checkIfBucketExists(this.bucketName))) {
// It means that bucket was removed manually but is still a part of the CloudFormation stack, we cannot manually fix it
throw new ServerlessError(
'Deployment bucket has been removed manually. Please recreate it or remove your service and attempt to deploy it again',
'DEPLOYMENT_BUCKET_REMOVED_MANUALLY'
);
}
return;
}

legacy.log(
'Deployment bucket not found. Updating stack to include deployment bucket definition.'
);
log.info(
'Deployment bucket not found. Updating stack to include deployment bucket definition.'
);
const stackName = this.provider.naming.getStackName();

// This is situation where the bucket is not defined in the template at all
// It covers the case where someone was using custom deployment bucket
// but removed that setting from the configuration
mainProgress.notice('Ensuring that deployment bucket exists', { isMainEvent: true });
const getTemplateResult = await this.provider.request('CloudFormation', 'getTemplate', {
StackName: stackName,
TemplateStage: 'Original',
});

const templateBody = JSON.parse(getTemplateResult.TemplateBody);
if (!templateBody.Resources) {
templateBody.Resources = {};
}
if (!templateBody.Outputs) {
templateBody.Outputs = {};
}
Object.assign(
templateBody.Resources,
this.serverless.service.provider.coreCloudFormationTemplate.Resources
);
Object.assign(
templateBody.Outputs,
this.serverless.service.provider.coreCloudFormationTemplate.Outputs
);
let stackTags = { STAGE: this.provider.getStage() };

// Merge additional stack tags
if (this.serverless.service.provider.stackTags) {
const customKeys = Object.keys(this.serverless.service.provider.stackTags);
const collisions = Object.keys(stackTags).filter((defaultKey) =>
customKeys.some((key) => defaultKey.toLowerCase() === key.toLowerCase())
);

// Delete collisions upfront
for (const key of collisions) {
delete stackTags[key];
}

stackTags = Object.assign(stackTags, this.serverless.service.provider.stackTags);
}

const params = {
StackName: stackName,
Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
Parameters: [],
Tags: Object.keys(stackTags).map((key) => ({ Key: key, Value: stackTags[key] })),
TemplateBody: JSON.stringify(templateBody),
};

const customDeploymentRole = this.provider.getCustomDeploymentRole();
if (customDeploymentRole) {
params.RoleARN = customDeploymentRole;
}

if (this.serverless.service.provider.notificationArns) {
params.NotificationARNs = this.serverless.service.provider.notificationArns;
}

if (this.serverless.service.provider.stackParameters) {
params.Parameters = this.serverless.service.provider.stackParameters;
}

// Policy must have at least one statement, otherwise no updates would be possible at all
if (
this.serverless.service.provider.stackPolicy &&
Object.keys(this.serverless.service.provider.stackPolicy).length
) {
params.StackPolicyBody = JSON.stringify({
Statement: this.serverless.service.provider.stackPolicy,
});
}

if (this.serverless.service.provider.rollbackConfiguration) {
params.RollbackConfiguration = this.serverless.service.provider.rollbackConfiguration;
}

if (this.serverless.service.provider.disableRollback) {
params.DisableRollback = this.serverless.service.provider.disableRollback;
}

if (templateBody.Transform) {
params.Capabilities.push('CAPABILITY_AUTO_EXPAND');
}

const cfData = await this.provider.request('CloudFormation', 'updateStack', params);
await this.monitorStack('update', cfData);
await this.setBucketName();
},
};
33 changes: 0 additions & 33 deletions lib/plugins/aws/deploy/lib/existsDeploymentBucket.js

This file was deleted.

0 comments on commit f358585

Please sign in to comment.