Skip to content

Commit 99e4e1a

Browse files
authored
Merge pull request #6196 from serverless/s3-package-artifacts
Add support for S3 hosted package artifacts
2 parents 3b77632 + a02b31e commit 99e4e1a

File tree

6 files changed

+171
-7
lines changed

6 files changed

+171
-7
lines changed

docs/providers/aws/guide/packaging.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,18 @@ Serverless won't zip your service if this is configured and therefore `exclude`
9090
9191
The artifact option is especially useful in case your development environment allows you to generate a deployable artifact like Maven does for Java.
9292
93-
### Example
93+
#### Service package
9494
9595
```yml
9696
service: my-service
9797
package:
9898
artifact: path/to/my-artifact.zip
9999
```
100100

101-
You can also use this to package functions individually.
101+
#### Individual function packages
102+
103+
You can also use this to package functions individually:
102104

103-
### Example
104105
```yml
105106
service: my-service
106107

@@ -118,6 +119,34 @@ functions:
118119
method: get
119120
```
120121
122+
#### Artifacst hosted on S3
123+
124+
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.
125+
126+
##### Service package
127+
128+
```yml
129+
service: my-service
130+
131+
package:
132+
artifact: https://s3.amazonaws.com/some-bucket/service-artifact.zip
133+
```
134+
135+
##### Individual function packages
136+
137+
```yml
138+
service: my-service
139+
140+
package:
141+
individually: true
142+
143+
functions:
144+
hello:
145+
handler: com.serverless.Handler
146+
package:
147+
artifact: https://s3.amazonaws.com/some-bucket/function-artifact.zip
148+
```
149+
121150
### Packaging functions separately
122151
123152
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.

lib/classes/Utils.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
const os = require('os');
4+
const crypto = require('crypto');
35
const fs = require('fs');
46
const path = require('path');
57
const ci = require('ci-info');
@@ -31,6 +33,13 @@ class Utils {
3133
return dirExistsSync(dirPath);
3234
}
3335

36+
getTmpDirPath() {
37+
const dirPath = path.join(os.tmpdir(),
38+
'tmpdirs-serverless', crypto.randomBytes(8).toString('hex'));
39+
fse.ensureDirSync(dirPath);
40+
return dirPath;
41+
}
42+
3443
fileExistsSync(filePath) {
3544
return fileExistsSync(filePath);
3645
}

lib/classes/Utils.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ describe('Utils', () => {
2525
utils = new Utils(serverless);
2626
});
2727

28+
describe('#getTmpDirPath()', () => {
29+
it('should create a scoped tmp directory', () => {
30+
const dirPath = serverless.utils.getTmpDirPath();
31+
const stats = fse.statSync(dirPath);
32+
expect(dirPath).to.include('tmpdirs-serverless');
33+
expect(stats.isDirectory()).to.equal(true);
34+
});
35+
});
36+
2837
describe('#dirExistsSync()', () => {
2938
describe('When reading a directory', () => {
3039
it('should detect if a directory exists', () => {

lib/plugins/aws/package/compile/functions/index.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
const AWS = require('aws-sdk');
34
const BbPromise = require('bluebird');
45
const crypto = require('crypto');
56
const fs = require('fs');
@@ -23,6 +24,7 @@ class AwsCompileFunctions {
2324

2425
this.hooks = {
2526
'package:compileFunctions': () => BbPromise.bind(this)
27+
.then(this.downloadPackageArtifacts)
2628
.then(this.compileFunctions),
2729
};
2830
}
@@ -66,6 +68,46 @@ class AwsCompileFunctions {
6668
}
6769
}
6870

71+
downloadPackageArtifact(functionName) {
72+
const { region } = this.options;
73+
const S3 = new AWS.S3({ region });
74+
75+
const functionObject = this.serverless.service.getFunction(functionName);
76+
const artifactFilePath = _.get(functionObject, 'package.artifact') ||
77+
_.get(this, 'serverless.service.package.artifact');
78+
79+
const regex = new RegExp('.*s3.amazonaws.com/(.+)/(.+)');
80+
const match = artifactFilePath.match(regex);
81+
82+
if (match) {
83+
return new BbPromise((resolve, reject) => {
84+
const tmpDir = this.serverless.utils.getTmpDirPath();
85+
const filePath = path.join(tmpDir, match[2]);
86+
87+
const readStream = S3.getObject({
88+
Bucket: match[1],
89+
Key: match[2],
90+
}).createReadStream();
91+
92+
const writeStream = fs.createWriteStream(filePath);
93+
94+
readStream.on('error', (error) => reject(error));
95+
readStream.pipe(writeStream)
96+
.on('error', reject)
97+
.on('close', () => {
98+
if (functionObject.package.artifact) {
99+
functionObject.package.artifact = filePath;
100+
} else {
101+
this.serverless.service.package.artifact = filePath;
102+
}
103+
return resolve(filePath);
104+
});
105+
});
106+
}
107+
108+
return BbPromise.resolve();
109+
}
110+
69111
compileFunction(functionName) {
70112
const newFunction = this.cfLambdaFunctionTemplate();
71113
const functionObject = this.serverless.service.getFunction(functionName);
@@ -76,6 +118,7 @@ class AwsCompileFunctions {
76118

77119
let artifactFilePath = functionObject.package.artifact ||
78120
this.serverless.service.package.artifact;
121+
79122
if (!artifactFilePath ||
80123
(this.serverless.service.artifact && !functionObject.package.artifact)) {
81124
let artifactFileName = serviceArtifactFileName;
@@ -452,12 +495,18 @@ class AwsCompileFunctions {
452495
});
453496
}
454497

498+
downloadPackageArtifacts() {
499+
const allFunctions = this.serverless.service.getAllFunctions();
500+
return BbPromise.each(
501+
allFunctions,
502+
functionName => this.downloadPackageArtifact(functionName));
503+
}
504+
455505
compileFunctions() {
456506
const allFunctions = this.serverless.service.getAllFunctions();
457507
return BbPromise.each(
458508
allFunctions,
459-
functionName => this.compileFunction(functionName)
460-
);
509+
functionName => this.compileFunction(functionName));
461510
}
462511

463512
// helper functions

lib/plugins/aws/package/compile/functions/index.test.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
'use strict';
22

3+
const AWS = require('aws-sdk');
4+
const fs = require('fs');
35
const _ = require('lodash');
46
const path = require('path');
57
const chai = require('chai');
8+
const sinon = require('sinon');
69
const AwsProvider = require('../../../provider/awsProvider');
710
const AwsCompileFunctions = require('./index');
811
const Serverless = require('../../../../../Serverless');
9-
const { getTmpDirPath } = require('../../../../../../tests/utils/fs');
12+
const { getTmpDirPath, createTmpFile } = require('../../../../../../tests/utils/fs');
1013

1114
chai.use(require('chai-as-promised'));
15+
chai.use(require('sinon-chai'));
1216

1317
const expect = chai.expect;
1418

1519
describe('AwsCompileFunctions', () => {
1620
let serverless;
21+
let awsProvider;
1722
let awsCompileFunctions;
1823
const functionName = 'test';
1924
const compiledFunctionName = 'TestLambdaFunction';
@@ -24,7 +29,8 @@ describe('AwsCompileFunctions', () => {
2429
region: 'us-east-1',
2530
};
2631
serverless = new Serverless(options);
27-
serverless.setProvider('aws', new AwsProvider(serverless, options));
32+
awsProvider = new AwsProvider(serverless, options);
33+
serverless.setProvider('aws', awsProvider);
2834
serverless.cli = new serverless.classes.CLI();
2935
awsCompileFunctions = new AwsCompileFunctions(serverless, options);
3036
awsCompileFunctions.serverless.service.provider.compiledCloudFormationTemplate = {
@@ -74,6 +80,60 @@ describe('AwsCompileFunctions', () => {
7480
expect(awsCompileFunctions.isArnRefGetAttOrImportValue({ Blah: 'vtha' })).to.equal(false));
7581
});
7682

83+
describe('#downloadPackageArtifacts()', () => {
84+
let requestStub;
85+
let testFilePath;
86+
const s3BucketName = 'test-bucket';
87+
const s3ArtifactName = 's3-hosted-artifact.zip';
88+
89+
beforeEach(() => {
90+
testFilePath = createTmpFile('dummy-artifact');
91+
requestStub = sinon.stub(AWS, 'S3').returns({
92+
getObject: () => ({
93+
createReadStream() {
94+
return fs.createReadStream(testFilePath);
95+
},
96+
}),
97+
});
98+
});
99+
100+
afterEach(() => {
101+
AWS.S3.restore();
102+
});
103+
104+
it('should download the file and replace the artifact path for function packages', () => {
105+
awsCompileFunctions.serverless.service.package.individually = true;
106+
awsCompileFunctions.serverless.service.functions[functionName]
107+
.package.artifact = `https://s3.amazonaws.com/${s3BucketName}/${s3ArtifactName}`;
108+
109+
return expect(awsCompileFunctions.downloadPackageArtifacts()).to.be.fulfilled
110+
.then(() => {
111+
const artifactFileName = awsCompileFunctions.serverless.service
112+
.functions[functionName].package.artifact.split(path.sep).pop();
113+
114+
expect(requestStub.callCount).to.equal(1);
115+
expect(artifactFileName).to.equal(s3ArtifactName);
116+
});
117+
});
118+
119+
it('should download the file and replace the artifact path for service-wide packages', () => {
120+
awsCompileFunctions.serverless.service.package.individually = false;
121+
awsCompileFunctions.serverless.service.functions[functionName]
122+
.package.artifact = false;
123+
awsCompileFunctions.serverless.service.package
124+
.artifact = `https://s3.amazonaws.com/${s3BucketName}/${s3ArtifactName}`;
125+
126+
return expect(awsCompileFunctions.downloadPackageArtifacts()).to.be.fulfilled
127+
.then(() => {
128+
const artifactFileName = awsCompileFunctions.serverless.service.package.artifact
129+
.split(path.sep).pop();
130+
131+
expect(requestStub.callCount).to.equal(1);
132+
expect(artifactFileName).to.equal(s3ArtifactName);
133+
});
134+
});
135+
});
136+
77137
describe('#compileFunctions()', () => {
78138
it('should use service artifact if not individually', () => {
79139
awsCompileFunctions.serverless.service.package.individually = false;

tests/utils/fs/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const os = require('os');
44
const path = require('path');
55
const fs = require('fs');
6+
const fse = require('fs-extra');
67
const crypto = require('crypto');
78
const YAML = require('js-yaml');
89
const JSZip = require('jszip');
@@ -18,6 +19,12 @@ function getTmpFilePath(fileName) {
1819
return path.join(getTmpDirPath(), fileName);
1920
}
2021

22+
function createTmpFile(name) {
23+
const filePath = getTmpFilePath(name);
24+
fse.ensureFileSync(filePath);
25+
return filePath;
26+
}
27+
2128
function replaceTextInFile(filePath, subString, newSubString) {
2229
const fileContent = fs.readFileSync(filePath).toString();
2330
fs.writeFileSync(filePath, fileContent.replace(subString, newSubString));
@@ -43,6 +50,7 @@ module.exports = {
4350
tmpDirCommonPath,
4451
getTmpDirPath,
4552
getTmpFilePath,
53+
createTmpFile,
4654
replaceTextInFile,
4755
readYamlFile,
4856
writeYamlFile,

0 commit comments

Comments
 (0)