UNPKG

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