UNPKG

30.5 kBJavaScriptView Raw
1/**
2 * This module overrides the Kes Class and the Lambda class of Kes
3 * to support specific needs of the Cumulus Deployment.
4 *
5 * Specifically, this module changes the default Kes Deployment in the following ways:
6 *
7 * - Adds the ability to add Cumulus Configuration for each Step Function Task
8 * - @fixCumulusMessageSyntax
9 * - @extractCumulusConfigFromSF
10 * - Generates a public and private key to encrypt private information
11 * - @generateKeyPair
12 * - @uploadKeyPair
13 * - @crypto
14 * - Creates Cumulus Message Templates for each Step Function Workflow
15 * - @template
16 * - @generateTemplates
17 * - Adds Cumulus Message Adapter code to any Lambda Function that uses it
18 * - Uploads the public/private keys and the templates to S3
19 * - Restart Existing ECS tasks after each deployment
20 * - Redeploy API Gateway endpoints after Each Deployment
21 *
22 */
23
24'use strict';
25
26function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
27
28const cloneDeep = require('lodash.clonedeep');
29const zipObject = require('lodash.zipobject');
30const get = require('lodash.get');
31const { Kes, utils } = require('kes');
32const fs = require('fs-extra');
33const Handlebars = require('handlebars');
34const path = require('path');
35const util = require('util');
36const { sleep } = require('@cumulus/common/util');
37
38const Lambda = require('./lambda');
39const { validateConfig } = require('./configValidators');
40const { crypto } = require('./crypto');
41const { fetchMessageAdapter } = require('./adapter');
42const { extractCumulusConfigFromSF, generateTemplates } = require('./message');
43
44const fsWriteFile = util.promisify(fs.writeFile);
45
46/**
47 * A subclass of Kes class that overrides opsStack method.
48 * The subclass checks whether the public/private keys are generated
49 * and uploaded to the deployment bucket. If not, they are generated and
50 * uploaded.
51 *
52 * After the successful deployment of a CloudFormation template, the subclass
53 * generates and uploads payload and StepFunction templates and restarts ECS
54 * tasks if there is an active cluster with running tasks.
55 *
56 * @class UpdatedKes
57 */
58class UpdatedKes extends Kes {
59 /**
60 * Overrides the default constructor. It updates the default
61 * Lambda class and adds a git repository path for the cumulus
62 * message adapter
63 *
64 * @param {Object} config - kes config object
65 */
66 constructor(config) {
67 super(config);
68 this.Lambda = Lambda;
69 validateConfig(config);
70 this.messageAdapterGitPath = `${config.repo_owner}/${config.message_adapter_repo}`;
71 }
72
73 /**
74 * Redeploy the given api gateway (more info: https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-deploy-api.html)
75 *
76 * @param {string} name - the name of the api gateway deployment (used for logging)
77 * @param {string} restApiId - the api gateway id
78 * @param {string} stageName - the deployment stage name
79 * @returns {Promise.<boolean>} returns true if successful
80 */
81 redeployApiGateWay(name, restApiId, stageName) {
82 var _this = this;
83
84 return _asyncToGenerator(function* () {
85 const waitTime = 20;
86 if (restApiId) {
87 try {
88 const apigateway = new _this.AWS.APIGateway();
89 yield apigateway.createDeployment({ restApiId, stageName }).promise();
90 console.log(`${name} endpoints with the id ${restApiId} redeployed.`);
91 } catch (e) {
92 if (e.message && e.message.includes('Too Many Requests')) {
93 console.log(`Redeploying ${restApiId} was throttled. ` + `Another attempt will be made in ${waitTime} seconds`);
94 yield sleep(waitTime * 1000);
95 return _this.redeployApiGateWay(name, restApiId, stageName);
96 }
97 throw e;
98 }
99 }
100 return true;
101 })();
102 }
103
104 /**
105 * Restart all active tasks in the clusters of a deployed
106 * CloudFormation
107 *
108 * @param {Object} config - Kes Config object
109 * @returns {Promise} undefined
110 */
111 restartECSTasks(config) {
112 var _this2 = this;
113
114 return _asyncToGenerator(function* () {
115 const ecs = new _this2.AWS.ECS();
116
117 // only restart the tasks if the user has turned it on the config
118 if (config.ecs.restartTasksOnDeploy) {
119 try {
120 let resources = [];
121 const params = { StackName: config.stackName };
122 while (true) {
123 // eslint-disable-line no-constant-condition
124 // eslint-disable-next-line no-await-in-loop
125 const data = yield _this2.cf.listStackResources(params).promise();
126 resources = resources.concat(data.StackResourceSummaries);
127 if (data.NextToken) params.NextToken = data.NextToken;else break;
128 }
129
130 const clusters = resources.filter(function (resource) {
131 return resource.ResourceType === 'AWS::ECS::Cluster';
132 }).map(function (cluster) {
133 return cluster.PhysicalResourceId;
134 });
135
136 for (let clusterCtr = 0; clusterCtr < clusters.length; clusterCtr += 1) {
137 const cluster = clusters[clusterCtr];
138 // eslint-disable-next-line no-await-in-loop
139 const tasks = yield ecs.listTasks({ cluster }).promise();
140
141 for (let taskCtr = 0; taskCtr < tasks.taskArns.length; taskCtr += 1) {
142 const task = tasks.taskArns[taskCtr];
143 console.log(`restarting ECS task ${task}`);
144 // eslint-disable-next-line no-await-in-loop
145 yield ecs.stopTask({
146 task: task,
147 cluster
148 }).promise();
149 console.log(`ECS task ${task} restarted`);
150 }
151 }
152 } catch (err) {
153 console.log(err);
154 }
155 }
156 })();
157 }
158
159 /**
160 * build CloudWatch alarm widgets
161 *
162 * @param {string[]} alarmNames list of alarm names
163 * @param {Object} alarmTemplate widget template for alarm
164 * @returns {Object[]} list of alarm widgets
165 */
166 buildAlarmWidgets(alarmNames, alarmTemplate) {
167 return alarmNames.map(alarmName => {
168 const alarm = cloneDeep(alarmTemplate);
169 alarm.properties.title = alarmName;
170 alarm.properties.annotations.alarms[0] = alarm.properties.annotations.alarms[0].replace('alarmTemplate', alarmName);
171 return alarm;
172 });
173 }
174
175 /**
176 * Build list of buckets of desired type.
177 *
178 * @param {Object} buckets - config buckets
179 * @param {string} bucketType - selected type.
180 * @returns {string} - comma separated list of every bucket in buckets that matches bucketType.
181 */
182 collectBuckets(buckets, bucketType) {
183 const matchingBuckets = Object.values(buckets).filter(bucket => bucket.type === bucketType).map(object => object.name);
184 return new Handlebars.SafeString(matchingBuckets.toString());
185 }
186
187 /**
188 * build CloudWatch dashboard based on the dashboard configuration and other configurations
189 *
190 * @param {Object} dashboardConfig dashboard configuration for creating widgets
191 * @param {Object} ecs Elastic Container Service configuration including custom configuration
192 * for alarms
193 * @param {Object} es Elasticsearch configuration including configuration for alarms
194 * @param {string} stackName stack name
195 * @returns {string} returns dashboard body string
196 */
197 buildCWDashboard(dashboardConfig, ecs, es, stackName) {
198 const alarmTemplate = dashboardConfig.alarmTemplate;
199
200 // build ECS alarm widgets
201 const ecsAlarmNames = [];
202 if (ecs && ecs.services) {
203 Object.keys(ecs.services).forEach(serviceName => {
204 // default alarm
205 const defaultAlarmName = `${stackName}-${serviceName}-TaskCountLowAlarm`;
206 ecsAlarmNames.push(defaultAlarmName);
207 // custom alarm
208 if (ecs.services[serviceName].alarms) {
209 Object.keys(ecs.services[serviceName].alarms).forEach(alarmName => {
210 const name = `${stackName}-${serviceName}-${alarmName}Alarm`;
211 ecsAlarmNames.push(name);
212 });
213 }
214 });
215 }
216
217 const ecsAlarms = this.buildAlarmWidgets(ecsAlarmNames, alarmTemplate);
218
219 // build ES alarm widgets
220 let esWidgets = [];
221 if (es && es.alarms) {
222 const esAlarmNames = Object.keys(es.alarms).map(alarmName => `${stackName}-${es.name}-${alarmName}Alarm`);
223 const esAlarms = this.buildAlarmWidgets(esAlarmNames, alarmTemplate);
224 esWidgets = dashboardConfig.esHeader.concat(cloneDeep(dashboardConfig.alarmHeader), esAlarms, dashboardConfig.esWidgets);
225 }
226
227 // put all widgets together
228 let x = 0;
229 let y = 0;
230
231 const widgets = [];
232 const allWidgets = dashboardConfig.ecsHeader.concat(cloneDeep(dashboardConfig.alarmHeader), ecsAlarms, esWidgets);
233
234 let previousWgHeight = 0;
235 // place the widgets side by side until reach width 24
236 allWidgets.forEach(widget => {
237 if (x + widget.width > 24) {
238 x = 0;
239 y += previousWgHeight;
240 }
241 widgets.push(Object.assign(widget, { x, y }));
242 x += widget.width;
243 previousWgHeight = widget.height;
244 });
245
246 return JSON.stringify({ widgets });
247 }
248
249 /**
250 * Override CF parse to add Handlebars template helpers
251 *
252 * @param {string} cfFile - Filename
253 * @returns {string} - Contents of cfFile templated using Handlebars
254 */
255 parseCF(cfFile) {
256 // Parent kes deployed into packages/deployment contains
257 // Original registered helpers 'ifEquals', 'ifNotEquals', and 'ToJson'
258
259 // Arrow functions cannot be used when registering Handlebars helpers
260 // https://stackoverflow.com/questions/43932566/handlebars-block-expression-do-not-work
261
262 Handlebars.registerHelper('collectBuckets', (buckets, bucketType) => this.collectBuckets(buckets, bucketType));
263
264 Handlebars.registerHelper('buildCWDashboard', (dashboardConfig, ecs, es, stackName) => this.buildCWDashboard(dashboardConfig, ecs, es, stackName));
265
266 Handlebars.registerHelper('ifPrivateApi', (configs, api, options) => {
267 const privateApi = configs && configs[api] ? configs[api].private : false;
268 return privateApi ? options.fn(this) : options.inverse(this);
269 });
270
271 Handlebars.registerHelper('getApiPortSuffix', (configs, api) => {
272 if (configs && configs[api] && configs[api].port) {
273 return `:${configs[api].port}/`;
274 }
275 return '/';
276 });
277
278 Handlebars.registerHelper('ifLogApiGatewayToCloudWatch', function ifLogApiGatewayToCloudWatch(configs, api, options) {
279 const logApiGatewayToCloudWatch = configs && configs[api] ? configs[api].logApiGatewayToCloudWatch : false;
280 return logApiGatewayToCloudWatch ? options.fn(this) : options.inverse(this);
281 });
282
283 Handlebars.registerHelper('ifDeployApi', (templateKey, deployDistribution, options) => templateKey !== 'CumulusApiDistribution' || deployDistribution ? options.fn(this) : options.inverse(this));
284
285 return super.parseCF(cfFile);
286 }
287
288 /**
289 * Override CF compilation to inject cumulus message adapter
290 *
291 * @returns {Promise} returns the promise of an AWS response object
292 */
293 compileCF() {
294 const filename = this.config.message_adapter_filename || '';
295 const customCompile = this.config.customCompilation || '';
296 const kesBuildFolder = path.join(this.config.kesFolder, 'build');
297 const unzipFolderName = path.basename(filename, '.zip');
298
299 const src = path.join(process.cwd(), kesBuildFolder, filename);
300 const dest = path.join(process.cwd(), kesBuildFolder, 'adapter', unzipFolderName);
301
302 this.updateRulesConfig();
303
304 // If custom compile configuration flag not set, skip custom compilation
305 if (!customCompile) return super.compileCF();
306
307 // If not using message adapter, don't fetch it
308 if (!filename) return this.superCompileCF();
309
310 return fetchMessageAdapter(this.config.message_adapter_version, this.messageAdapterGitPath, filename, src, dest).then(() => {
311 this.Lambda.messageAdapterZipFileHash = new this.Lambda(this.config).getHash(src);
312 return this.superCompileCF();
313 });
314 }
315
316 /**
317 * Preprocess/update rules config to avoid deployment issues.
318 *
319 * Cloudwatch rules that are triggered by Cloudwatch Step Function events
320 * should be restricted to run only for Step Functions within the current
321 * deployment. Due to character limits for Cloudwatch rule definitions, we
322 * may need to split up a rule into multiple rules so that we can ensure it
323 * is only triggered by Step Functions in this deployment.
324 */
325 updateRulesConfig() {
326 if (!this.config.rules || !this.config.stepFunctions) {
327 return;
328 }
329
330 const { prefixNoDash, rules, stepFunctions } = this.config;
331 const updatedRules = {};
332
333 const initializeNewRule = rule => {
334 const newRule = cloneDeep(rule);
335 newRule.stateMachines = [];
336 newRule.eventPattern.detail.stateMachineArn = [];
337 return newRule;
338 };
339
340 Object.keys(rules).forEach(ruleName => {
341 const rule = rules[ruleName];
342 const eventSource = get(rule, 'eventPattern.source', []);
343
344 // If this rule doesn't use Step Functions as a source, stop processing.
345 if (!eventSource.includes('aws.states')) {
346 updatedRules[ruleName] = rule;
347 return;
348 }
349
350 const initialPatternLength = JSON.stringify(rule.eventPattern).length;
351 // Pessimistically assume longest possible state machine ARN:
352 // 80 = max state machine length
353 // 64 = length of other ARN characters
354 // 2 = two double quotes
355 const arnLength = 80 + 64 + 2;
356 // Determine how many state machines can be added as conditions to the eventPattern
357 // before it would exceed the maximum allowed length of 2048 characters.
358 const stateMachinesPerRule = Math.ceil((2048 - initialPatternLength) / arnLength);
359
360 let stateMachinesCount = 0;
361 let newRule = initializeNewRule(rule);
362 let ruleNumber = 1;
363
364 const stepFunctionNames = Object.keys(stepFunctions);
365 stepFunctionNames.forEach(sfName => {
366 stateMachinesCount += 1;
367
368 if (stateMachinesCount >= stateMachinesPerRule) {
369 stateMachinesCount = 0;
370 newRule = initializeNewRule(rule);
371 ruleNumber += 1;
372 }
373
374 const stateMachineName = `${prefixNoDash}${sfName}StateMachine`;
375 const stateMachineArnRef = `\$\{${stateMachineName}\}`;
376
377 newRule.stateMachines.push(stateMachineName);
378 newRule.eventPattern.detail.stateMachineArn.push(stateMachineArnRef);
379
380 updatedRules[`${ruleName}${ruleNumber}`] = newRule;
381 });
382 });
383
384 this.config.rules = updatedRules;
385 }
386
387 /**
388 * setParentConfigvalues - Overrides nested stack template with parent values
389 * defined in the override_with_parent config key
390 */
391 setParentOverrideConfigValues() {
392 if (!this.config.parent) return;
393 const parent = this.config.parent;
394 this.config.override_with_parent.forEach(value => {
395 this.config[value] = parent[value] == null ? this.config[value] : parent[value];
396 });
397 }
398
399 /**
400 * Modified version of Kes superclass compileCF method
401 *
402 * Compiles a CloudFormation template in Yaml format.
403 *
404 * Reads the configuration yaml from `.kes/config.yml`.
405 *
406 * Writes the template to `.kes/cloudformation.yml`.
407 *
408 * Uses `.kes/cloudformation.template.yml` as the base template
409 * for generating the final CF template.
410 *
411 * @returns {Promise} returns the promise of an AWS response object
412 */
413 superCompileCF() {
414 var _this3 = this;
415
416 return _asyncToGenerator(function* () {
417 _this3.setParentOverrideConfigValues();
418 const lambda = new _this3.Lambda(_this3.config);
419
420 // Process default dead letter queue configs if this value is set
421 if (_this3.config.processDefaultDeadLetterQueues) {
422 _this3.addLambdaDeadLetterQueues();
423 }
424
425 // If the lambdaProcess is set on the subtemplate default configuration
426 // then *build* the lambdas and populate the config object
427 // else only populate the configuration object but do not rebuild
428 // lhe lambda zips
429 if (_this3.config.lambdaProcess) {
430 _this3.config = yield lambda.process();
431 } else {
432 lambda.buildAllLambdaConfiguration('lambdas');
433 }
434
435 let cf;
436
437 // Inject Lambda Alias values into configuration,
438 // then update configured workflow lambda references
439 // to reference the generated alias values
440 if (_this3.config.useWorkflowLambdaVersions === true) {
441 if (_this3.config.oldLambdaInjection === true) {
442 lambda.buildAllLambdaConfiguration('workflowLambdas');
443 yield _this3.injectOldWorkflowLambdaAliases();
444 }
445 if (_this3.config.injectWorkflowLambdaAliases === true) {
446 _this3.injectWorkflowLambdaAliases();
447 }
448 }
449
450 // Update workflowLambdas with generated hash values
451 lambda.addWorkflowLambdaHashes();
452
453 // if there is a template parse CF there first
454 if (_this3.config.template) {
455 const mainCF = _this3.parseCF(_this3.config.template.cfFile);
456
457 // check if there is a CF over
458 try {
459 fs.lstatSync(_this3.config.cfFile);
460 const overrideCF = _this3.parseCF(_this3.config.cfFile);
461
462 // merge the the two
463 cf = utils.mergeYamls(mainCF, overrideCF);
464 } catch (e) {
465 if (!e.message.includes('ENOENT')) {
466 console.log(`compiling the override template at ${_this3.config.cfFile} failed:`);
467 throw e;
468 }
469 cf = mainCF;
470 }
471 } else {
472 cf = _this3.parseCF(_this3.config.cfFile);
473 }
474 const destPath = path.join(_this3.config.kesFolder, _this3.cf_template_name);
475
476 console.log(`Template saved to ${destPath}`);
477 return fsWriteFile(destPath, cf);
478 })();
479 }
480
481 /**
482 * Calls CloudFormation's update-stack or create-stack methods
483 * Changed to support multi-template configs by checking for params sub-objects, i.e.:
484 * params:
485 * app:
486 * - name: someName
487 * value: someValue
488 *
489 * @returns {Promise} returns the promise of an AWS response object
490 */
491 cloudFormation() {
492 if (this.config.app && this.config.app.params) this.config.params = this.config.app.params;
493 // Fetch db stack outputs to retrieve DynamoDBStreamARNs and ESDomainEndpoint
494 return this.describeStack(this.config.dbStackName).then(r => {
495 if (r && r.Stacks[0] && r.Stacks[0].Outputs) {
496 r.Stacks[0].Outputs.forEach(o => this.config.params.push({
497 name: o.OutputKey,
498 value: o.OutputValue
499 }));
500 } else {
501 throw new Error(`Failed to fetch outputs for db stack ${this.config.dbStackName}`);
502 }
503 }).then(() => super.cloudFormation());
504 }
505
506 /**
507 * Updates lambda/sqs configuration to include an sqs dead letter queue
508 * matching the lambdas's name (e.g. {lambda.name}DeadLetterQueue)
509 * @returns {void} Returns nothing.
510 */
511 addLambdaDeadLetterQueues() {
512 const lambdas = this.config.lambdas;
513 Object.keys(lambdas).forEach(key => {
514 const lambda = lambdas[key];
515 if (lambda.namedLambdaDeadLetterQueue) {
516 console.log(`Adding named dead letter queue for ${lambda.name}`);
517 const queueName = `${lambda.name}DeadLetterQueue`;
518 this.config.sqs[queueName] = {
519 MessageRetentionPeriod: this.config.DLQDefaultMessageRetentionPeriod,
520 visibilityTimeout: this.config.DLQDefaultTimeout
521 };
522 this.config.lambdas[lambda.name].deadletterqueue = queueName;
523 }
524 });
525 }
526
527 /**
528 *
529 * @param {Object} lambda - AWS lambda object
530 * @param {Object} config - AWS listAliases configuration object.
531 * @returns {Promise.Object[]} returns the promise of an array of AWS Alias objects
532 */
533 getAllLambdaAliases(lambda, config) {
534 var _this4 = this;
535
536 return _asyncToGenerator(function* () {
537 const lambdaConfig = Object.assign({}, config);
538 let aliasPage;
539 try {
540 aliasPage = yield lambda.listAliases(lambdaConfig).promise();
541 } catch (err) {
542 if (err.statusCode === 404) {
543 return [];
544 }
545 throw err;
546 }
547
548 if (!aliasPage.NextMarker) {
549 return aliasPage.Aliases;
550 }
551 const aliases = aliasPage.Aliases;
552 lambdaConfig.Marker = aliasPage.NextMarker;
553
554 return aliases.concat((yield _this4.getAllLambdaAliases(lambda, lambdaConfig)));
555 })();
556 }
557
558 /**
559 * Using the object configuration, this function gets the 'config.maxNumerOfRetainedLambdas'
560 * number of most recent lambda alias names to retain in the 'Old Lambda Resources' section of
561 * the LambdaVersion template, avoiding duplicates of items in the Current Lambda section.
562 *
563 * @returns {Promise.string[]} returns the promise of a list of alias metadata
564 * objects: keys (Name, humanReadableIdentifier)
565 **/
566 getRetainedLambdaAliasMetadata() {
567 var _this5 = this;
568
569 return _asyncToGenerator(function* () {
570 const awsLambda = new _this5.AWS.Lambda();
571 const cumulusAliasDescription = 'Cumulus AutoGenerated Alias';
572 const configLambdas = _this5.config.workflowLambdas;
573 const numberOfRetainedLambdas = _this5.config.maxNumberOfRetainedLambdas;
574
575 let aliasMetadataObjects = [];
576
577 const lambdaNames = Object.keys(configLambdas);
578 const aliasListsPromises = lambdaNames.map((() => {
579 var _ref = _asyncToGenerator(function* (lambdaName) {
580 const listAliasesConfig = {
581 MaxItems: 10000,
582 FunctionName: `${_this5.config.stackName}-${lambdaName}`
583 };
584 return _this5.getAllLambdaAliases(awsLambda, listAliasesConfig);
585 });
586
587 return function (_x) {
588 return _ref.apply(this, arguments);
589 };
590 })());
591
592 const aliasLists = yield Promise.all(aliasListsPromises);
593 const aliasListsObject = zipObject(lambdaNames, aliasLists);
594
595 lambdaNames.forEach(function (lambdaName) {
596 console.log(`Evaluating: ${lambdaName} for old versions/aliases to retain. `);
597 const aliases = aliasListsObject[lambdaName];
598 const cumulusAliases = aliases.filter(function (alias) {
599 return alias.Description.includes(cumulusAliasDescription);
600 });
601
602 if (cumulusAliases.length === 0) return;
603
604 cumulusAliases.sort(function (a, b) {
605 return b.FunctionVersion - a.FunctionVersion;
606 });
607 const oldAliases = cumulusAliases.filter(function (alias) {
608 return _this5.parseAliasName(alias.Name).hash !== configLambdas[lambdaName].hash;
609 });
610 const oldAliasMetadataObjects = oldAliases.map(function (alias) {
611 return {
612 name: alias.Name,
613 humanReadableIdentifier: _this5.getHumanReadableIdentifier(alias.Description)
614 };
615 }).slice(0, numberOfRetainedLambdas);
616
617 if (oldAliasMetadataObjects.length > 0) {
618 console.log('Adding the following "old" versions to LambdaVersions:', `${JSON.stringify(oldAliasMetadataObjects.map(function (obj) {
619 return obj.name;
620 }))}`);
621 }
622 aliasMetadataObjects = aliasMetadataObjects.concat(oldAliasMetadataObjects);
623 });
624 return aliasMetadataObjects;
625 })();
626 }
627
628 /**
629 * Parses a passed in alias description field for a version string,
630 * (e.g. `Cumulus Autogenerated Alias |version`)
631 *
632 * @param {string} description lambda alias description
633 * @returns {string} Returns the human readable version or '' if no match is found
634 */
635 getHumanReadableIdentifier(description) {
636 const descriptionMatch = description.match(/.*\|(.*)$/);
637 if (!descriptionMatch) return '';
638 return descriptionMatch[1] || '';
639 }
640
641 /**
642 * Parses Alias name properties into a results object
643 *
644 * @param {string} name - Cumulus created CF Lambda::Alias name parameter
645 * in format Name-Hash,
646 * @returns {Object} returns hash with name/value keys mapped to appropriate
647 * matches and sets hash to null if no hash match in 'name'
648 */
649 parseAliasName(name) {
650 const regExp = /^([^-]*)-([^-]*)$/;
651 const regExpResults = regExp.exec(name);
652 let hashValue = null;
653 if (regExpResults[2]) hashValue = regExpResults[2];
654 return { name: regExpResults[1], hash: hashValue };
655 }
656
657 /**
658 * Uses getRetainedLambdaAliasMetadata to generate a list of lambda
659 * aliases to save, then parses each name/hash pair to generate CF template
660 * configuration name: [hashes] and injects that into the oldLambdas config
661 * key
662 *
663 * @returns {Promise.void} Returns nothing.
664 */
665 injectOldWorkflowLambdaAliases() {
666 var _this6 = this;
667
668 return _asyncToGenerator(function* () {
669 const oldLambdaMetadataObjects = yield _this6.getRetainedLambdaAliasMetadata();
670 const oldLambdas = {};
671
672 oldLambdaMetadataObjects.forEach(function (obj) {
673 const matchObject = _this6.parseAliasName(obj.name);
674 if (matchObject.hash) {
675 if (!oldLambdas[matchObject.name]) oldLambdas[matchObject.name] = { lambdaRefs: [] };
676 oldLambdas[matchObject.name].lambdaRefs.push({
677 hash: matchObject.hash,
678 humanReadableIdentifier: obj.humanReadableIdentifier
679 });
680 }
681 });
682 _this6.config.oldLambdas = oldLambdas;
683 })();
684 }
685
686 /**
687 * Updates all this.config.stepFunctions state objects of type Task with
688 * a LambdaFunction.ARN resource to refer to the a generated LambdaAlias
689 * reference elsewhere in the template.
690 *
691 * Functions without a unique identifier (hash), and therefore no alias
692 * will continue to utilize the original reference.
693 *
694 * @returns {void} Returns nothing.
695 */
696 injectWorkflowLambdaAliases() {
697 console.log('Updating workflow Lambda ARN references to Lambda Alias references');
698 Object.keys(this.config.stepFunctions).forEach(stepFunction => {
699 const stepFunctionStateKeys = Object.keys(this.config.stepFunctions[stepFunction].States);
700 stepFunctionStateKeys.forEach(stepFunctionState => {
701 const stateObject = this.config.stepFunctions[stepFunction].States[stepFunctionState];
702
703 if (stateObject.Type === 'Task' && stateObject.Resource.endsWith('LambdaFunction.Arn}')) {
704 console.log(`Updating workflow ${stateObject.Resource} reference`);
705 const lambdaAlias = this.lookupLambdaReference(stateObject.Resource);
706 console.log(`Setting reference to ${lambdaAlias}`);
707 stateObject.Resource = lambdaAlias;
708 }
709 });
710 });
711 }
712
713 /**
714 * Programatically evaluates a lambda ARN reference and returns the expected template reference.
715 * This will either be the unqualified Lambda reference if unique identifier exists, or a
716 * reference to the expected LambdaAliasOutput key from the LambdaVersions subtemplate.
717 *
718 * @param {string} stateObjectResource - CF template resource reference for a state function
719 * @returns {string} The correct reference to the lambda function, either a hashed alias
720 * reference or the passed in resource if hashing/versioning isn't possible for this resource
721 * @throws {Error} Throws an error if the passed in stateObjectResource isn't a LambdaFunctionArn
722 * reference
723 */
724 lookupLambdaReference(stateObjectResource) {
725 let lambdaKey;
726 const regExp = /^\$\{(.*)LambdaFunction.Arn/;
727 const matchArray = regExp.exec(stateObjectResource);
728
729 if (matchArray) {
730 lambdaKey = matchArray[1];
731 } else {
732 console.log(`Invalid workflow configuration, ${stateObjectResource} ` + 'is not a valid Lambda ARN');
733 throw new Error(`Invalid stateObjectResource: ${stateObjectResource}`);
734 }
735 const lambdaHash = this.config.lambdas[lambdaKey].hash;
736 // If a lambda resource doesn't have a hash, refer directly to the function ARN
737 if (!lambdaHash) {
738 console.log(`No unique identifier for ${lambdaKey}, referencing ${stateObjectResource}`);
739 return stateObjectResource;
740 }
741
742 return `\$\{${lambdaKey}LambdaAliasOutput\}`;
743 }
744
745 uploadTaskReaper() {
746 return this.s3.putObject({
747 Bucket: this.bucket,
748 Key: `${this.stack}/deployment-staging/task-reaper.sh`,
749 Body: fs.createReadStream(path.join(__dirname, '..', 'task-reaper.sh'))
750 }).promise();
751 }
752
753 /**
754 * Override opsStack method.
755 *
756 * @returns {Promise} aws response
757 */
758 opsStack() {
759 // check if public and private key are generated
760 // if not generate and upload them
761 const apis = {};
762
763 // remove config variable from all workflow steps
764 // and keep them in a separate variable.
765 // this is needed to prevent StepFunction deployment from crashing
766 this.config = extractCumulusConfigFromSF(this.config);
767
768 return crypto(this.stack, this.bucket, this.s3).then(() => this.uploadTaskReaper()).then(() => super.opsStack()).then(() => this.describeCF()).then(r => {
769 const outputs = r.Stacks[0].Outputs;
770
771 const urls = {
772 Api: 'token',
773 Distribution: 'redirect'
774 };
775 console.log('\nHere are the important URLs for this deployment:\n');
776 outputs.forEach(o => {
777 if (Object.keys(urls).includes(o.OutputKey)) {
778 console.log(`${o.OutputKey}: `, o.OutputValue);
779 console.log('Add this url to URS: ', `${o.OutputValue}${urls[o.OutputKey]}`, '\n');
780
781 if (o.OutputKey === 'Distribution') {
782 this.config.distribution_endpoint = o.OutputValue;
783 }
784 }
785
786 switch (o.OutputKey) {
787 case 'ApiId':
788 apis.api = o.OutputValue;
789 break;
790 case 'DistributionId':
791 apis.distribution = o.OutputValue;
792 break;
793 case 'ApiStage':
794 apis.stageName = o.OutputValue;
795 break;
796 default:
797 //nothing
798 }
799 });
800
801 return generateTemplates(this.config, outputs, this.uploadToS3.bind(this));
802 }).then(() => this.restartECSTasks(this.config)).then(() => this.redeployApiGateWay('api', apis.api, apis.stageName)).then(() => this.redeployApiGateWay('distribution', apis.distribution, apis.stageName)).catch(e => {
803 console.log(e);
804 throw e;
805 });
806 }
807}
808
809module.exports = UpdatedKes;
\No newline at end of file