UNPKG

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