Skip to content

Commit

Permalink
Add support for existing S3 buckets
Browse files Browse the repository at this point in the history
  • Loading branch information
pmuens committed Jul 8, 2019
1 parent 1fe3abb commit d05d7a1
Show file tree
Hide file tree
Showing 22 changed files with 1,174 additions and 19 deletions.
20 changes: 20 additions & 0 deletions docs/providers/aws/events/s3.md
Expand Up @@ -114,3 +114,23 @@ resources:
Ref: AWS::AccountId
SourceArn: 'arn:aws:s3:::my-custom-bucket-name'
```

## Using existing buckets

Sometimes you might want to attach Lambda functions to existing S3 buckets. In that case you just need to set the `existing` event configuration property to `true`. All the other config parameters can also be used on existing buckets:

**NOTE:** Using the `existing` config will add an additional Lambda function and IAM Role to your stack. The Lambda function backs-up the Custom S3 Resource which is used to support existing S3 buckets.

```yaml
functions:
users:
handler: users.handler
events:
- s3:
bucket: legacy-photos
event: s3:ObjectCreated:*
rules:
- prefix: uploads/
- suffix: .jpg
existing: true
```
110 changes: 110 additions & 0 deletions lib/plugins/aws/customResources/index.js
@@ -0,0 +1,110 @@
'use strict';

const path = require('path');
const createZipFile = require('../../../utils/fs/createZipFile');

function addCustomResourceToService(resourceName, iamRoleStatements) {
let FunctionName;
let Handler;
let customResourceFunctionLogicalId;

const Statement = iamRoleStatements;
const customResourcesRoleLogicalId = this.provider.naming.getCustomResourcesRoleLogicalId();
const srcDirPath = path.join(__dirname, 'resources');
const destDirPath = path.join(
this.serverless.config.servicePath,
'.serverless',
this.provider.naming.getCustomResourcesArtifactDirectoryName()
);
const zipFilePath = `${destDirPath}.zip`;
this.serverless.utils.writeFileDir(zipFilePath);

if (resourceName === 's3') {
FunctionName = `${this.serverless.service.service}-${
this.options.stage
}-${this.provider.naming.getCustomResourceS3HandlerFunctionName()}`;
Handler = 's3/handler.handler';
customResourceFunctionLogicalId = this.provider.naming.getCustomResourceS3HandlerFunctionLogicalId();
}

return createZipFile(srcDirPath, zipFilePath).then(outputFilePath => {
let S3Bucket = {
Ref: this.provider.naming.getDeploymentBucketLogicalId(),
};
if (this.serverless.service.package.deploymentBucket) {
S3Bucket = this.serverless.service.package.deploymentBucket;
}
const s3Folder = this.serverless.service.package.artifactDirectoryName;
const s3FileName = outputFilePath.split(path.sep).pop();
const S3Key = `${s3Folder}/${s3FileName}`;

const customResourceRole = {
[customResourcesRoleLogicalId]: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
Service: ['lambda.amazonaws.com'],
},
Action: ['sts:AssumeRole'],
},
],
},
Policies: [
{
PolicyName: {
'Fn::Join': [
'-',
[
this.provider.getStage(),
this.provider.serverless.service.service,
'custom-resources-lambda',
],
],
},
PolicyDocument: {
Version: '2012-10-17',
Statement,
},
},
],
},
},
};

const customResourceFunction = {
[customResourceFunctionLogicalId]: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket,
S3Key,
},
FunctionName,
Handler,
MemorySize: 1024,
Role: {
'Fn::GetAtt': [customResourcesRoleLogicalId, 'Arn'],
},
Runtime: 'nodejs10.x',
Timeout: 6,
},
DependsOn: [customResourcesRoleLogicalId],
},
};

Object.assign(
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
customResourceFunction,
customResourceRole
);
});
}

module.exports = {
addCustomResourceToService,
};
117 changes: 117 additions & 0 deletions lib/plugins/aws/customResources/index.test.js
@@ -0,0 +1,117 @@
'use strict';

const path = require('path');
const fs = require('fs');
const chai = require('chai');
const AwsProvider = require('../provider/awsProvider');
const Serverless = require('../../../Serverless');
const { createTmpDir } = require('../../../../tests/utils/fs');
const { addCustomResourceToService } = require('./index.js');

const expect = chai.expect;
chai.use(require('chai-as-promised'));

describe('#addCustomResourceToService()', () => {
let tmpDirPath;
let serverless;
let provider;
let context;
const iamRoleStatements = [
{
Effect: 'Allow',
Resource: 'arn:aws:lambda:*:*:function:custom-resource-func',
Action: ['lambda:AddPermission', 'lambda:RemovePermission'],
},
];

beforeEach(() => {
const options = {
stage: 'dev',
region: 'us-east-1',
};
tmpDirPath = createTmpDir();
serverless = new Serverless();
provider = new AwsProvider(serverless, options);
serverless.setProvider('aws', provider);
serverless.service.service = 'some-service';
serverless.service.provider.compiledCloudFormationTemplate = {
Resources: {},
};
serverless.config.servicePath = tmpDirPath;
serverless.service.package.artifactDirectoryName = 'artifact-dir-name';
context = {
serverless,
provider,
options,
};
});

describe('when using the custom S3 resouce', () => {
it('should add the custom resource to the service', () => {
return expect(
addCustomResourceToService.call(context, 's3', iamRoleStatements)
).to.be.fulfilled.then(() => {
const { Resources } = serverless.service.provider.compiledCloudFormationTemplate;
const customResourcesZipFilePath = path.join(
tmpDirPath,
'.serverless',
'custom-resources.zip'
);

expect(fs.existsSync(customResourcesZipFilePath)).to.equal(true);
expect(Resources).to.deep.equal({
CustomDashresourceDashexistingDashs3LambdaFunction: {
Type: 'AWS::Lambda::Function',
Properties: {
Code: {
S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
S3Key: 'artifact-dir-name/custom-resources.zip',
},
FunctionName: 'some-service-dev-custom-resource-existing-s3',
Handler: 's3/handler.handler',
MemorySize: 1024,
Role: { 'Fn::GetAtt': ['IamRoleCustomResourcesLambdaExecution', 'Arn'] },
Runtime: 'nodejs10.x',
Timeout: 6,
},
DependsOn: ['IamRoleCustomResourcesLambdaExecution'],
},
IamRoleCustomResourcesLambdaExecution: {
Type: 'AWS::IAM::Role',
Properties: {
AssumeRolePolicyDocument: {
Statement: [
{
Action: ['sts:AssumeRole'],
Effect: 'Allow',
Principal: {
Service: ['lambda.amazonaws.com'],
},
},
],
Version: '2012-10-17',
},
Policies: [
{
PolicyDocument: {
Statement: [
{
Effect: 'Allow',
Resource: 'arn:aws:lambda:*:*:function:custom-resource-func',
Action: ['lambda:AddPermission', 'lambda:RemovePermission'],
},
],
Version: '2012-10-17',
},
PolicyName: {
'Fn::Join': ['-', ['dev', 'some-service', 'custom-resources-lambda']],
},
},
],
},
},
});
});
});
});
});
3 changes: 3 additions & 0 deletions lib/plugins/aws/customResources/resources/README.md
@@ -0,0 +1,3 @@
# Serverless Custom CloudFormation Resources

This directory contains the Lambda functions for the Serverless Custom CloudFormation Resources.
85 changes: 85 additions & 0 deletions lib/plugins/aws/customResources/resources/s3/handler.js
@@ -0,0 +1,85 @@
'use strict';

const { addPermission, removePermission } = require('./lib/permissions');
const { updateConfiguration, removeConfiguration } = require('./lib/bucket');
const { response, getEnvironment, getLambdaArn } = require('../utils');

function handler(event, context) {
const PhysicalResourceId = 'CustomResouceExistingS3';
event = Object.assign({}, event, { PhysicalResourceId });

if (event.RequestType === 'Create') {
return create(event, context);
} else if (event.RequestType === 'Update') {
return update(event, context);
} else if (event.RequestType === 'Delete') {
return remove(event, context);
}
const error = new Error(`Unhandled RequestType ${event.RequestType}`);
return response(event, context, 'FAILED', {}, error);
}

function create(event, context) {
const { Region, AccountId } = getEnvironment(context);
const { FunctionName, BucketName, BucketConfig } = event.ResourceProperties;

const lambdaArn = getLambdaArn(Region, AccountId, FunctionName);

return addPermission({
functionName: FunctionName,
bucketName: BucketName,
region: Region,
})
.then(() =>
updateConfiguration({
lambdaArn,
region: Region,
functionName: FunctionName,
bucketName: BucketName,
bucketConfig: BucketConfig,
})
)
.then(() => response(event, context, 'SUCCESS'))
.catch(error => response(event, context, 'FAILED', {}, error));
}

function update(event, context) {
const { Region, AccountId } = getEnvironment(context);
const { FunctionName, BucketName, BucketConfig } = event.ResourceProperties;

const lambdaArn = getLambdaArn(Region, AccountId, FunctionName);

return updateConfiguration({
lambdaArn,
region: Region,
functionName: FunctionName,
bucketName: BucketName,
bucketConfig: BucketConfig,
})
.then(() => response(event, context, 'SUCCESS'))
.catch(error => response(event, context, 'FAILED', {}, error));
}

function remove(event, context) {
const { Region } = getEnvironment(context);
const { FunctionName, BucketName } = event.ResourceProperties;

return removePermission({
functionName: FunctionName,
bucketName: BucketName,
region: Region,
})
.then(() =>
removeConfiguration({
region: Region,
functionName: FunctionName,
bucketName: BucketName,
})
)
.then(() => response(event, context, 'SUCCESS'))
.catch(error => response(event, context, 'FAILED', {}, error));
}

module.exports = {
handler,
};

0 comments on commit d05d7a1

Please sign in to comment.