UNPKG

10.6 kBJavaScriptView Raw
1'use strict';
2
3const get = require('lodash.get');
4const moment = require('moment');
5const Handlebars = require('handlebars');
6const forge = require('node-forge');
7const AWS = require('aws-sdk');
8const path = require('path');
9const fs = require('fs-extra');
10const Lambda = require('./lambda');
11const utils = require('./utils');
12
13/**
14 * The main Kes class. This class is used in the command module to create
15 * the CLI interface for kes. This class can be extended in order to override
16 * and modify the behaviour of kes cli.
17 *
18 * @example
19 * const { Kes, Config } = require('kes');
20 *
21 * const options = { stack: 'myStack' };
22 * const config = new Config(options);
23 * const kes = new Kes(config);
24 *
25 * // create a new stack
26 * kes.deployStack()
27 * .then(() => describeCF())
28 * .then(() => updateSingleLambda('myLambda'))
29 * .catch(e => console.log(e));
30 *
31 * @param {Object} config an instance of the Config class (config.js)
32 */
33class Kes {
34 constructor(config) {
35 this.config = config;
36
37 this.stack = this.config.stack;
38 this.bucket = get(config, 'bucket');
39 this.templateUrl = `https://s3.amazonaws.com/${this.bucket}/${this.stack}/cloudformation.yml`;
40
41 utils.configureAws(this.config.region, this.config.profile, this.config.role);
42 this.s3 = new AWS.S3();
43 this.cf = new AWS.CloudFormation();
44 this.AWS = AWS;
45 this.Lambda = Lambda;
46 this.startTime = moment();
47 }
48
49 /**
50 * Updates code of a deployed lambda function
51 *
52 * @param {String} name the name of the lambda function defined in config.yml
53 * @return {Promise} returns the promise of an AWS response object
54 */
55 updateSingleLambda(name) {
56 const lambda = new this.Lambda(this.config);
57 return lambda.updateSingleLambda(name);
58 }
59
60 parseCF(cfFile) {
61 const t = fs.readFileSync(cfFile, 'utf8');
62
63 Handlebars.registerHelper('ToMd5', function(value) {
64 if (value) {
65 const md = forge.md.md5.create();
66 md.update(value);
67 return md.digest().toHex();
68 }
69 return value;
70 });
71
72 Handlebars.registerHelper('ToJson', function(value) {
73 return JSON.stringify(value);
74 });
75
76 const template = Handlebars.compile(t);
77 return template(this.config);
78 }
79
80 /**
81 * Compiles a CloudFormation template in Yaml format.
82 *
83 * Reads the configuration yaml from `.kes/config.yml`.
84 *
85 * Writes the template to `.kes/cloudformation.yml`.
86 *
87 * Uses `.kes/cloudformation.template.yml` as the base template
88 * for generating the final CF template.
89 *
90 * @return {Promise} returns the promise of an AWS response object
91 */
92 compileCF() {
93 const lambda = new this.Lambda(this.config);
94
95 return lambda.process().then((config) => {
96 this.config = config;
97 let cf;
98
99 // if there is a template parse CF there first
100 if (this.config.template) {
101 let mainCF = this.parseCF(this.config.template.cfFile);
102
103 // check if there is a CF over
104 try {
105 fs.lstatSync(this.config.cfFile);
106 let overrideCF = this.parseCF(this.config.cfFile);
107
108 // merge the the two
109 cf = utils.mergeYamls(mainCF, overrideCF);
110 }
111 catch (e) {
112 cf = mainCF;
113 }
114 }
115 else {
116 cf = this.parseCF(this.config.cfFile);
117 }
118
119 const destPath = path.join(this.config.kesFolder, 'cloudformation.yml');
120 console.log(`Template saved to ${destPath}`);
121 return fs.writeFileSync(destPath, cf);
122 });
123 }
124
125 /**
126 * This is just a wrapper around AWS s3.upload method.
127 * It uploads a given string to a S3 object.
128 *
129 * @param {String} bucket the s3 bucket name
130 * @param {String} key the path and name of the object
131 * @param {String} body the content of the object
132 * @returns {Promise} returns the promise of an AWS response object
133 */
134 uploadToS3(bucket, key, body) {
135 return this.s3.upload({ Bucket: bucket, Key: key, Body: body })
136 .promise()
137 .then(() => console.log(`Uploaded: s3://${bucket}/${key}`));
138 }
139
140 /**
141 * Uploads the Cloud Formation template to a given S3 location
142 *
143 * @returns {Promise} returns the promise of an AWS response object
144 */
145 uploadCF() {
146 // build the template first
147 return this.compileCF().then(() => {
148 // make sure cloudformation template exists
149 try {
150 fs.accessSync(path.join(this.config.kesFolder, 'cloudformation.yml'));
151 }
152 catch (e) {
153 throw new Error('cloudformation.yml is missing.');
154 }
155
156 // upload CF template to S3
157 if (this.bucket) {
158 return this.uploadToS3(
159 this.bucket,
160 `${this.stack}/cloudformation.yml`,
161 fs.readFileSync(path.join(this.config.kesFolder, 'cloudformation.yml'))
162 );
163 }
164 else {
165 console.log('Skipping CF template upload because internal bucket value is not provided.');
166 return true;
167 }
168 });
169 }
170
171 /**
172 * Calls CloudFormation's update-stack or create-stack methods
173 *
174 * @returns {Promise} returns the promise of an AWS response object
175 */
176 cloudFormation() {
177 const cfParams = [];
178 // add custom params from the config file if any
179 if (this.config.params) {
180 this.config.params.forEach((p) => {
181 cfParams.push({
182 ParameterKey: p.name,
183 ParameterValue: p.value,
184 UsePreviousValue: p.usePrevious || false
185 //NoEcho: p.noEcho || true
186 });
187 });
188 }
189
190 const capabilities = get(this.config, 'capabilities', []);
191
192 const params = {
193 StackName: this.stack,
194 Parameters: cfParams,
195 Capabilities: capabilities
196 };
197
198 if (this.config.tags) {
199 params.Tags = Object.keys(this.config.tags).map((key) => ({
200 Key: key,
201 Value: this.config.tags[key]
202 }));
203 }
204 else {
205 params.Tags = [];
206 }
207
208 if (this.bucket) {
209 params.TemplateURL = this.templateUrl;
210 }
211 else {
212 params.TemplateBody = fs.readFileSync(path.join(this.config.kesFolder, 'cloudformation.yml')).toString();
213 }
214
215 let wait = 'stackUpdateComplete';
216
217 // check if the stack exists
218 return this.cf.describeStacks({ StackName: this.stack }).promise()
219 .then(r => this.cf.updateStack(params).promise())
220 .catch(e => {
221 if (e.message.includes('does not exist')) {
222 wait = 'stackCreateComplete';
223 return this.cf.createStack(params).promise();
224 }
225 throw e;
226 })
227 .then(() => {
228 console.log('Waiting for the CF operation to complete');
229 return this.cf.waitFor(wait, { StackName: this.stack }).promise();
230 })
231 .then(r => console.log(`CF operation is in state of ${r.Stacks[0].StackStatus}`))
232 .catch((e) => {
233 const errorsWithDetail = [
234 'CREATE_FAILED',
235 'Resource is not in the state stackUpdateComplete',
236 'UPDATE_ROLLBACK_COMPLETE',
237 'ROLLBACK_COMPLETE',
238 'UPDATE_ROLLBACK_FAILED'
239 ];
240 const errorRequiresDetail = errorsWithDetail.filter(i => e.message.includes(i));
241
242 if (e.message === 'No updates are to be performed.') {
243 console.log(e.message);
244 return e.message;
245 }
246 else if (errorRequiresDetail.length > 0) {
247 console.log('There was an error deploying the CF stack');
248 console.log(e.message);
249
250 // get the error info here
251 return this.cf.describeStackEvents({ StackName: this.stack }).promise();
252 }
253 else {
254 console.log('There was an error deploying the CF stack');
255 throw e;
256 }
257 })
258 .then((r) => {
259 if (r && r.StackEvents) {
260 console.log('Here is the list of failures in chronological order:');
261 r.StackEvents.forEach((s) => {
262 if (s.ResourceStatus &&
263 s.ResourceStatus.includes('FAILED') &&
264 moment(s.Timestamp) > this.startTime) {
265 console.log(`${s.Timestamp} | ` +
266 `${s.ResourceStatus} | ` +
267 `${s.ResourceType} | ` +
268 `${s.LogicalResourceId} | ` +
269 `${s.ResourceStatusReason}`);
270 }
271 });
272 throw new Error('CloudFormation Deployment failed');
273 }
274 });
275 }
276
277 /**
278 * Validates the CF template
279 *
280 * @returns {Promise} returns the promise of an AWS response object
281 */
282 validateTemplate() {
283 console.log('Validating the template');
284 const url = `https://s3.amazonaws.com/${this.bucket}/${this.stack}/cloudformation.yml`;
285
286 const params = {};
287
288 if (this.bucket) {
289 // upload the template to the bucket first
290 params.TemplateURL = url;
291
292 return this.uploadCF()
293 .then(() => this.cf.validateTemplate(params).promise())
294 .then(() => console.log('Template is valid'));
295 }
296 else {
297 params.TemplateBody = fs.readFileSync(path.join(this.config.kesFolder, 'cloudformation.yml')).toString();
298 }
299
300 // Build and upload the CF template
301 return this.cf.validateTemplate(params)
302 .promise().then(() => console.log('Template is valid'));
303 }
304
305 /**
306 * Describes the cloudformation stack deployed
307 *
308 * @returns {Promise} returns the promise of an AWS response object
309 */
310 describeCF() {
311 const cf = new AWS.CloudFormation();
312
313 return cf.describeStacks({
314 StackName: this.stack
315 }).promise();
316 }
317
318 /**
319 * Generic create/update method for CloudFormation
320 *
321 * @returns {Promise} returns the promise of an AWS response object
322 */
323 opsStack() {
324 return this.uploadCF().then(() => this.cloudFormation());
325 }
326
327 /**
328 * [Deprecated] Creates a CloudFormation stack for the class instance
329 * If exists, will update the existing one
330 *
331 * @returns {Promise} returns the promise of an AWS response object
332 */
333 upsertStack() {
334 return this.opsStack();
335 }
336
337 /**
338 * Creates a CloudFormation stack for the class instance
339 * If exists, will update the existing one
340 *
341 * @returns {Promise} returns the promise of an AWS response object
342 */
343 deployStack() {
344 return this.opsStack();
345 }
346 /**
347 * [Deprecated] Creates a CloudFormation stack for the class instance
348 *
349 * @returns {Promise} returns the promise of an AWS response object
350 */
351 createStack() {
352 return this.opsStack();
353 }
354
355 /**
356 * [Deprecated] Updates an existing CloudFormation stack for the class instance
357 *
358 * @returns {Promise} returns the promise of an AWS response object
359 */
360 updateStack() {
361 return this.opsStack();
362 }
363}
364
365module.exports = Kes;