This repository has been archived by the owner on Dec 10, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathindex.js
288 lines (232 loc) · 8.58 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
/**
* AWS Lambda function to send SNS notifications to a Slack channel.
* Based on a gist by Joseph Terranova.
*
* @author Joseph Terranova
* @author Tim Malone <[email protected]>
* @see https://gist.github.com/terranware/962da63ca547f55667f6
*/
'use strict';
const https = require( 'https' ),
globby = require( 'globby' ),
isPlainObj = require( 'is-plain-obj' );
/* eslint-disable no-process-env */
const DEBUG = 'true' === process.env.DEBUG,
SLACK_HOOK = process.env.SLACK_HOOK;
/* eslint-enable no-process-env */
const INDEX_OF_NOT_PRESENT = -1,
FIRST_ITEM = 0,
FIRST_MATCH = 1,
ONE_PROPERTY = 1,
JSON_SPACES = 2,
SLACK_COLUMN_LENGTH = 28;
const dangerMessages = [
' but with errors',
' to RED',
'During an aborted deployment',
'Failed to deploy application',
'Failed to deploy configuration',
'has a dependent object',
'is not authorized to perform',
'Pending to Degraded',
'Stack deletion failed',
'Unsuccessful command execution',
'You do not have permission',
'Your quota allows for 0 more running instance',
' has reached the build status of \'FAILED\''
];
const warningMessages = [
' aborted operation.',
' to YELLOW',
'Adding instance ',
'Degraded to Info',
'Deleting SNS topic',
'is currently running under desired capacity',
'Ok to Info',
'Ok to Warning',
'Pending Initialization',
'Removed instance ',
'Rollback of environment',
' has reached the build status of \'IN_PROGRESS\''
];
exports.handler = ( event, context, callback ) => {
if ( DEBUG ) console.log( JSON.stringify( event, null, JSON_SPACES ) );
const sns = event.Records[ FIRST_ITEM ].Sns,
arn = sns.TopicArn,
topicName = getNameFromArn( arn );
console.log( 'From SNS:', sns );
const slackMessage = {
text: sns.Subject ? '*' + sns.Subject + '*' : '',
username: topicName || arn
};
if ( DEBUG ) slackMessage.text += '\n' + JSON.stringify( event, null, JSON_SPACES );
const attachment = {
color: getColorBySeverity( sns.Message ),
text: sns.Message,
footer: ( sns.UnsubscribeUrl ? '<' + sns.UnsubscribeUrl + '|Unsubscribe>' : '' )
};
// If we can format our message into fields, do that instead of printing it as text.
attachment.text = maybeGetAttachmentFields( sns.Message );
if ( 'object' === typeof attachment.text ) {
attachment.fields = attachment.text;
attachment.text = '';
}
// Trim quotes, in case we've ended up with a JSON string.
if ( 'string' === typeof attachment.text ) {
attachment.text = attachment.text.replace( /(^"|"$)/g, '' );
}
slackMessage.attachments = [ attachment ];
sendToSlack( slackMessage, callback );
}; // Exports.handler.
/**
* Sends a message to Slack, calling a callback when complete or throwing on error.
*
* @param {object} message The message to send.
* @param {function} callback A callback to call on completion.
* @returns {undefined}
* @see https://api.slack.com/docs/messages
* @see https://api.slack.com/incoming-webhooks
*/
function sendToSlack( message, callback ) {
const options = {
method: 'POST',
hostname: 'hooks.slack.com',
port: 443,
path: '/services/' + SLACK_HOOK
};
if ( DEBUG ) console.log( options );
const request = https.request( options, ( response ) => {
let body = '';
response.setEncoding( 'utf8' );
response.on( 'data', ( chunk ) => {
body += chunk;
}).on( 'end', () => {
console.log( 'Response from Slack: ' + body );
callback( null, body );
});
});
request.on( 'error', ( error ) => {
throw Error( 'Problem with Slack request: ' + error.message );
});
request.write( JSON.stringify( message ) );
request.end();
} // Function sendToSlack.
/**
* Given a standard ARN of an SNS topic, attempts to return just the name of the topic.
*
* @param {string} arn A full Amazon Resource Name for an SNS topic.
* @returns {string|boolean} Either the topic name, or false if it could not be determined.
*/
function getNameFromArn( arn ) {
try {
return arn.match( /\d\d\d:(.*)$/ )[ FIRST_MATCH ];
} catch ( error ) {
return false;
}
} // Function getNameFromArn.
/**
* Given message text from an SNS notification, attempts to determine the severity of the
* notification and return an appropriate colour setting for a Slack message attachment.
*
* @param {string} text The message text supplied by the incoming SNS notification.
* @returns {string} A valid Slack colour setting: 'danger', 'warning', 'good', or possibly a hex
* code.
* @see https://api.slack.com/docs/message-attachments#color
*/
function getColorBySeverity( text ) {
for ( const dangerMessagesItem in dangerMessages ) {
if ( INDEX_OF_NOT_PRESENT !== text.indexOf( dangerMessages[dangerMessagesItem]) ) {
return 'danger';
}
}
for ( const warningMessagesItem in warningMessages ) {
if ( INDEX_OF_NOT_PRESENT !== text.indexOf( warningMessages[warningMessagesItem]) ) {
return 'warning';
}
}
return 'good';
} // Function getColorBySeverity.
/**
* Given an incoming SNS message in either JSON or form data, attempts to convert it into Slack
* message attachment fields. This improves readability - it's much easier for a human to decipher!
*
* @param {string} text An incoming SNS message.
* @returns {array|string} An array of Slack attachment fields if possible; otherwise a string to
* be used as the message text. The string will either be the same as the
* input, or reduced down to a string if it is the only property in an
* object.
* @see https://api.slack.com/docs/message-attachments#fields
*/
function maybeGetAttachmentFields( text ) {
const fields = [];
let data = '';
// - Try first to parse JSON.
// - If we can't do that, try to parse a query string (+ force it into the same object prototype).
// - If we can't do that, just return the text we end up with.
//
// Note that even with a single string, most of the time the querystring parser will probably not
// fail, and we'll end up with a one property JSON with the string as the key and the value
// blank. That's not ideal, but we'll check for it when we try to narrow down to a single
// property.
try {
data = JSON.parse( text );
} catch ( error ) {
try {
const querystring = require( 'querystring' );
data = JSON.parse( JSON.stringify( querystring.parse( text ) ) );
} catch ( error ) {
return text;
}
}
// We don't want to try splitting up a number, array or string.
if ( ! isPlainObj( data ) ) {
return text;
}
// Apply any registered filters in ./filters/.
data = applyParsingFilters( data );
// If we only have one property, jump down a level and use that instead. We also need to check
// that we have an array or an object too, and that the one property has a value. This last check
// is important - because of our sorta querystring hack above we'll often end up with a JSON key
// with no value if the message was a simple string!
while (
'object' === typeof data &&
ONE_PROPERTY === Object.keys( data ).length &&
data[ Object.keys( data ).shift() ]
) {
data = data[ Object.keys( data ).shift() ];
}
// And, in case we haven't ended up with an object...
if ( ! isPlainObj( data ) ) {
return JSON.stringify( data );
}
// Turn all remaining properties into fields.
Object.keys( data ).forEach( ( key ) => {
const value = 'string' === typeof data[key] ? data[key] : JSON.stringify( data[key]);
// In most cases, show the key as the title and the value as the value. But if we have a key
// only, we'll use that as the value instead.
fields.push({
title: value ? key : '',
value: value ? value : key,
short: value ? value.length <= SLACK_COLUMN_LENGTH : key.length <= SLACK_COLUMN_LENGTH
});
});
return fields;
} // Function maybeGetAttachmentFields.
/**
* Given an input, runs it through any supplied filter functions and returns the resulting output.
*
* @param {object} input The input object.
* @returns {object} output The resulting object, after being passed through all filters.
*/
function applyParsingFilters( input ) {
const parsingFilters = globby.sync( 'filters/**/*.js' );
let output = input;
parsingFilters.forEach( ( filter ) => {
output = require( filter )( output );
});
return output;
} // Function applyParsingFilters.
// Export functions for unit testing.
exports.getNameFromArn = getNameFromArn;
exports.getColorBySeverity = getColorBySeverity;
exports.maybeGetAttachmentFields = maybeGetAttachmentFields;