Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for S3 hosted package artifacts #6196

Merged
merged 1 commit into from
Jun 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 32 additions & 3 deletions docs/providers/aws/guide/packaging.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -90,17 +90,18 @@ Serverless won't zip your service if this is configured and therefore `exclude`


The artifact option is especially useful in case your development environment allows you to generate a deployable artifact like Maven does for Java. The artifact option is especially useful in case your development environment allows you to generate a deployable artifact like Maven does for Java.


### Example #### Service package


```yml ```yml
service: my-service service: my-service
package: package:
artifact: path/to/my-artifact.zip artifact: path/to/my-artifact.zip
``` ```


You can also use this to package functions individually. #### Individual function packages

You can also use this to package functions individually:


### Example
```yml ```yml
service: my-service service: my-service


Expand All @@ -118,6 +119,34 @@ functions:
method: get method: get
``` ```


#### Artifacst hosted on S3

Artifacts can also be fetched from a remote S3 bucket. In this case you just need to provide the S3 object URL as the artifact value. This applies to both, service-wide and function-level artifact setups.

##### Service package

```yml
service: my-service

package:
artifact: https://s3.amazonaws.com/some-bucket/service-artifact.zip
```

##### Individual function packages

```yml
service: my-service

package:
individually: true

functions:
hello:
handler: com.serverless.Handler
package:
artifact: https://s3.amazonaws.com/some-bucket/function-artifact.zip
```

### Packaging functions separately ### Packaging functions separately


If you want even more controls over your functions for deployment you can configure them to be packaged independently. This allows you more control for optimizing your deployment. To enable individual packaging set `individually` to true in the service or function wide packaging settings. If you want even more controls over your functions for deployment you can configure them to be packaged independently. This allows you more control for optimizing your deployment. To enable individual packaging set `individually` to true in the service or function wide packaging settings.
Expand Down
9 changes: 9 additions & 0 deletions lib/classes/Utils.js
Original file line number Original file line Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict'; 'use strict';


const os = require('os');
const crypto = require('crypto');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const ci = require('ci-info'); const ci = require('ci-info');
Expand Down Expand Up @@ -31,6 +33,13 @@ class Utils {
return dirExistsSync(dirPath); return dirExistsSync(dirPath);
} }


getTmpDirPath() {
const dirPath = path.join(os.tmpdir(),
'tmpdirs-serverless', crypto.randomBytes(8).toString('hex'));
fse.ensureDirSync(dirPath);
return dirPath;
}

fileExistsSync(filePath) { fileExistsSync(filePath) {
return fileExistsSync(filePath); return fileExistsSync(filePath);
} }
Expand Down
9 changes: 9 additions & 0 deletions lib/classes/Utils.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ describe('Utils', () => {
utils = new Utils(serverless); utils = new Utils(serverless);
}); });


describe('#getTmpDirPath()', () => {
it('should create a scoped tmp directory', () => {
const dirPath = serverless.utils.getTmpDirPath();
const stats = fse.statSync(dirPath);
expect(dirPath).to.include('tmpdirs-serverless');
expect(stats.isDirectory()).to.equal(true);
});
});

describe('#dirExistsSync()', () => { describe('#dirExistsSync()', () => {
describe('When reading a directory', () => { describe('When reading a directory', () => {
it('should detect if a directory exists', () => { it('should detect if a directory exists', () => {
Expand Down
53 changes: 51 additions & 2 deletions lib/plugins/aws/package/compile/functions/index.js
Original file line number Original file line Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'; 'use strict';


const AWS = require('aws-sdk');
const BbPromise = require('bluebird'); const BbPromise = require('bluebird');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs'); const fs = require('fs');
Expand All @@ -23,6 +24,7 @@ class AwsCompileFunctions {


this.hooks = { this.hooks = {
'package:compileFunctions': () => BbPromise.bind(this) 'package:compileFunctions': () => BbPromise.bind(this)
.then(this.downloadPackageArtifacts)
.then(this.compileFunctions), .then(this.compileFunctions),
}; };
} }
Expand Down Expand Up @@ -66,6 +68,46 @@ class AwsCompileFunctions {
} }
} }


downloadPackageArtifact(functionName) {
const { region } = this.options;
const S3 = new AWS.S3({ region });

const functionObject = this.serverless.service.getFunction(functionName);
const artifactFilePath = _.get(functionObject, 'package.artifact') ||
_.get(this, 'serverless.service.package.artifact');

const regex = new RegExp('.*s3.amazonaws.com/(.+)/(.+)');
pmuens marked this conversation as resolved.
Show resolved Hide resolved
const match = artifactFilePath.match(regex);

if (match) {
return new BbPromise((resolve, reject) => {
const tmpDir = this.serverless.utils.getTmpDirPath();
const filePath = path.join(tmpDir, match[2]);

const readStream = S3.getObject({
Bucket: match[1],
Key: match[2],
}).createReadStream();

const writeStream = fs.createWriteStream(filePath);

readStream.on('error', (error) => reject(error));
readStream.pipe(writeStream)
.on('error', reject)
.on('close', () => {
if (functionObject.package.artifact) {
functionObject.package.artifact = filePath;
} else {
this.serverless.service.package.artifact = filePath;
}
return resolve(filePath);
});
});
}

return BbPromise.resolve();
}

compileFunction(functionName) { compileFunction(functionName) {
const newFunction = this.cfLambdaFunctionTemplate(); const newFunction = this.cfLambdaFunctionTemplate();
const functionObject = this.serverless.service.getFunction(functionName); const functionObject = this.serverless.service.getFunction(functionName);
Expand All @@ -76,6 +118,7 @@ class AwsCompileFunctions {


let artifactFilePath = functionObject.package.artifact || let artifactFilePath = functionObject.package.artifact ||
this.serverless.service.package.artifact; this.serverless.service.package.artifact;

if (!artifactFilePath || if (!artifactFilePath ||
(this.serverless.service.artifact && !functionObject.package.artifact)) { (this.serverless.service.artifact && !functionObject.package.artifact)) {
let artifactFileName = serviceArtifactFileName; let artifactFileName = serviceArtifactFileName;
Expand Down Expand Up @@ -452,12 +495,18 @@ class AwsCompileFunctions {
}); });
} }


downloadPackageArtifacts() {
const allFunctions = this.serverless.service.getAllFunctions();
return BbPromise.each(
allFunctions,
functionName => this.downloadPackageArtifact(functionName));
}

compileFunctions() { compileFunctions() {
const allFunctions = this.serverless.service.getAllFunctions(); const allFunctions = this.serverless.service.getAllFunctions();
return BbPromise.each( return BbPromise.each(
allFunctions, allFunctions,
functionName => this.compileFunction(functionName) functionName => this.compileFunction(functionName));
);
} }


// helper functions // helper functions
Expand Down
64 changes: 62 additions & 2 deletions lib/plugins/aws/package/compile/functions/index.test.js
Original file line number Original file line Diff line number Diff line change
@@ -1,19 +1,24 @@
'use strict'; 'use strict';


const AWS = require('aws-sdk');
const fs = require('fs');
const _ = require('lodash'); const _ = require('lodash');
const path = require('path'); const path = require('path');
const chai = require('chai'); const chai = require('chai');
const sinon = require('sinon');
const AwsProvider = require('../../../provider/awsProvider'); const AwsProvider = require('../../../provider/awsProvider');
const AwsCompileFunctions = require('./index'); const AwsCompileFunctions = require('./index');
const Serverless = require('../../../../../Serverless'); const Serverless = require('../../../../../Serverless');
const { getTmpDirPath } = require('../../../../../../tests/utils/fs'); const { getTmpDirPath, createTmpFile } = require('../../../../../../tests/utils/fs');


chai.use(require('chai-as-promised')); chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));


const expect = chai.expect; const expect = chai.expect;


describe('AwsCompileFunctions', () => { describe('AwsCompileFunctions', () => {
let serverless; let serverless;
let awsProvider;
let awsCompileFunctions; let awsCompileFunctions;
const functionName = 'test'; const functionName = 'test';
const compiledFunctionName = 'TestLambdaFunction'; const compiledFunctionName = 'TestLambdaFunction';
Expand All @@ -24,7 +29,8 @@ describe('AwsCompileFunctions', () => {
region: 'us-east-1', region: 'us-east-1',
}; };
serverless = new Serverless(options); serverless = new Serverless(options);
serverless.setProvider('aws', new AwsProvider(serverless, options)); awsProvider = new AwsProvider(serverless, options);
serverless.setProvider('aws', awsProvider);
serverless.cli = new serverless.classes.CLI(); serverless.cli = new serverless.classes.CLI();
awsCompileFunctions = new AwsCompileFunctions(serverless, options); awsCompileFunctions = new AwsCompileFunctions(serverless, options);
awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = { awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = {
Expand Down Expand Up @@ -74,6 +80,60 @@ describe('AwsCompileFunctions', () => {
expect(awsCompileFunctions.isArnRefGetAttOrImportValue({ Blah: 'vtha' })).to.equal(false)); expect(awsCompileFunctions.isArnRefGetAttOrImportValue({ Blah: 'vtha' })).to.equal(false));
}); });


describe('#downloadPackageArtifacts()', () => {
let requestStub;
let testFilePath;
const s3BucketName = 'test-bucket';
const s3ArtifactName = 's3-hosted-artifact.zip';

beforeEach(() => {
testFilePath = createTmpFile('dummy-artifact');
requestStub = sinon.stub(AWS, 'S3').returns({
getObject: () => ({
createReadStream() {
return fs.createReadStream(testFilePath);
},
}),
});
});

afterEach(() => {
AWS.S3.restore();
});

it('should download the file and replace the artifact path for function packages', () => {
awsCompileFunctions.serverless.service.package.individually = true;
awsCompileFunctions.serverless.service.functions[functionName]
.package.artifact = `https://s3.amazonaws.com/${s3BucketName}/${s3ArtifactName}`;

return expect(awsCompileFunctions.downloadPackageArtifacts()).to.be.fulfilled
.then(() => {
const artifactFileName = awsCompileFunctions.serverless.service
.functions[functionName].package.artifact.split(path.sep).pop();

expect(requestStub.callCount).to.equal(1);
expect(artifactFileName).to.equal(s3ArtifactName);
});
});

it('should download the file and replace the artifact path for service-wide packages', () => {
awsCompileFunctions.serverless.service.package.individually = false;
awsCompileFunctions.serverless.service.functions[functionName]
.package.artifact = false;
awsCompileFunctions.serverless.service.package
.artifact = `https://s3.amazonaws.com/${s3BucketName}/${s3ArtifactName}`;

return expect(awsCompileFunctions.downloadPackageArtifacts()).to.be.fulfilled
.then(() => {
const artifactFileName = awsCompileFunctions.serverless.service.package.artifact
.split(path.sep).pop();

expect(requestStub.callCount).to.equal(1);
expect(artifactFileName).to.equal(s3ArtifactName);
});
});
});

describe('#compileFunctions()', () => { describe('#compileFunctions()', () => {
it('should use service artifact if not individually', () => { it('should use service artifact if not individually', () => {
awsCompileFunctions.serverless.service.package.individually = false; awsCompileFunctions.serverless.service.package.individually = false;
Expand Down
8 changes: 8 additions & 0 deletions tests/utils/fs/index.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const fse = require('fs-extra');
const crypto = require('crypto'); const crypto = require('crypto');
const YAML = require('js-yaml'); const YAML = require('js-yaml');
const JSZip = require('jszip'); const JSZip = require('jszip');
Expand All @@ -18,6 +19,12 @@ function getTmpFilePath(fileName) {
return path.join(getTmpDirPath(), fileName); return path.join(getTmpDirPath(), fileName);
} }


function createTmpFile(name) {
const filePath = getTmpFilePath(name);
fse.ensureFileSync(filePath);
return filePath;
}

function replaceTextInFile(filePath, subString, newSubString) { function replaceTextInFile(filePath, subString, newSubString) {
const fileContent = fs.readFileSync(filePath).toString(); const fileContent = fs.readFileSync(filePath).toString();
fs.writeFileSync(filePath, fileContent.replace(subString, newSubString)); fs.writeFileSync(filePath, fileContent.replace(subString, newSubString));
Expand All @@ -43,6 +50,7 @@ module.exports = {
tmpDirCommonPath, tmpDirCommonPath,
getTmpDirPath, getTmpDirPath,
getTmpFilePath, getTmpFilePath,
createTmpFile,
replaceTextInFile, replaceTextInFile,
readYamlFile, readYamlFile,
writeYamlFile, writeYamlFile,
Expand Down