1 |
|
2 | const cf = require('@mapbox/cloudfriend');
|
3 |
|
4 | module.exports.build = build;
|
5 | function build(params) {
|
6 | const functions = new Set([
|
7 | 'SnsSubscription',
|
8 | 'DynamoDBStreamLabel',
|
9 | 'StackOutputs',
|
10 | 'SpotFleet',
|
11 | 'DefaultVpc',
|
12 | 'S3NotificationTopicConfig',
|
13 | 'S3Inventory'
|
14 | ]);
|
15 |
|
16 | if(!params.CustomResourceName)
|
17 | throw new Error('Missing CustomResourceName');
|
18 | if (!functions.has(params.CustomResourceName))
|
19 | throw new Error(`${params.CustomResourceName} is not an available magical-cfn-resource`);
|
20 | if (!params.LogicalName)
|
21 | throw new Error('Missing LogicalName');
|
22 | if (!params.S3Bucket)
|
23 | throw new Error('Missing S3Bucket');
|
24 | if (!params.S3Key)
|
25 | throw new Error('Missing S3Key');
|
26 | if (!params.Handler)
|
27 | throw new Error('Missing Handler');
|
28 | if (!params.Properties)
|
29 | throw new Error('Missing Properties');
|
30 | if (params.CustomResourceName === 'SpotFleet') {
|
31 | if (!params.Properties.SpotFleetRequestConfigData)
|
32 | throw new Error('Missing SpotFleetRequestConfigData');
|
33 | if (!params.Properties.SpotFleetRequestConfigData.LaunchSpecifications)
|
34 | throw new Error('Missing LaunchSpecifications in SpotFleetRequestConfigData');
|
35 | if (!params.Properties.SpotFleetRequestConfigData.LaunchSpecifications[0])
|
36 | throw new Error('Missing LaunchSpecifications[0] in SpotFleetRequestConfigData');
|
37 | if (!params.Properties.SpotFleetRequestConfigData.LaunchSpecifications[0].IamInstanceProfile)
|
38 | throw new Error('Missing IamInstanceProfile in SpotFleetRequestConfigData.LaunchSpecifications[0]');
|
39 | if (!params.Properties.SpotFleetRequestConfigData.IamFleetRole)
|
40 | throw new Error('Missing IamFleetRole in SpotFleetRequestConfigData');
|
41 | }
|
42 |
|
43 | const CustomResource = {};
|
44 |
|
45 | CustomResource[`${params.LogicalName}Role`] = role(params);
|
46 |
|
47 |
|
48 | moreResources(params, CustomResource);
|
49 |
|
50 | CustomResource[`${params.LogicalName}Function`] = {
|
51 | Type: 'AWS::Lambda::Function',
|
52 |
|
53 | Properties: {
|
54 |
|
55 |
|
56 | Code: {
|
57 | S3Bucket: params.S3Bucket,
|
58 | S3Key: params.S3Key
|
59 | },
|
60 |
|
61 | Role: cf.getAtt(`${params.LogicalName}Role`, 'Arn'),
|
62 |
|
63 |
|
64 | Description: `Manages ${params.CustomResourceName}`,
|
65 | Handler: params.Handler,
|
66 | MemorySize: 128,
|
67 | Runtime: 'nodejs16.x',
|
68 | Timeout: 30
|
69 | }
|
70 | };
|
71 | CustomResource[params.LogicalName] = {
|
72 | Type: 'Custom::'+params.CustomResourceName,
|
73 | Properties: Object.assign({ ServiceToken: cf.getAtt(`${params.LogicalName}Function`, 'Arn')}, params.Properties)
|
74 | };
|
75 |
|
76 | if (params.Condition) {
|
77 | CustomResource[`${params.LogicalName}Role`]['Condition'] = params.Condition;
|
78 | CustomResource[`${params.LogicalName}Function`]['Condition'] = params.Condition;
|
79 | CustomResource[params.LogicalName]['Condition'] = params.Condition;
|
80 | }
|
81 |
|
82 | return { Resources: CustomResource };
|
83 | }
|
84 |
|
85 | function role(params) {
|
86 | switch (params.CustomResourceName) {
|
87 | case 'SnsSubscription':
|
88 | return {
|
89 | Type: 'AWS::IAM::Role',
|
90 | Properties: {
|
91 |
|
92 |
|
93 | AssumeRolePolicyDocument: {
|
94 | Statement: [
|
95 | {
|
96 | Sid: '',
|
97 | Effect: 'Allow',
|
98 | Principal: {
|
99 | Service: 'lambda.amazonaws.com'
|
100 | },
|
101 | Action: 'sts:AssumeRole'
|
102 | }
|
103 | ]
|
104 | },
|
105 | Policies: [
|
106 | {
|
107 |
|
108 |
|
109 |
|
110 | PolicyName: 'SnsSubscriptionPolicy',
|
111 | PolicyDocument: {
|
112 | Statement: [
|
113 |
|
114 | {
|
115 | Effect: 'Allow',
|
116 | Action: ['logs:*'],
|
117 | Resource: cf.sub('arn:${AWS::Partition}:logs:*:*:*')
|
118 | },
|
119 |
|
120 |
|
121 | {
|
122 | Effect: 'Allow',
|
123 | Action: [
|
124 | 'sns:Subscribe',
|
125 | 'sns:Unsubscribe',
|
126 | 'sns:ListSubscriptionsByTopic'
|
127 | ],
|
128 | Resource: '*'
|
129 | }
|
130 | ]
|
131 | }
|
132 | }
|
133 | ]
|
134 | }
|
135 | };
|
136 | case 'DynamoDBStreamLabel':
|
137 | return {
|
138 | Type: 'AWS::IAM::Role',
|
139 | Properties: {
|
140 |
|
141 |
|
142 | AssumeRolePolicyDocument: {
|
143 | Statement: [
|
144 | {
|
145 | Sid: '',
|
146 | Effect: 'Allow',
|
147 | Principal: { Service: 'lambda.amazonaws.com' },
|
148 | Action: 'sts:AssumeRole'
|
149 | }
|
150 | ]
|
151 | },
|
152 | Policies: [
|
153 | {
|
154 |
|
155 |
|
156 |
|
157 | PolicyName: 'DynamoDBStreamLabelPolicy',
|
158 | PolicyDocument: {
|
159 | Statement: [
|
160 |
|
161 | {
|
162 | Effect: 'Allow',
|
163 | Action: ['logs:*'],
|
164 | Resource: cf.sub('arn:${AWS::Partition}:logs:*:*:*')
|
165 | },
|
166 |
|
167 | {
|
168 | Effect: 'Allow',
|
169 | Action: [
|
170 | 'dynamodb:DescribeTable'
|
171 | ],
|
172 | Resource: '*'
|
173 | }
|
174 | ]
|
175 | }
|
176 | }
|
177 | ]
|
178 | }
|
179 | };
|
180 | case 'StackOutputs':
|
181 | return {
|
182 | Type: 'AWS::IAM::Role',
|
183 | Properties: {
|
184 |
|
185 |
|
186 | AssumeRolePolicyDocument: {
|
187 | Statement: [
|
188 | {
|
189 | Sid: '',
|
190 | Effect: 'Allow',
|
191 | Principal: {
|
192 | Service: 'lambda.amazonaws.com'
|
193 | },
|
194 | Action: 'sts:AssumeRole'
|
195 | }
|
196 | ]
|
197 | },
|
198 | Policies: [
|
199 | {
|
200 |
|
201 |
|
202 |
|
203 | PolicyName: 'LogGroupPolicy',
|
204 | PolicyDocument: {
|
205 | Statement: [
|
206 |
|
207 | {
|
208 | Effect: 'Allow',
|
209 | Action: ['logs:*'],
|
210 | Resource: cf.sub('arn:${AWS::Partition}:logs:*:*:*')
|
211 | },
|
212 |
|
213 | {
|
214 | Effect: 'Allow',
|
215 | Action: ['cloudformation:DescribeStacks'],
|
216 | Resource: '*'
|
217 | }
|
218 | ]
|
219 | }
|
220 | }
|
221 | ]
|
222 | }
|
223 |
|
224 | };
|
225 | case 'S3NotificationTopicConfig':
|
226 | return {
|
227 | Type: 'AWS::IAM::Role',
|
228 | Properties: {
|
229 |
|
230 |
|
231 | AssumeRolePolicyDocument: {
|
232 | Statement: [
|
233 | {
|
234 | Sid: '',
|
235 | Effect: 'Allow',
|
236 | Principal: {
|
237 | Service: 'lambda.amazonaws.com'
|
238 | },
|
239 | Action: 'sts:AssumeRole'
|
240 | }
|
241 | ]
|
242 | },
|
243 | Policies: [
|
244 | {
|
245 |
|
246 |
|
247 |
|
248 | PolicyName: 'S3NotificationTopicConfigPolicy',
|
249 | PolicyDocument: {
|
250 | Statement: [
|
251 |
|
252 | {
|
253 | Effect: 'Allow',
|
254 | Action: ['logs:*'],
|
255 | Resource: cf.sub('arn:${AWS::Partition}:logs:*:*:*')
|
256 | },
|
257 |
|
258 | {
|
259 | Effect: 'Allow',
|
260 | Action: ['s3:getBucketNotification', 's3:putBucketNotification'],
|
261 | Resource: params.Properties.BucketNotificationResources || '*'
|
262 | }
|
263 | ]
|
264 | }
|
265 | }
|
266 | ]
|
267 | }
|
268 | }
|
269 | case 'SpotFleet':
|
270 | return {
|
271 | Type: 'AWS::IAM::Role',
|
272 | Properties: {
|
273 |
|
274 |
|
275 | AssumeRolePolicyDocument: {
|
276 | Statement: [
|
277 | {
|
278 | Sid: '',
|
279 | Effect: 'Allow',
|
280 | Principal: {
|
281 | Service: 'lambda.amazonaws.com'
|
282 | },
|
283 | Action: 'sts:AssumeRole'
|
284 | }
|
285 | ]
|
286 | },
|
287 | Policies: [
|
288 | {
|
289 |
|
290 |
|
291 |
|
292 | PolicyName: 'SpotFleetPolicy',
|
293 | PolicyDocument: {
|
294 | Statement: [
|
295 |
|
296 | {
|
297 | Effect: 'Allow',
|
298 | Action: ['logs:*'],
|
299 | Resource: cf.sub('arn:${AWS::Partition}:logs:*:*:*')
|
300 | },
|
301 |
|
302 | {
|
303 | Effect: 'Allow',
|
304 | Action: ['ec2:*'],
|
305 | Resource: '*'
|
306 | },
|
307 | {
|
308 | Effect: 'Allow',
|
309 | Action: [
|
310 | 'iam:ListRoles',
|
311 | 'iam:PassRole'
|
312 | ],
|
313 | Resource: params.Properties.SpotFleetRequestConfigData.IamFleetRole
|
314 | },
|
315 | {
|
316 | Effect: 'Allow',
|
317 | Action: ['iam:ListInstanceProfiles'],
|
318 | Resource: params.Properties.SpotFleetRequestConfigData.LaunchSpecifications[0].IamInstanceProfile.Arn
|
319 | }
|
320 | ]
|
321 | }
|
322 | }
|
323 | ]
|
324 | }
|
325 | };
|
326 | case 'DefaultVpc':
|
327 | return {
|
328 | Type: 'AWS::IAM::Role',
|
329 | Properties: {
|
330 | AssumeRolePolicyDocument: {
|
331 | Statement: [
|
332 | {
|
333 | Sid: '',
|
334 | Effect: 'Allow',
|
335 | Principal: { Service: 'lambda.amazonaws.com' },
|
336 | Action: 'sts:AssumeRole'
|
337 | }
|
338 | ]
|
339 | },
|
340 | Policies: [
|
341 | {
|
342 | PolicyName: 'DefaultVpcPolicy',
|
343 | PolicyDocument: {
|
344 | Statement: [
|
345 | {
|
346 | Effect: 'Allow',
|
347 | Action: ['logs:*'],
|
348 | Resource: cf.sub('arn:${AWS::Partition}:logs:*:*:*')
|
349 | },
|
350 | {
|
351 | Effect: 'Allow',
|
352 | Action: [
|
353 | 'ec2:describeVpcs',
|
354 | 'ec2:describeSubnets',
|
355 | 'ec2:describeRouteTables'
|
356 | ],
|
357 | Resource: '*'
|
358 | }
|
359 | ]
|
360 | }
|
361 | }
|
362 | ]
|
363 | }
|
364 | };
|
365 | case 'S3Inventory':
|
366 | return {
|
367 | Type: 'AWS::IAM::Role',
|
368 | Properties: {
|
369 | AssumeRolePolicyDocument: {
|
370 | Statement: [
|
371 | {
|
372 | Effect: 'Allow',
|
373 | Principal: { Service: 'lambda.amazonaws.com' },
|
374 | Action: 'sts:AssumeRole'
|
375 | }
|
376 | ]
|
377 | },
|
378 | Policies: [
|
379 | {
|
380 | PolicyName: 'S3InventoryPolicy',
|
381 | PolicyDocument: {
|
382 | Statement: [
|
383 | {
|
384 | Effect: 'Allow',
|
385 | Action: ['logs:*'],
|
386 | Resource: cf.sub('arn:${AWS::Partition}:logs:*:*:*')
|
387 | },
|
388 | {
|
389 | Effect: 'Allow',
|
390 | Action: [
|
391 | 's3:PutInventoryConfiguration',
|
392 | 's3:DeleteInventoryConfiguration'
|
393 | ],
|
394 | Resource: '*'
|
395 | }
|
396 | ]
|
397 | }
|
398 | }
|
399 | ]
|
400 | }
|
401 | }
|
402 | default:
|
403 | throw new Error('Unknown resource name');
|
404 | }
|
405 | }
|
406 |
|
407 | function moreResources(params, customResource) {
|
408 | switch (params.CustomResourceName) {
|
409 | case 'S3NotificationTopicConfig':
|
410 | customResource[`${params.LogicalName}SnsPolicy`] = {
|
411 | Type: 'AWS::SNS::TopicPolicy',
|
412 | Properties: {
|
413 | PolicyDocument: {
|
414 | Id: `${params.LogicalName}SnsPolicy`,
|
415 | Version: '2012-10-17',
|
416 | Statement: [
|
417 | {
|
418 | Sid: '',
|
419 | Effect: 'Allow',
|
420 | Principal: { Service: 's3.amazonaws.com'},
|
421 | Action: 'SNS:Publish',
|
422 | Resource: params.Properties.SnsTopicArn,
|
423 | Condition: {
|
424 | ArnLike: { 'aws:SourceArn': cf.sub('arn:${AWS::Partition}:s3:::${bucket}', { bucket: params.Properties.Bucket }) }
|
425 | }
|
426 | }
|
427 | ]
|
428 | },
|
429 | Topics: [ params.Properties.SnsTopicArn ]
|
430 | }
|
431 | }
|
432 | break;
|
433 | default:
|
434 | break;
|
435 | }
|
436 | }
|