Skip to content

Commit

Permalink
feat: Opt-in support for deployment bucket versioning (#9912)
Browse files Browse the repository at this point in the history
Configurable via `provider.deploymentBucket.versioning`
  • Loading branch information
mars-lan committed Oct 12, 2021
1 parent db85602 commit c4cb0f3
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 2 deletions.
2 changes: 2 additions & 0 deletions docs/providers/aws/guide/deploying.md
Expand Up @@ -83,6 +83,8 @@ The Serverless Framework translates all syntax in `serverless.yml` to a single A
- You can disable creation of default S3 bucket policy by setting `skipPolicySetup` under `deploymentBucket` config. It only applies to deployment bucket that is automatically created
by the Serverless Framework.

- You can enable versioning for the deployment bucket by setting `versioning` under `deploymentBucket` config to `true`.

Check out the [deploy command docs](../cli-reference/deploy.md) for all details and options.

- For information on multi-region deployments, [checkout this article](https://serverless.com/blog/build-multiregion-multimaster-application-dynamodb-global-tables).
Expand Down
1 change: 1 addition & 0 deletions docs/providers/aws/guide/serverless.yml.md
Expand Up @@ -54,6 +54,7 @@ provider:
skipPolicySetup: false # Prevents creation of default bucket policy when framework creates the deployment bucket. Default is false
name: com.serverless.${self:provider.region}.deploys # Deployment bucket name. Default is generated by the framework
maxPreviousDeploymentArtifacts: 5 # On every deployment the framework prunes the bucket to remove artifacts older than this limit. The default is 5
versioning: false # enable bucket versioning. Default is false
serverSideEncryption: AES256 # server-side encryption method
sseKMSKeyId: arn:aws:kms:us-east-1:xxxxxxxxxxxx:key/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa # when using server-side encryption
sseCustomerAlgorithim: AES256 # when using server-side encryption and custom keys
Expand Down
11 changes: 11 additions & 0 deletions lib/plugins/aws/package/lib/generateCoreTemplate.js
Expand Up @@ -60,6 +60,17 @@ module.exports = {
);
}

// enable S3 bucket versioning
if (deploymentBucketObject.versioning) {
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
deploymentBucketLogicalId
].Properties = {
VersioningConfiguration: {
Status: 'Enabled',
},
};
}

if (deploymentBucketObject.skipPolicySetup) {
const deploymentBucketPolicyLogicalId =
this.provider.naming.getDeploymentBucketPolicyLogicalId();
Expand Down
1 change: 1 addition & 0 deletions lib/plugins/aws/provider.js
Expand Up @@ -819,6 +819,7 @@ class AwsProvider {
skipPolicySetup: { type: 'boolean' },
maxPreviousDeploymentArtifacts: { type: 'integer', minimum: 0 },
name: { $ref: '#/definitions/awsS3BucketName' },
versioning: { type: 'boolean' },
serverSideEncryption: { enum: ['AES256', 'aws:kms'] },
sseCustomerAlgorithim: { type: 'string' },
sseCustomerKey: { type: 'string' },
Expand Down
41 changes: 40 additions & 1 deletion lib/plugins/aws/remove/lib/bucket.js
Expand Up @@ -10,7 +10,7 @@ module.exports = {
});
},

async listObjects() {
async listObjectsV2() {
this.objectsInBucket = [];

legacy.log('Getting all objects in S3 bucket...');
Expand All @@ -33,6 +33,45 @@ module.exports = {
});
},

async listObjectVersions() {
this.objectsInBucket = [];

this.serverless.cli.log('Getting all objects in S3 bucket...');
const serviceStage = `${this.serverless.service.service}/${this.provider.getStage()}`;

const result = await this.provider.request('S3', 'listObjectVersions', {
Bucket: this.bucketName,
Prefix: `${this.provider.getDeploymentPrefix()}/${serviceStage}`,
});

if (result) {
if (result.Versions) {
result.Versions.forEach((object) => {
this.objectsInBucket.push({
Key: object.Key,
VersionId: object.VersionId,
});
});
}

if (result.DeleteMarkers) {
result.DeleteMarkers.forEach((object) => {
this.objectsInBucket.push({
Key: object.Key,
VersionId: object.VersionId,
});
});
}
}
},

async listObjects() {
const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
return deploymentBucketObject && deploymentBucketObject.versioning
? this.listObjectVersions()
: this.listObjectsV2();
},

async deleteObjects() {
legacy.log('Removing objects in S3 bucket...');
if (this.objectsInBucket.length) {
Expand Down
21 changes: 21 additions & 0 deletions test/unit/lib/plugins/aws/package/lib/generateCoreTemplate.test.js
Expand Up @@ -95,6 +95,27 @@ describe('#generateCoreTemplate()', () => {
});
}));

it('should enable S3 bucket versioning if specified', async () => {
const { cfTemplate } = await runServerless({
config: {
service: 'irrelevant',
provider: {
name: 'aws',
deploymentBucket: {
versioning: true,
},
},
},
command: 'package',
});

expect(cfTemplate.Resources.ServerlessDeploymentBucket.Properties).to.deep.include({
VersioningConfiguration: {
Status: 'Enabled',
},
});
});

it('should add resource tags to the bucket if present', () =>
runServerless({
config: {
Expand Down
139 changes: 138 additions & 1 deletion test/unit/lib/plugins/aws/remove/lib/bucket.test.js
@@ -1,11 +1,14 @@
'use strict';

const expect = require('chai').expect;

const sinon = require('sinon');
const AwsProvider = require('../../../../../../../lib/plugins/aws/provider');
const AwsRemove = require('../../../../../../../lib/plugins/aws/remove/index');
const Serverless = require('../../../../../../../lib/Serverless');

const runServerless = require('../../../../../../utils/run-serverless');

describe('emptyS3Bucket', () => {
const options = {
stage: 'dev',
Expand Down Expand Up @@ -37,7 +40,7 @@ describe('emptyS3Bucket', () => {
});
});

describe('#listObjects()', () => {
describe('#listObjectsV2()', () => {
it('should resolve if no objects are present', () => {
const listObjectsStub = sinon.stub(awsRemove.provider, 'request').resolves();

Expand Down Expand Up @@ -80,6 +83,140 @@ describe('emptyS3Bucket', () => {
});
});

describe('#listObjectVersions()', () => {
const baseAwsRequestStubMap = {
STS: {
getCallerIdentity: {
ResponseMetadata: { RequestId: 'ffffffff-ffff-ffff-ffff-ffffffffffff' },
UserId: 'XXXXXXXXXXXXXXXXXXXXX',
Account: '999999999999',
Arn: 'arn:aws:iam::999999999999:user/test',
},
},
ECR: {
describeRepositories: sinon.stub().throws({
providerError: { code: 'RepositoryNotFoundException' },
}),
},
CloudFormation: {
describeStacks: {},
describeStackEvents: {
StackEvents: [
{
EventId: '1e2f3g4h',
StackName: 'new-service-dev',
LogicalResourceId: 'new-service-dev',
ResourceType: 'AWS::CloudFormation::Stack',
Timestamp: new Date(),
ResourceStatus: 'DELETE_COMPLETE',
},
],
},
describeStackResource: {
StackResourceDetail: { PhysicalResourceId: 'deployment-bucket' },
},
deleteStack: {},
},
};

it('should resolve if no object versions are present', async () => {
const listObjectVersionsStub = sinon.stub().resolves();

await runServerless({
command: 'remove',
config: {
service: 'test-service',
provider: {
name: 'aws',
stage: 'dev',
region: 'us-east-1',
deploymentPrefix: 'serverless',
deploymentBucket: {
name: 'bucket',
versioning: true,
},
},
},
awsRequestStubMap: {
...baseAwsRequestStubMap,
S3: {
listObjectVersions: listObjectVersionsStub,
},
},
});

expect(
listObjectVersionsStub.calledWithExactly({
Bucket: 'bucket',
Prefix: 'serverless/test-service/dev',
})
).to.be.equal(true);
});

it('should push objects to the array if present', async () => {
const listObjectVersionsStub = sinon.stub().resolves({
Versions: [
{ Key: 'object1', VersionId: null },
{ Key: 'object2', VersionId: 'v1' },
],
DeleteMarkers: [{ Key: 'object3', VersionId: 'v2' }],
});

const deleteObjectsStub = sinon.stub().resolves({
Deleted: [
{ Key: 'object1', VersionId: null },
{ Key: 'object2', VersionId: 'v1' },
{ Key: 'object3', VersionId: 'v2' },
],
});

await runServerless({
command: 'remove',
config: {
service: 'test-service',
provider: {
name: 'aws',
stage: 'dev',
region: 'us-east-1',
deploymentPrefix: 'serverless',
deploymentBucket: {
name: 'bucket',
versioning: true,
},
},
},
awsRequestStubMap: {
...baseAwsRequestStubMap,
S3: {
listObjectVersions: listObjectVersionsStub,
deleteObjects: deleteObjectsStub,
},
},
});

expect(listObjectVersionsStub.calledOnce).to.be.equal(true);
expect(
listObjectVersionsStub.calledWithExactly({
Bucket: 'bucket',
Prefix: 'serverless/test-service/dev',
})
).to.be.equal(true);

expect(
deleteObjectsStub.calledWithExactly({
Bucket: 'bucket',
Delete: {
Objects: [
{ Key: 'object1', VersionId: null },
{ Key: 'object2', VersionId: 'v1' },
{ Key: 'object3', VersionId: 'v2' },
],
},
})
).to.be.equal(true);
});
});

describe('#deleteObjects()', () => {
it('should delete all objects in the S3 bucket', () => {
awsRemove.objectsInBucket = [{ Key: 'foo' }];
Expand Down

0 comments on commit c4cb0f3

Please sign in to comment.