UNPKG

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