@cumulus/message 18.2.1 → 18.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ import { Message } from '@cumulus/types';
2
+
3
+ /**
4
+ * Bare check for CumulusMessage Shape
5
+ */
6
+ export const isCumulusMessageLike = (message: any): message is Message.CumulusMessage => (
7
+ message instanceof Object
8
+ && 'cumulus_meta' in message
9
+ );
@@ -1,54 +1,197 @@
1
- import { SQSRecord, EventBridgeEvent } from 'aws-lambda';
1
+ //@ts-check
2
+ import { SQSRecord } from 'aws-lambda';
3
+ import moment from 'moment';
2
4
 
3
- import { parseSQSMessageBody } from '@cumulus/aws-client/SQS';
5
+ import { uuid } from 'uuidv4';
6
+ import { isEventBridgeEvent, StepFunctionEventBridgeEvent } from '@cumulus/aws-client/Lambda';
7
+ import { parseSQSMessageBody, isSQSRecordLike } from '@cumulus/aws-client/SQS';
4
8
  import { CumulusMessage } from '@cumulus/types/message';
9
+ import { DLQRecord, DLARecord } from '@cumulus/types/api/dead_letters';
5
10
  import Logger from '@cumulus/logger';
6
-
11
+ import { MessageGranule } from '@cumulus/types';
12
+ import { isMessageWithProvider, getMessageProviderId } from './Providers';
13
+ import { isCumulusMessageLike } from './CumulusMessage';
7
14
  import { getCumulusMessageFromExecutionEvent } from './StepFunctions';
15
+ import { constructCollectionId } from './Collections';
8
16
 
9
17
  const log = new Logger({ sender: '@cumulus/DeadLetterMessage' });
10
18
 
11
- type StepFunctionEventBridgeEvent = EventBridgeEvent<'Step Functions Execution Status Change', { [key: string]: string }>;
12
- type UnwrapDeadLetterCumulusMessageReturnType = (
19
+ type UnwrapDeadLetterCumulusMessageInputType = (
13
20
  StepFunctionEventBridgeEvent
14
- | AWS.SQS.Message
15
- | SQSRecord
21
+ | DLQRecord | SQSRecord
16
22
  | CumulusMessage
17
23
  );
18
24
 
25
+ /**
26
+ * Bare check for SQS message Shape
27
+ */
28
+ export const isDLQRecordLike = (message: Object): message is DLQRecord => (
29
+ isSQSRecordLike(message)
30
+ && 'error' in message
31
+ );
32
+
19
33
  /**
20
34
  * Unwrap dead letter Cumulus message, which may be wrapped in a
21
35
  * States cloudwatch event, which is wrapped in an SQS message.
22
- *
23
- * @param {Object} messageBody - received SQS message
24
- * @returns {Object} the cumulus message or nearest available object
25
36
  */
26
37
  export const unwrapDeadLetterCumulusMessage = async (
27
- messageBody: UnwrapDeadLetterCumulusMessageReturnType
28
- ): Promise<UnwrapDeadLetterCumulusMessageReturnType> => {
38
+ messageBody: UnwrapDeadLetterCumulusMessageInputType
39
+ ): Promise<UnwrapDeadLetterCumulusMessageInputType> => {
29
40
  try {
30
- if ('cumulus_meta' in messageBody) {
31
- return messageBody;
32
- }
33
- if ('Body' in messageBody || 'body' in messageBody) {
41
+ if (isSQSRecordLike(messageBody)) {
34
42
  // AWS.SQS.Message/SQS.Record case
35
43
  const unwrappedMessageBody = parseSQSMessageBody(
36
44
  messageBody
37
- ) as CumulusMessage;
45
+ );
38
46
  return await unwrapDeadLetterCumulusMessage(unwrappedMessageBody);
39
47
  }
40
- if (!('detail' in messageBody)) {
41
- // Non-typed catchall
48
+ if (isEventBridgeEvent(messageBody)) {
49
+ return await getCumulusMessageFromExecutionEvent(
50
+ messageBody
51
+ );
52
+ }
53
+ if (isCumulusMessageLike(messageBody)) {
42
54
  return messageBody;
43
55
  }
44
- // StepFunctionEventBridgeEvent case
45
- const unwrappedMessageBody = await getCumulusMessageFromExecutionEvent(messageBody);
46
- return await unwrapDeadLetterCumulusMessage(unwrappedMessageBody);
56
+ throw new TypeError('DeadLetter CumulusMessage in unrecognized format');
47
57
  } catch (error) {
48
58
  log.error(
49
59
  'Falling back to storing wrapped message after encountering unwrap error',
50
- error
60
+ error,
61
+ JSON.stringify(messageBody)
51
62
  );
52
63
  return messageBody;
53
64
  }
54
65
  };
66
+
67
+ interface PayloadWithGranules {
68
+ granules: Array<MessageGranule>
69
+ }
70
+
71
+ const payloadHasGranules = (payload: any): payload is PayloadWithGranules => (
72
+ payload instanceof Object
73
+ && 'granules' in payload
74
+ && Array.isArray(payload.granules)
75
+ );
76
+
77
+ const extractCollectionId = (message: CumulusMessage): string | null => {
78
+ const collectionName = message?.meta?.collection?.name || null;
79
+ const collectionVersion = message?.meta?.collection?.version || null;
80
+ if (collectionName && collectionVersion) {
81
+ return constructCollectionId(collectionName, collectionVersion);
82
+ }
83
+ return null;
84
+ };
85
+
86
+ const extractGranules = (message: CumulusMessage): Array<string | null> | null => {
87
+ if (payloadHasGranules(message.payload)) {
88
+ return message.payload.granules.map((granule) => granule?.granuleId || null);
89
+ }
90
+ return null;
91
+ };
92
+
93
+ type DLQMetadata = Partial<DLQRecord> & { body: undefined };
94
+ /**
95
+ * peel out metadata from an SQS(/DLQ)record
96
+ * @param message DLQ or SQS message
97
+ * @returns the given message without its body
98
+ */
99
+ const extractSQSMetadata = (message: DLQRecord | SQSRecord): DLQMetadata => {
100
+ const metadata = { ...message } as any;
101
+ delete metadata.body;
102
+ delete metadata.Body;
103
+ return metadata;
104
+ };
105
+
106
+ /**
107
+ * Reformat object with key attributes at top level.
108
+ *
109
+ */
110
+ export const hoistCumulusMessageDetails = async (dlqRecord: SQSRecord): Promise<DLARecord> => {
111
+ let executionArn = null;
112
+ let stateMachineArn = null;
113
+ let status = null;
114
+ let time = null;
115
+ let collectionId = null;
116
+ let granules = null;
117
+ let providerId = null;
118
+
119
+ let messageBody;
120
+ messageBody = dlqRecord;
121
+ let metadata = extractSQSMetadata(messageBody);
122
+ /* de-nest sqs records of unknown depth */
123
+ while (isSQSRecordLike(messageBody)) {
124
+ /* prefer outermost recorded metadata */
125
+ metadata = { ...extractSQSMetadata(messageBody), ...metadata };
126
+ messageBody = parseSQSMessageBody(messageBody);
127
+ }
128
+ const error = 'error' in metadata ? metadata.error : null;
129
+ if (isEventBridgeEvent(messageBody)) {
130
+ executionArn = messageBody?.detail?.executionArn || null;
131
+ stateMachineArn = messageBody?.detail?.stateMachineArn || null;
132
+ status = messageBody?.detail?.status || null;
133
+ time = messageBody?.time || null;
134
+ let cumulusMessage;
135
+ try {
136
+ cumulusMessage = await getCumulusMessageFromExecutionEvent(messageBody);
137
+ } catch (error_) {
138
+ cumulusMessage = undefined;
139
+ log.error(
140
+ 'could not parse details from DLQ message body',
141
+ error_,
142
+ messageBody
143
+ );
144
+ }
145
+ if (cumulusMessage) {
146
+ collectionId = extractCollectionId(cumulusMessage);
147
+ granules = extractGranules(cumulusMessage);
148
+ if (isMessageWithProvider(cumulusMessage)) {
149
+ providerId = getMessageProviderId(cumulusMessage) || null;
150
+ }
151
+ }
152
+ } else {
153
+ log.error(
154
+ 'could not parse details from DLQ message body',
155
+ messageBody,
156
+ 'expected EventBridgeEvent'
157
+ );
158
+ }
159
+ return {
160
+ ...metadata,
161
+ body: JSON.stringify(messageBody),
162
+ collectionId,
163
+ providerId,
164
+ granules,
165
+ executionArn,
166
+ stateMachineArn,
167
+ status,
168
+ time,
169
+ error,
170
+ } as DLARecord; // cast to DLARecord: ts is confused by explicit 'undefined' fields in metadata
171
+ };
172
+
173
+ export const getDLARootKey = (stackName: string) => (
174
+ `${stackName}/dead-letter-archive/sqs/`
175
+ );
176
+
177
+ export const extractDateString = (message: DLARecord): string => (
178
+ message.time && moment.utc(message.time).isValid() ? moment.utc(message.time).format('YYYY-MM-DD') : moment.utc().format('YYYY-MM-DD')
179
+ );
180
+
181
+ export const extractFileName = (message: DLARecord): string => {
182
+ // get token after the last / or :
183
+ const executionName = message.executionArn ? message.executionArn.split(/[/:]/).pop() : 'unknown';
184
+ return `${executionName}-${uuid()}`;
185
+ };
186
+
187
+ export const getDLAKey = (stackName: string, message: DLARecord): string => {
188
+ const dateString = extractDateString(message);
189
+ const fileName = extractFileName(message);
190
+ return `${getDLARootKey(stackName)}${dateString}/${fileName}`;
191
+ };
192
+
193
+ export const getDLAFailureKey = (stackName: string, message: DLARecord): string => {
194
+ const dateString = extractDateString(message);
195
+ const fileName = extractFileName(message);
196
+ return `${stackName}/dead-letter-archive/failed-sqs/${dateString}/${fileName}`;
197
+ };
package/src/Providers.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Message } from '@cumulus/types';
2
-
2
+ import { isCumulusMessageLike } from './CumulusMessage';
3
3
  type MessageProvider = {
4
4
  id: string
5
5
  protocol: string
@@ -7,12 +7,29 @@ type MessageProvider = {
7
7
  port?: number
8
8
  };
9
9
 
10
+ export const isMessageProvider = (
11
+ obj: any
12
+ ): obj is MessageProvider => (
13
+ obj instanceof Object
14
+ // in testing instanceof String can return false for string literals
15
+ && 'id' in obj && (typeof (obj.id) === 'string' || obj.id instanceof String)
16
+ && 'protocol' in obj && (typeof (obj.protocol) === 'string' || obj.protocol instanceof String)
17
+ && 'host' in obj && (typeof (obj.host) === 'string' || obj.host instanceof String)
18
+ && ('port' in obj ? (typeof (obj.port) === 'number' || obj.port instanceof Number) : true)
19
+ );
20
+
10
21
  type MessageWithProvider = Message.CumulusMessage & {
11
22
  meta: {
12
23
  provider?: MessageProvider
13
24
  }
14
25
  };
15
26
 
27
+ export const isMessageWithProvider = (
28
+ obj: any
29
+ ): obj is MessageWithProvider => (
30
+ isCumulusMessageLike(obj)
31
+ && isMessageProvider(obj?.meta?.provider)
32
+ );
16
33
  /**
17
34
  * Get the provider from a workflow message, if any.
18
35
  *
@@ -2,10 +2,10 @@
2
2
 
3
3
  /**
4
4
  * Utility functions for working with AWS Step Function events/messages
5
- * @module StepFunctions
6
5
  *
6
+ * @module StepFunctions
7
7
  * @example
8
- * const StepFunctions = require('@cumulus/message/StepFunctions');
8
+ * const StepFunctions = require('@cumulus/message/StepFunctions');
9
9
  */
10
10
 
11
11
  import { EventBridgeEvent } from 'aws-lambda';
@@ -13,7 +13,7 @@ import { JSONPath } from 'jsonpath-plus';
13
13
  import get from 'lodash/get';
14
14
  import set from 'lodash/set';
15
15
 
16
- import { getExecutionHistory } from '@cumulus/aws-client/StepFunctions';
16
+ import { getExecutionHistory, HistoryEvent } from '@cumulus/aws-client/StepFunctions';
17
17
  import { getStepExitedEvent, getTaskExitedEventOutput } from '@cumulus/common/execution-history';
18
18
  import { Message } from '@cumulus/types';
19
19
  import * as s3Utils from '@cumulus/aws-client/S3';
@@ -49,9 +49,9 @@ const executionStatusToWorkflowStatus = (
49
49
  * of S3 remote message
50
50
  *
51
51
  * @param {Message.CumulusRemoteMessage} event - Source event
52
- * @returns {Promise<Object>} Updated event with target path replaced by remote message
52
+ * @param event.replace - Cumulus message replace config
53
+ * @returns {Promise<object>} Updated event with target path replaced by remote message
53
54
  * @throws {Error} if target path cannot be found on source event
54
- *
55
55
  * @async
56
56
  * @alias module:StepFunctions
57
57
  */
@@ -92,8 +92,7 @@ export const pullStepFunctionEvent = async (
92
92
  *
93
93
  * @param {CMAMessage} stepMessage - Message for the step
94
94
  * @param {string} stepName - Name of the step
95
- * @returns {Promise<Object>} Parsed and updated event with target path replaced by remote message
96
- *
95
+ * @returns {Promise<object>} Parsed and updated event with target path replaced by remote message
97
96
  * @async
98
97
  * @alias module:StepFunctions
99
98
  */
@@ -123,17 +122,18 @@ export const parseStepMessage = async (
123
122
  * Searches the Execution step History for the TaskStateEntered pertaining to
124
123
  * the failed task Id. HistoryEvent ids are numbered sequentially, starting at
125
124
  * one.
126
- *
125
+ *
127
126
  * @param {HistoryEvent[]} events - Step Function events array
128
- * @param {HistoryEvent} failedStepEvent - Step Function's failed event.
127
+ * @param {failedStepEvent} failedStepEvent - Step Function's failed event.
128
+ * @param failedStepEvent.id - number (long), Step Functions failed event id.
129
129
  * @returns {string} name of the current stepfunction task or 'UnknownFailedStepName'.
130
130
  */
131
131
  export const getFailedStepName = (
132
- events: AWS.StepFunctions.HistoryEvent[],
133
- failedStepEvent: { id: number }
132
+ events: HistoryEvent[],
133
+ failedStepEvent: HistoryEvent
134
134
  ) => {
135
135
  try {
136
- const previousEvents = events.slice(0, failedStepEvent.id - 1);
136
+ const previousEvents = events.slice(0, failedStepEvent.id as number - 1);
137
137
  const startEvents = previousEvents.filter(
138
138
  (e) => e.type === 'TaskStateEntered'
139
139
  );
@@ -150,15 +150,15 @@ export const getFailedStepName = (
150
150
  /**
151
151
  * Finds all failed execution events and returns the last one in the list.
152
152
  *
153
- * @param {Array<HistoryEventList>} events - array of AWS Stepfunction execution HistoryEvents
154
- * @returns {HistoryEventList | undefined} - the last lambda or activity that failed in the
153
+ * @param {HistoryEvent[]} events - array of AWS Stepfunction execution HistoryEvents
154
+ * @returns {HistoryEvent[] | undefined} - the last lambda or activity that failed in the
155
155
  * event array, or an empty array.
156
156
  */
157
157
  export const lastFailedEventStep = (
158
- events: AWS.StepFunctions.HistoryEvent[]
159
- ): AWS.StepFunctions.HistoryEvent | undefined => {
158
+ events: HistoryEvent[]
159
+ ): HistoryEvent | undefined => {
160
160
  const failures = events.filter((event) =>
161
- ['LambdaFunctionFailed', 'ActivityFailed'].includes(event.type));
161
+ ['LambdaFunctionFailed', 'ActivityFailed'].includes(event.type as string));
162
162
  return failures.pop();
163
163
  };
164
164