UNPKG

29.4 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 this.setParentOverrideConfigValues();
319
320 // If not using message adapter, don't fetch it
321 if (!filename) return this.superCompileCF();
322
323 return fetchMessageAdapter(
324 this.config.message_adapter_version,
325 this.messageAdapterGitPath,
326 filename,
327 src,
328 dest
329 ).then(() => {
330 this.Lambda.messageAdapterZipFileHash = new this.Lambda(this.config).getHash(src);
331 return this.superCompileCF();
332 });
333 }
334
335 /**
336 * Preprocess/update rules config to avoid deployment issues.
337 *
338 * Cloudwatch rules that are triggered by Cloudwatch Step Function events
339 * should be restricted to run only for Step Functions within the current
340 * deployment. Due to character limits for Cloudwatch rule definitions, we
341 * may need to split up a rule into multiple rules so that we can ensure it
342 * is only triggered by Step Functions in this deployment.
343 */
344 updateRulesConfig() {
345 if (!this.config.rules || !this.config.stepFunctions) {
346 return;
347 }
348
349 const { prefixNoDash, rules, stepFunctions } = this.config;
350 const updatedRules = {};
351
352 const initializeNewRule = (rule) => {
353 const newRule = cloneDeep(rule);
354 newRule.stateMachines = [];
355 newRule.eventPattern.detail = newRule.eventPattern.detail || {};
356 newRule.eventPattern.detail.stateMachineArn = [];
357 return newRule;
358 };
359
360 Object.keys(rules).forEach((ruleName) => {
361 const rule = rules[ruleName];
362 const eventSource = get(rule, 'eventPattern.source', []);
363
364 // If this rule doesn't use Step Functions as a source, stop processing.
365 if (!eventSource.includes('aws.states')) {
366 updatedRules[ruleName] = rule;
367 return;
368 }
369
370 const initialPatternLength = JSON.stringify(rule.eventPattern).length;
371 // Pessimistically assume longest possible state machine ARN:
372 // 80 = max state machine length
373 // 64 = length of other ARN characters
374 // 2 = two double quotes
375 const arnLength = 80 + 64 + 2;
376 // Determine how many state machines can be added as conditions to the eventPattern
377 // before it would exceed the maximum allowed length of 2048 characters.
378 const stateMachinesPerRule = Math.ceil((2048 - initialPatternLength) / arnLength);
379
380 let stateMachinesCount = 0;
381 let newRule = initializeNewRule(rule);
382 let ruleNumber = 1;
383
384 const stepFunctionNames = Object.keys(stepFunctions);
385 stepFunctionNames.forEach((sfName) => {
386 stateMachinesCount += 1;
387
388 if (stateMachinesCount >= stateMachinesPerRule) {
389 stateMachinesCount = 0;
390 newRule = initializeNewRule(rule);
391 ruleNumber += 1;
392 }
393
394 const stateMachineName = `${prefixNoDash}${sfName}StateMachine`;
395 const stateMachineArnRef = `\$\{${stateMachineName}\}`;
396
397 newRule.stateMachines.push(stateMachineName);
398 newRule.eventPattern.detail.stateMachineArn.push(stateMachineArnRef);
399
400 updatedRules[`${ruleName}${ruleNumber}`] = newRule;
401 });
402 });
403
404 this.config.rules = updatedRules;
405 }
406
407 /**
408 * setParentConfigvalues - Overrides nested stack template with parent values
409 * defined in the override_with_parent config key
410 */
411 setParentOverrideConfigValues() {
412 if (!this.config.parent) return;
413 const parent = this.config.parent;
414 this.config.override_with_parent.forEach((value) => {
415 this.config[value] = (parent[value] == null) ? this.config[value] : parent[value];
416 });
417 }
418
419 /**
420 * Modified version of Kes superclass compileCF method
421 *
422 * Compiles a CloudFormation template in Yaml format.
423 *
424 * Reads the configuration yaml from `.kes/config.yml`.
425 *
426 * Writes the template to `.kes/cloudformation.yml`.
427 *
428 * Uses `.kes/cloudformation.template.yml` as the base template
429 * for generating the final CF template.
430 *
431 * @returns {Promise} returns the promise of an AWS response object
432 */
433 async superCompileCF() {
434 const lambda = new this.Lambda(this.config);
435
436 // Process default dead letter queue configs if this value is set
437 if (this.config.processDefaultDeadLetterQueues) {
438 this.addLambdaDeadLetterQueues();
439 }
440
441 // If the lambdaProcess is set on the subtemplate default configuration
442 // then *build* the lambdas and populate the config object
443 // else only populate the configuration object but do not rebuild
444 // lhe lambda zips
445 if (this.config.lambdaProcess) {
446 this.config = await lambda.process();
447 } else {
448 lambda.buildAllLambdaConfiguration('lambdas');
449 }
450
451 let cf;
452
453 // Inject Lambda Alias values into configuration,
454 // then update configured workflow lambda references
455 // to reference the generated alias values
456 if (this.config.useWorkflowLambdaVersions === true) {
457 if (this.config.oldLambdaInjection === true) {
458 lambda.buildAllLambdaConfiguration('workflowLambdas');
459 await this.injectOldWorkflowLambdaAliases();
460 }
461 if (this.config.injectWorkflowLambdaAliases === true) {
462 this.injectWorkflowLambdaAliases();
463 }
464 }
465
466
467 // Update workflowLambdas with generated hash values
468 lambda.addWorkflowLambdaHashes();
469
470 // if there is a template parse CF there first
471 if (this.config.template) {
472 const mainCF = this.parseCF(this.config.template.cfFile);
473
474 // check if there is a CF over
475 try {
476 fs.lstatSync(this.config.cfFile);
477 const overrideCF = this.parseCF(this.config.cfFile);
478
479 // merge the the two
480 cf = utils.mergeYamls(mainCF, overrideCF);
481 } catch (e) {
482 if (!e.message.includes('ENOENT')) {
483 console.log(`compiling the override template at ${this.config.cfFile} failed:`);
484 throw e;
485 }
486 cf = mainCF;
487 }
488 } else {
489 cf = this.parseCF(this.config.cfFile);
490 }
491 const destPath = path.join(this.config.kesFolder, this.cf_template_name);
492
493 console.log(`Template saved to ${destPath}`);
494 return fsWriteFile(destPath, cf);
495 }
496
497 /**
498 * Calls CloudFormation's update-stack or create-stack methods
499 * Changed to support multi-template configs by checking for params sub-objects, i.e.:
500 * params:
501 * app:
502 * - name: someName
503 * value: someValue
504 *
505 * @returns {Promise} returns the promise of an AWS response object
506 */
507 cloudFormation() {
508 if (this.config.app && this.config.app.params) this.config.params = this.config.app.params;
509 // Fetch db stack outputs to retrieve DynamoDBStreamARNs and ESDomainEndpoint
510 return this.describeStack(this.config.dbStackName).then((r) => {
511 if (r && r.Stacks[0] && r.Stacks[0].Outputs) {
512 r.Stacks[0].Outputs.forEach((o) => this.config.params.push({
513 name: o.OutputKey,
514 value: o.OutputValue
515 }));
516 } else {
517 throw new Error(`Failed to fetch outputs for db stack ${this.config.dbStackName}`);
518 }
519 }).then(() => super.cloudFormation());
520 }
521
522
523 /**
524 * Updates lambda/sqs configuration to include an sqs dead letter queue
525 * matching the lambdas's name (e.g. {lambda.name}DeadLetterQueue)
526 * @returns {void} Returns nothing.
527 */
528 addLambdaDeadLetterQueues() {
529 const lambdas = this.config.lambdas;
530 Object.keys(lambdas).forEach((key) => {
531 const lambda = lambdas[key];
532 if (lambda.namedLambdaDeadLetterQueue) {
533 console.log(`Adding named dead letter queue for ${lambda.name}`);
534 const queueName = `${lambda.name}DeadLetterQueue`;
535 this.config.sqs[queueName] = {
536 MessageRetentionPeriod: this.config.DLQDefaultMessageRetentionPeriod,
537 visibilityTimeout: this.config.DLQDefaultTimeout
538 };
539 this.config.lambdas[lambda.name].deadletterqueue = queueName;
540 }
541 });
542 }
543
544 /**
545 *
546 * @param {Object} lambda - AWS lambda object
547 * @param {Object} config - AWS listAliases configuration object.
548 * @returns {Promise.Object[]} returns the promise of an array of AWS Alias objects
549 */
550 async getAllLambdaAliases(lambda, config) {
551 const lambdaConfig = Object.assign({}, config);
552 let aliasPage;
553 try {
554 aliasPage = await lambda.listAliases(lambdaConfig).promise();
555 } catch (err) {
556 if (err.statusCode === 404) {
557 return [];
558 }
559 throw (err);
560 }
561
562 if (!aliasPage.NextMarker) {
563 return aliasPage.Aliases;
564 }
565 const aliases = aliasPage.Aliases;
566 lambdaConfig.Marker = aliasPage.NextMarker;
567
568 return aliases.concat(await this.getAllLambdaAliases(lambda, lambdaConfig));
569 }
570
571 /**
572 * Using the object configuration, this function gets the 'config.maxNumerOfRetainedLambdas'
573 * number of most recent lambda alias names to retain in the 'Old Lambda Resources' section of
574 * the LambdaVersion template, avoiding duplicates of items in the Current Lambda section.
575 *
576 * @returns {Promise.string[]} returns the promise of a list of alias metadata
577 * objects: keys (Name, humanReadableIdentifier)
578 **/
579 async getRetainedLambdaAliasMetadata() {
580 const awsLambda = new this.AWS.Lambda();
581 const cumulusAliasDescription = 'Cumulus AutoGenerated Alias';
582 const configLambdas = this.config.workflowLambdas;
583 const numberOfRetainedLambdas = this.config.maxNumberOfRetainedLambdas;
584
585 let aliasMetadataObjects = [];
586
587 const lambdaNames = Object.keys(configLambdas);
588 const aliasListsPromises = lambdaNames.map(async (lambdaName) => {
589 const listAliasesConfig = {
590 MaxItems: 10000,
591 FunctionName: `${this.config.stackName}-${lambdaName}`
592 };
593 return this.getAllLambdaAliases(awsLambda, listAliasesConfig);
594 });
595
596 const aliasLists = await Promise.all(aliasListsPromises);
597 const aliasListsObject = zipObject(lambdaNames, aliasLists);
598
599 lambdaNames.forEach((lambdaName) => {
600 console.log(`Evaluating: ${lambdaName} for old versions/aliases to retain. `);
601 const aliases = aliasListsObject[lambdaName];
602 const cumulusAliases = aliases.filter(
603 (alias) => alias.Description.includes(cumulusAliasDescription)
604 );
605
606 if (cumulusAliases.length === 0) return;
607
608 cumulusAliases.sort((a, b) => b.FunctionVersion - a.FunctionVersion);
609 const oldAliases = cumulusAliases.filter(
610 (alias) => this.parseAliasName(alias.Name).hash !== configLambdas[lambdaName].hash
611 );
612 const oldAliasMetadataObjects = oldAliases.map((alias) => (
613 {
614 name: alias.Name,
615 humanReadableIdentifier: this.getHumanReadableIdentifier(alias.Description)
616 }
617 )).slice(0, numberOfRetainedLambdas);
618
619 if (oldAliasMetadataObjects.length > 0) {
620 console.log(
621 'Adding the following "old" versions to LambdaVersions:',
622 `${JSON.stringify(oldAliasMetadataObjects.map((obj) => obj.name))}`
623 );
624 }
625 aliasMetadataObjects = aliasMetadataObjects.concat(oldAliasMetadataObjects);
626 });
627 return aliasMetadataObjects;
628 }
629
630
631 /**
632 * Parses a passed in alias description field for a version string,
633 * (e.g. `Cumulus Autogenerated Alias |version`)
634 *
635 * @param {string} description lambda alias description
636 * @returns {string} Returns the human readable version or '' if no match is found
637 */
638 getHumanReadableIdentifier(description) {
639 const descriptionMatch = description.match(/.*\|(.*)$/);
640 if (!descriptionMatch) return '';
641 return descriptionMatch[1] || '';
642 }
643
644 /**
645 * Parses Alias name properties into a results object
646 *
647 * @param {string} name - Cumulus created CF Lambda::Alias name parameter
648 * in format Name-Hash,
649 * @returns {Object} returns hash with name/value keys mapped to appropriate
650 * matches and sets hash to null if no hash match in 'name'
651 */
652 parseAliasName(name) {
653 const regExp = /^([^-]*)-([^-]*)$/;
654 const regExpResults = regExp.exec(name);
655 let hashValue = null;
656 if (regExpResults[2]) hashValue = regExpResults[2];
657 return { name: regExpResults[1], hash: hashValue };
658 }
659
660 /**
661 * Uses getRetainedLambdaAliasMetadata to generate a list of lambda
662 * aliases to save, then parses each name/hash pair to generate CF template
663 * configuration name: [hashes] and injects that into the oldLambdas config
664 * key
665 *
666 * @returns {Promise.void} Returns nothing.
667 */
668 async injectOldWorkflowLambdaAliases() {
669 const oldLambdaMetadataObjects = await this.getRetainedLambdaAliasMetadata();
670 const oldLambdas = {};
671
672 oldLambdaMetadataObjects.forEach((obj) => {
673 const matchObject = this.parseAliasName(obj.name);
674 if (matchObject.hash) {
675 if (!oldLambdas[matchObject.name]) oldLambdas[matchObject.name] = { lambdaRefs: [] };
676 oldLambdas[matchObject.name].lambdaRefs.push(
677 {
678 hash: matchObject.hash,
679 humanReadableIdentifier: obj.humanReadableIdentifier
680 }
681 );
682 }
683 });
684 this.config.oldLambdas = oldLambdas;
685 }
686
687
688 /**
689 * Updates all this.config.stepFunctions state objects of type Task with
690 * a LambdaFunction.ARN resource to refer to the a generated LambdaAlias
691 * reference elsewhere in the template.
692 *
693 * Functions without a unique identifier (hash), and therefore no alias
694 * will continue to utilize the original reference.
695 *
696 * @returns {void} Returns nothing.
697 */
698 injectWorkflowLambdaAliases() {
699 console.log('Updating workflow Lambda ARN references to Lambda Alias references');
700 Object.keys(this.config.stepFunctions).forEach((stepFunction) => {
701 const stepFunctionStateKeys = Object.keys(this.config.stepFunctions[stepFunction].States);
702 stepFunctionStateKeys.forEach((stepFunctionState) => {
703 const stateObject = this.config.stepFunctions[stepFunction].States[stepFunctionState];
704
705 if ((stateObject.Type === 'Task')
706 && (stateObject.Resource.endsWith('LambdaFunction.Arn}'))) {
707 console.log(`Updating workflow ${stateObject.Resource} reference`);
708 const lambdaAlias = this.lookupLambdaReference(stateObject.Resource);
709 console.log(`Setting reference to ${lambdaAlias}`);
710 stateObject.Resource = lambdaAlias;
711 }
712 });
713 });
714 }
715
716
717 /**
718 * Programatically evaluates a lambda ARN reference and returns the expected template reference.
719 * This will either be the unqualified Lambda reference if unique identifier exists, or a
720 * reference to the expected LambdaAliasOutput key from the LambdaVersions subtemplate.
721 *
722 * @param {string} stateObjectResource - CF template resource reference for a state function
723 * @returns {string} The correct reference to the lambda function, either a hashed alias
724 * reference or the passed in resource if hashing/versioning isn't possible for this resource
725 * @throws {Error} Throws an error if the passed in stateObjectResource isn't a LambdaFunctionArn
726 * reference
727 */
728 lookupLambdaReference(stateObjectResource) {
729 let lambdaKey;
730 const regExp = /^\$\{(.*)LambdaFunction.Arn/;
731 const matchArray = regExp.exec(stateObjectResource);
732
733 if (matchArray) {
734 lambdaKey = matchArray[1];
735 } else {
736 console.log(`Invalid workflow configuration, ${stateObjectResource} `
737 + 'is not a valid Lambda ARN');
738 throw new Error(`Invalid stateObjectResource: ${stateObjectResource}`);
739 }
740 const lambdaHash = this.config.lambdas[lambdaKey].hash;
741 // If a lambda resource doesn't have a hash, refer directly to the function ARN
742 if (!lambdaHash) {
743 console.log(`No unique identifier for ${lambdaKey}, referencing ${stateObjectResource}`);
744 return (stateObjectResource);
745 }
746
747 return `\$\{${lambdaKey}LambdaAliasOutput\}`;
748 }
749
750 uploadTaskReaper() {
751 return this.s3.putObject({
752 Bucket: this.bucket,
753 Key: `${this.stack}/deployment-staging/task-reaper.sh`,
754 Body: fs.createReadStream(path.join(__dirname, '..', 'task-reaper.sh'))
755 }).promise();
756 }
757
758 /**
759 * Override opsStack method.
760 *
761 * @returns {Promise} aws response
762 */
763 opsStack() {
764 // check if public and private key are generated
765 // if not generate and upload them
766 const apis = {};
767
768 // remove config variable from all workflow steps
769 // and keep them in a separate variable.
770 // this is needed to prevent StepFunction deployment from crashing
771 this.config = extractCumulusConfigFromSF(this.config);
772
773 return crypto(this.stack, this.bucket, this.s3)
774 .then(() => this.uploadTaskReaper())
775 .then(() => super.opsStack())
776 .then(() => this.describeCF())
777 .then((r) => {
778 const outputs = r.Stacks[0].Outputs;
779
780 const urls = {
781 Api: 'token',
782 Distribution: 'redirect'
783 };
784 console.log('\nHere are the important URLs for this deployment:\n');
785 outputs.forEach((o) => {
786 if (Object.keys(urls).includes(o.OutputKey)) {
787 console.log(`${o.OutputKey}: `, o.OutputValue);
788 console.log('Add this url to URS: ', `${o.OutputValue}${urls[o.OutputKey]}`, '\n');
789
790 if (o.OutputKey === 'Distribution') {
791 this.config.distribution_endpoint = o.OutputValue;
792 }
793 }
794
795 switch (o.OutputKey) {
796 case 'ApiId':
797 apis.api = o.OutputValue;
798 break;
799 case 'DistributionId':
800 apis.distribution = o.OutputValue;
801 break;
802 case 'ApiStage':
803 apis.stageName = o.OutputValue;
804 break;
805 default:
806 //nothing
807 }
808 });
809
810 return generateTemplates(this.config, outputs, this.uploadToS3.bind(this));
811 })
812 .then(() => this.restartECSTasks(this.config))
813 .then(() => this.redeployApiGateWay('api', apis.api, apis.stageName))
814 .then(() => this.redeployApiGateWay('distribution', apis.distribution, apis.stageName))
815 .catch((e) => {
816 console.log(e);
817 throw e;
818 });
819 }
820}
821
822module.exports = UpdatedKes;