UNPKG

8.99 kBJavaScriptView Raw
1'use strict';
2
3const AWS = require('aws-sdk');
4const get = require('lodash.get');
5const fs = require('fs-extra');
6const path = require('path');
7const utils = require('./utils');
8
9/**
10 * Copy, zip and upload lambda functions to S3
11 *
12 * @param {Object} config the configuration object
13 * @param {String} kesFolder the path to the `.kes` folder
14 * @param {String} bucket the S3 bucket name
15 * @param {String} key the main folder to store the data in the bucket (stack)
16 */
17class Lambda {
18 constructor(config) {
19 this.config = config;
20 this.cf_basename = path.basename(config.cfFile, '.template.yml');
21 this.kesFolder = config.kesFolder;
22 this.distFolder = path.join(this.kesFolder, 'dist');
23 this.buildFolder = path.join(this.kesFolder, 'build', this.cf_basename);
24 this.bucket = get(config, 'bucket');
25 this.key = path.join(this.config.stack, 'lambdas');
26 this.grouped = {};
27 }
28
29 /**
30 * Adds hash value, bucket name, and remote and local paths
31 * for lambdas that have source value.
32 *
33 * If a s3Source is usaed, only add remote and bucket values
34 * @param {object} lambda the lambda object
35 * @return {object} the lambda object
36 */
37 buildS3Path(lambda) {
38 if (lambda.source) {
39 // get hash
40 lambda.hash = this.getHash(lambda.source).toString();
41 lambda.bucket = this.bucket;
42
43 if (!this.grouped.hasOwnProperty(lambda.hash)) {
44 this.grouped[lambda.hash] = lambda.name;
45 }
46
47 // local zip
48 const lambdaName = this.grouped[lambda.hash];
49 const zipFile = `${lambda.hash}-${lambdaName}.zip`;
50 lambda.local = path.join(this.buildFolder, zipFile);
51
52 // remote address
53 lambda.remote = path.join(this.key, zipFile);
54 }
55 else if (lambda.s3Source) {
56 lambda.remote = lambda.s3Source.key;
57 lambda.bucket = lambda.s3Source.bucket;
58 }
59 return lambda;
60 }
61
62 /**
63 * calculate the hash value for a given path
64 * @param {string} folderName directory path
65 * @param {string} method hash type, default to shasum
66 * @return {buffer} hash value
67 */
68 getHash(folderName, method) {
69 if (!method) {
70 method = 'shasum';
71 }
72
73 const alternativeMethod = 'sha1sum';
74 let hash = utils.exec(`find ${folderName} -type f | \
75 xargs ${method} | ${method} | awk '{print $1}' ${''}`, false);
76
77 hash = hash.toString().replace(/\n/, '');
78
79 if (hash.length === 0) {
80 if (method === alternativeMethod) {
81 throw new Error('You must either have shasum or sha1sum');
82 }
83 console.log(`switching to ${alternativeMethod}`);
84 return this.getHash(folderName, alternativeMethod);
85 }
86
87 return hash;
88 }
89
90 /**
91 * zip a given lambda function source code
92 *
93 * @param {Object} lambda the lambda object
94 * @returns {Promise} returns the promise of the lambda object
95 */
96 zipLambda(lambda) {
97 console.log(`Zipping ${lambda.local}`);
98
99 // skip if the file with the same hash is zipped
100 if (fs.existsSync(lambda.local)) {
101 const stats = fs.statSync(lambda.local);
102 if (stats.isFile() && stats.size > 0) {
103 return Promise.resolve(lambda);
104 }
105 }
106
107 return utils.zip(lambda.local, [lambda.source]).then(() => {
108 console.log(`Zipped ${lambda.local}`);
109 return lambda;
110 });
111 }
112
113 /**
114 * Uploads the zipped lambda code to a given s3 bucket
115 * if the zip file already exists on S3 it skips the upload
116 *
117 * @param {Object} lambda the lambda object. It must have the following properties
118 * @param {String} lambda.bucket the s3 buckt name
119 * @param {String} lambda.remote the lambda code's key (path and filename) on s3
120 * @param {String} lambda.local the zip files location on local machine
121 * @returns {Promise} returns the promise of updated lambda object
122 */
123 uploadLambda(lambda) {
124 const s3 = new AWS.S3();
125
126 const params = {
127 Bucket: this.bucket,
128 Key: lambda.remote,
129 Body: fs.readFileSync(lambda.local)
130 };
131
132 return new Promise((resolve, reject) => {
133 // check if it is already uploaded
134 s3.headObject({
135 Bucket: this.bucket,
136 Key: lambda.remote
137 }).promise().then((data) => {
138 if (data.ContentLength !== params.Body.byteLength) {
139 throw new Error('File sizes don\'t match');
140 }
141 console.log(`Already Uploaded: s3://${this.bucket}/${lambda.remote}`);
142 return resolve(lambda);
143 }).catch(() => {
144 s3.upload(params, (e, r) => {
145 if (e) return reject(e);
146 console.log(`Uploaded: s3://${this.bucket}/${lambda.remote}`);
147 return resolve(lambda);
148 });
149 });
150 });
151 }
152
153 /**
154 * Zips and Uploads a lambda function. If the source of the function
155 * is already zipped and uploaded, it skips the step only updates the
156 * lambda config object.
157 *
158 * @param {Object} lambda the lambda object.
159 * @returns {Promise} returns the promise of updated lambda object
160 */
161 zipAndUploadLambda(lambda) {
162 // only zip lambda if it's not zipped
163 const match = lambda.source.match(/.\.zip/);
164 if (match) {
165 lambda.local = lambda.source;
166 return this.uploadLambda(lambda);
167 }
168 return this.zipLambda(lambda)
169 .then(l => this.uploadLambda(l));
170 }
171
172 /**
173 * Zips and Uploads lambda functions in the configuration object.
174 * If the source of the function
175 * is already zipped and uploaded, it skips the step only updates the
176 * lambda config object.
177 *
178 * If the lambda config includes a link to zip file on S3, it skips
179 * the whole step.
180 *
181 * @returns {Promise} returns the promise of updated configuration object
182 */
183 process() {
184 if (this.config.lambdas) {
185 // create the lambda folder
186 fs.mkdirpSync(this.buildFolder);
187
188 let lambdas = this.config.lambdas;
189
190 // if the lambdas is not an array but a object, convert it to a list
191 if (!Array.isArray(this.config.lambdas)) {
192 lambdas = Object.keys(this.config.lambdas).map(name => {
193 const lambda = this.config.lambdas[name];
194 lambda.name = name;
195 return lambda;
196 });
197 }
198
199 // install npm packages
200 lambdas.filter(l => l.npmSource).forEach(l => utils.exec(`npm install ${l.npmSource.name}@${l.npmSource.version}`));
201
202 // build lambda path for lambdas that are zipped and uploaded
203 lambdas = lambdas.map(l => this.buildS3Path(l));
204
205 // zip and upload only unique hashes
206 let uniqueHashes = {};
207 lambdas.filter(l => l.source).forEach(l => {
208 uniqueHashes[l.hash] = l;
209 });
210 const jobs = Object.keys(uniqueHashes).map(l => this.zipAndUploadLambda(uniqueHashes[l]));
211
212 return Promise.all(jobs)
213 .then(() => {
214 // we handle lambdas as both arrays and key/objects
215 // below condition is intended to for cases where
216 // the lambda is returned as a list
217 if (Array.isArray(this.config.lambdas)) {
218 this.config.lambdas = lambdas;
219 return this.config;
220 }
221 const tmp = {};
222 lambdas.forEach(l => (tmp[l.name] = l));
223 this.config.lambdas = tmp;
224 return this.config;
225 })
226 .catch((e) => {
227 // if the zip operation stops for any of the lambdas because the source
228 // file is missing, zip files with the size of 0 are created. Removing
229 // the build folder ensures that we start fresh in the next round of zipping
230 if (e.message.includes('ENOENT')) {
231 fs.removeSync(this.buildFolder);
232 }
233 throw e;
234 });
235 }
236 else return Promise.resolve(this.config);
237 }
238
239 /**
240 * Uploads the zip code of a single lambda function to AWS Lambda
241 *
242 * @param {String} name name of the lambda function
243 * @returns {Promise} returns AWS response for lambda code update operation
244 */
245 updateSingleLambda(name) {
246 const l = new AWS.Lambda();
247
248 // create the lambda folder if it doesn't already exist
249 fs.mkdirpSync(this.buildFolder);
250
251 let lambda;
252 Object.keys(this.config.lambdas).forEach(n => {
253 if (n === name) {
254 lambda = this.config.lambdas[n];
255 }
256 });
257
258 if (!lambda) {
259 throw new Error('Lambda function is not defined in config.yml');
260 }
261 const stack = this.config.stackName;
262 lambda = this.buildS3Path(lambda);
263
264 console.log(`Updating ${name}`);
265 let promise;
266 const match = lambda.source.match(/.\.zip/);
267 if (match) {
268 lambda.local = lambda.source;
269 promise = l.updateFunctionCode({
270 FunctionName: `${stack}-${name}`,
271 ZipFile: fs.readFileSync(lambda.source)
272 }).promise();
273 }
274 else {
275 promise = this.zipLambda(lambda).then(lambda => l.updateFunctionCode({
276 FunctionName: `${stack}-${name}`,
277 ZipFile: fs.readFileSync(lambda.local)
278 }).promise());
279 }
280 return promise.then((r) => console.log(`Lambda function ${name} has been updated`));
281 }
282}
283
284module.exports = Lambda;