@fishawack/lab-env 5.7.0-beta.1 → 5.7.0-beta.3
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.
- package/CHANGELOG.md +58 -0
- package/_Test/provision.js +4 -9
- package/_Test/prune.js +8 -5
- package/bitbucket-pipelines.yml +2 -2
- package/cli.js +1 -0
- package/commands/create/cmds/dekey.js +13 -9
- package/commands/create/cmds/deprovision.js +36 -0
- package/commands/create/cmds/key.js +60 -41
- package/commands/create/cmds/provision.js +573 -69
- package/commands/create/cmds/rekey.js +218 -0
- package/commands/create/cmds/sync-secrets.js +2 -1
- package/commands/create/libs/output-credentials.js +59 -0
- package/commands/create/libs/parallel-runner.js +204 -0
- package/commands/create/libs/resolve-operator.js +59 -0
- package/commands/create/libs/utilities.js +73 -8
- package/commands/create/libs/vars.js +120 -136
- package/commands/create/services/aws/acm.js +61 -0
- package/commands/create/services/aws/cloudfront.js +51 -45
- package/commands/create/services/aws/ec2.js +675 -48
- package/commands/create/services/aws/elasticache.js +159 -0
- package/commands/create/services/aws/elasticbeanstalk.js +154 -57
- package/commands/create/services/aws/iam.js +402 -82
- package/commands/create/services/aws/index.js +1398 -260
- package/commands/create/services/aws/opensearch.js +155 -0
- package/commands/create/services/aws/rds.js +57 -31
- package/commands/create/services/aws/s3.js +52 -31
- package/commands/create/services/aws/secretsmanager.js +21 -12
- package/commands/create/services/aws/ses.js +195 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/cron.config +45 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/queue-worker.config +29 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/software.config +12 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/misc/setvars.config +2 -16
- package/commands/create/templates/elasticbeanstalk/.platform/confighooks/postdeploy/rewrite-flush.sh +1 -1
- package/commands/create/templates/elasticbeanstalk/.platform/confighooks/postdeploy/setvars.sh +19 -0
- package/commands/create/templates/elasticbeanstalk/.platform/hooks/postdeploy/restart_queue.sh +4 -0
- package/commands/create/templates/elasticbeanstalk/.platform/hooks/prebuild/setvars.sh +19 -0
- package/package.json +7 -2
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const md5 = require("apache-md5");
|
|
2
|
-
const fs = require("fs-extra");
|
|
3
1
|
const generator = require("generate-password");
|
|
4
2
|
const { S3Client, ListBucketsCommand } = require("@aws-sdk/client-s3");
|
|
5
3
|
const {
|
|
@@ -10,9 +8,17 @@ const {
|
|
|
10
8
|
SecretsManagerClient,
|
|
11
9
|
DescribeSecretCommand,
|
|
12
10
|
} = require("@aws-sdk/client-secrets-manager");
|
|
11
|
+
const {
|
|
12
|
+
OpenSearchClient,
|
|
13
|
+
DescribeDomainCommand,
|
|
14
|
+
} = require("@aws-sdk/client-opensearch");
|
|
15
|
+
const {
|
|
16
|
+
ElastiCacheClient,
|
|
17
|
+
DescribeReplicationGroupsCommand,
|
|
18
|
+
} = require("@aws-sdk/client-elasticache");
|
|
13
19
|
|
|
14
|
-
const { nameSafe, Spinner } = require("../../libs/utilities.js");
|
|
15
|
-
const { eb } = require("../../libs/vars.js");
|
|
20
|
+
const { nameSafe, Spinner, pollAll } = require("../../libs/utilities.js");
|
|
21
|
+
const { eb, secretGroups } = require("../../libs/vars.js");
|
|
16
22
|
|
|
17
23
|
module.exports.s3 = require("./s3.js");
|
|
18
24
|
module.exports.cloudfront = require("./cloudfront.js");
|
|
@@ -20,7 +26,11 @@ module.exports.iam = require("./iam.js");
|
|
|
20
26
|
module.exports.elasticbeanstalk = require("./elasticbeanstalk.js");
|
|
21
27
|
module.exports.ec2 = require("./ec2.js");
|
|
22
28
|
module.exports.rds = require("./rds.js");
|
|
29
|
+
module.exports.opensearch = require("./opensearch.js");
|
|
30
|
+
module.exports.elasticache = require("./elasticache.js");
|
|
23
31
|
module.exports.secretsmanager = require("./secretsmanager.js");
|
|
32
|
+
module.exports.acm = require("./acm.js");
|
|
33
|
+
module.exports.ses = require("./ses.js");
|
|
24
34
|
|
|
25
35
|
module.exports.slug = (repo, client, branch, service = "s3") =>
|
|
26
36
|
nameSafe(`${branch}-${repo}-${client}`, service);
|
|
@@ -29,13 +39,27 @@ module.exports.fullstackPreflight = async (
|
|
|
29
39
|
name,
|
|
30
40
|
repo,
|
|
31
41
|
branch,
|
|
32
|
-
{
|
|
42
|
+
{
|
|
43
|
+
storage = false,
|
|
44
|
+
database = false,
|
|
45
|
+
opensearch = false,
|
|
46
|
+
cache = false,
|
|
47
|
+
} = {},
|
|
33
48
|
) => {
|
|
34
|
-
const conflicts = [];
|
|
35
49
|
const existing = {
|
|
50
|
+
environment: false,
|
|
36
51
|
storage: false,
|
|
37
52
|
database: false,
|
|
38
|
-
|
|
53
|
+
opensearch: false,
|
|
54
|
+
cache: false,
|
|
55
|
+
secrets: {
|
|
56
|
+
app: null,
|
|
57
|
+
storage: null,
|
|
58
|
+
database: null,
|
|
59
|
+
opensearch: null,
|
|
60
|
+
cache: null,
|
|
61
|
+
mail: null,
|
|
62
|
+
},
|
|
39
63
|
};
|
|
40
64
|
|
|
41
65
|
const spinner = new Spinner("Running pre-flight checks", true);
|
|
@@ -50,9 +74,7 @@ module.exports.fullstackPreflight = async (
|
|
|
50
74
|
(e) => e.Status !== "Terminated",
|
|
51
75
|
);
|
|
52
76
|
if (active.length) {
|
|
53
|
-
|
|
54
|
-
`Elastic Beanstalk environment "${name}" already exists (status: ${active[0].Status})`,
|
|
55
|
-
);
|
|
77
|
+
existing.environment = active[0];
|
|
56
78
|
}
|
|
57
79
|
} catch {
|
|
58
80
|
/* does not exist */
|
|
@@ -105,8 +127,56 @@ module.exports.fullstackPreflight = async (
|
|
|
105
127
|
}
|
|
106
128
|
}
|
|
107
129
|
|
|
130
|
+
// Check OpenSearch
|
|
131
|
+
if (opensearch) {
|
|
132
|
+
const osSlug = module.exports.slug(
|
|
133
|
+
repo,
|
|
134
|
+
process.env.AWS_PROFILE,
|
|
135
|
+
branch,
|
|
136
|
+
"os",
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
spinner.ora.text = "Checking for existing OpenSearch domain";
|
|
140
|
+
try {
|
|
141
|
+
const { DomainStatus } = await new OpenSearchClient({}).send(
|
|
142
|
+
new DescribeDomainCommand({ DomainName: osSlug }),
|
|
143
|
+
);
|
|
144
|
+
if (DomainStatus && !DomainStatus.Deleted) {
|
|
145
|
+
existing.opensearch = true;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
/* does not exist */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check ElastiCache
|
|
153
|
+
if (cache) {
|
|
154
|
+
const cacheSlug = module.exports.slug(
|
|
155
|
+
repo,
|
|
156
|
+
process.env.AWS_PROFILE,
|
|
157
|
+
branch,
|
|
158
|
+
"cache",
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
spinner.ora.text = "Checking for existing ElastiCache cluster";
|
|
162
|
+
try {
|
|
163
|
+
const { ReplicationGroups } = await new ElastiCacheClient(
|
|
164
|
+
{},
|
|
165
|
+
).send(
|
|
166
|
+
new DescribeReplicationGroupsCommand({
|
|
167
|
+
ReplicationGroupId: cacheSlug,
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
const group = ReplicationGroups[0];
|
|
171
|
+
if (group && group.Status !== "deleting") {
|
|
172
|
+
existing.cache = true;
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
/* does not exist */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
108
179
|
// Check for existing secrets
|
|
109
|
-
const secretGroups = ["app", "storage", "database"];
|
|
110
180
|
for (const group of secretGroups) {
|
|
111
181
|
spinner.ora.text = `Checking for existing ${group} secret`;
|
|
112
182
|
try {
|
|
@@ -125,7 +195,7 @@ module.exports.fullstackPreflight = async (
|
|
|
125
195
|
throw e;
|
|
126
196
|
}
|
|
127
197
|
|
|
128
|
-
return {
|
|
198
|
+
return { existing };
|
|
129
199
|
};
|
|
130
200
|
|
|
131
201
|
module.exports.clients = [
|
|
@@ -272,140 +342,659 @@ module.exports.staticTerminate = async (name, repo, branch, id) => {
|
|
|
272
342
|
}
|
|
273
343
|
};
|
|
274
344
|
|
|
275
|
-
|
|
276
|
-
name,
|
|
277
|
-
tags = [],
|
|
278
|
-
credentials = [],
|
|
279
|
-
repo,
|
|
280
|
-
branch,
|
|
281
|
-
framework,
|
|
282
|
-
availability,
|
|
283
|
-
database = false,
|
|
284
|
-
storage = false,
|
|
285
|
-
existingSecrets = { app: null, storage: null, database: null },
|
|
286
|
-
snapshot = null,
|
|
287
|
-
) => {
|
|
288
|
-
const { platform, language } = eb.configurations[framework];
|
|
345
|
+
// Private provisioner functions for parallel execution
|
|
289
346
|
|
|
290
|
-
|
|
347
|
+
const provisionStorage = async (
|
|
348
|
+
aws,
|
|
349
|
+
s3Slug,
|
|
350
|
+
tags,
|
|
351
|
+
isLaravel,
|
|
352
|
+
existingResources,
|
|
353
|
+
) => {
|
|
291
354
|
let s3Data = {};
|
|
292
355
|
|
|
293
|
-
if (storage) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
356
|
+
if (!existingResources.storage) {
|
|
357
|
+
if (isLaravel) {
|
|
358
|
+
await aws.s3.createS3Bucket(s3Slug, tags, {
|
|
359
|
+
BlockPublicAcls: true,
|
|
360
|
+
IgnorePublicAcls: true,
|
|
361
|
+
BlockPublicPolicy: false,
|
|
362
|
+
RestrictPublicBuckets: false,
|
|
363
|
+
});
|
|
300
364
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
s3Slug,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
365
|
+
await aws.s3.setPublicReadPolicy(s3Slug, "public/*");
|
|
366
|
+
} else {
|
|
367
|
+
await aws.iam.createIAMUser(s3Slug, tags);
|
|
368
|
+
await aws.iam.attachIAMPolicy(
|
|
369
|
+
s3Slug,
|
|
370
|
+
"arn:aws:iam::aws:policy/AmazonS3FullAccess",
|
|
371
|
+
);
|
|
372
|
+
const {
|
|
373
|
+
AccessKey: { AccessKeyId },
|
|
374
|
+
AccessKey: { SecretAccessKey },
|
|
375
|
+
} = await aws.iam.createAccessKey(s3Slug);
|
|
310
376
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
377
|
+
await aws.s3.createS3Bucket(
|
|
378
|
+
s3Slug,
|
|
379
|
+
tags,
|
|
380
|
+
false,
|
|
381
|
+
"BucketOwnerPreferred",
|
|
382
|
+
);
|
|
317
383
|
|
|
318
|
-
|
|
384
|
+
s3Data = { s3Slug, AccessKeyId, SecretAccessKey };
|
|
385
|
+
}
|
|
319
386
|
}
|
|
320
387
|
|
|
321
|
-
|
|
322
|
-
|
|
388
|
+
if (!s3Data.s3Slug) {
|
|
389
|
+
s3Data = { s3Slug };
|
|
390
|
+
}
|
|
323
391
|
|
|
324
|
-
|
|
325
|
-
|
|
392
|
+
return s3Data;
|
|
393
|
+
};
|
|
326
394
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
395
|
+
const provisionRDS = async (
|
|
396
|
+
aws,
|
|
397
|
+
name,
|
|
398
|
+
repo,
|
|
399
|
+
branch,
|
|
400
|
+
tags,
|
|
401
|
+
vpcData,
|
|
402
|
+
existingResources,
|
|
403
|
+
existingSecrets,
|
|
404
|
+
snapshot,
|
|
405
|
+
) => {
|
|
406
|
+
const rdsSlug = aws.slug(repo, process.env.AWS_PROFILE, branch, "rds");
|
|
407
|
+
const rdsClient = new RDSClient({});
|
|
334
408
|
|
|
409
|
+
if (!existingResources.database) {
|
|
335
410
|
const dbPassword = generator.generate({
|
|
336
411
|
length: 10,
|
|
337
412
|
numbers: true,
|
|
338
413
|
});
|
|
339
414
|
|
|
340
|
-
await
|
|
415
|
+
await aws.rds.ensureSSLParameterGroup();
|
|
341
416
|
|
|
342
|
-
const rdsSecurityGroupId =
|
|
343
|
-
|
|
417
|
+
const rdsSecurityGroupId = await aws.ec2.createRDSSecurityGroup(
|
|
418
|
+
rdsSlug,
|
|
419
|
+
tags,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const dbSubnetGroupName = await aws.rds.ensureDBSubnetGroup(
|
|
423
|
+
vpcData.privateSubnetIds,
|
|
424
|
+
);
|
|
344
425
|
|
|
345
426
|
if (snapshot) {
|
|
346
|
-
await
|
|
427
|
+
await aws.rds.restoreRDSInstanceFromSnapshot(
|
|
347
428
|
rdsSlug,
|
|
348
429
|
snapshot,
|
|
349
430
|
rdsSecurityGroupId,
|
|
350
431
|
tags,
|
|
432
|
+
dbSubnetGroupName,
|
|
351
433
|
);
|
|
352
434
|
|
|
353
|
-
await
|
|
354
|
-
|
|
355
|
-
await module.exports.rds.modifyRDSPassword(rdsSlug, dbPassword);
|
|
435
|
+
await aws.rds.waitForRDSInstance(rdsSlug);
|
|
436
|
+
await aws.rds.modifyRDSPassword(rdsSlug, dbPassword);
|
|
356
437
|
} else {
|
|
357
|
-
await
|
|
438
|
+
await aws.rds.createRDSInstance(
|
|
358
439
|
rdsSlug,
|
|
359
440
|
tags,
|
|
360
441
|
dbPassword,
|
|
361
442
|
rdsSecurityGroupId,
|
|
443
|
+
dbSubnetGroupName,
|
|
362
444
|
);
|
|
363
445
|
}
|
|
364
446
|
|
|
365
|
-
|
|
366
|
-
|
|
447
|
+
return {
|
|
448
|
+
rdsData: { RDS_PASSWORD: dbPassword },
|
|
449
|
+
rdsSlug,
|
|
450
|
+
poll: {
|
|
451
|
+
name: "RDS",
|
|
452
|
+
cb: async () =>
|
|
453
|
+
(
|
|
454
|
+
await rdsClient.send(
|
|
455
|
+
new DescribeDBInstancesCommand({
|
|
456
|
+
DBInstanceIdentifier: rdsSlug,
|
|
457
|
+
}),
|
|
458
|
+
)
|
|
459
|
+
).DBInstances[0],
|
|
460
|
+
shouldContinue: ({ DBInstanceStatus }) =>
|
|
461
|
+
DBInstanceStatus !== "available",
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
} else if (existingResources.database && !existingSecrets.database) {
|
|
465
|
+
const dbPassword = generator.generate({
|
|
466
|
+
length: 10,
|
|
467
|
+
numbers: true,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
await aws.rds.modifyRDSPassword(rdsSlug, dbPassword);
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
rdsData: { RDS_PASSWORD: dbPassword },
|
|
474
|
+
rdsSlug,
|
|
475
|
+
poll: {
|
|
476
|
+
name: "RDS",
|
|
477
|
+
cb: async () =>
|
|
478
|
+
(
|
|
479
|
+
await rdsClient.send(
|
|
480
|
+
new DescribeDBInstancesCommand({
|
|
481
|
+
DBInstanceIdentifier: rdsSlug,
|
|
482
|
+
}),
|
|
483
|
+
)
|
|
484
|
+
).DBInstances[0],
|
|
485
|
+
shouldContinue: ({ DBInstanceStatus }) =>
|
|
486
|
+
DBInstanceStatus !== "available",
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
} else {
|
|
490
|
+
const secretValue = await aws.secretsmanager.getSecretValue(
|
|
491
|
+
`${name}-database`,
|
|
492
|
+
);
|
|
493
|
+
return {
|
|
494
|
+
rdsData: {
|
|
495
|
+
RDS_HOSTNAME: secretValue.RDS_HOSTNAME,
|
|
496
|
+
RDS_PORT: secretValue.RDS_PORT,
|
|
497
|
+
RDS_PASSWORD: secretValue.RDS_PASSWORD,
|
|
498
|
+
},
|
|
499
|
+
rdsSlug,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const provisionOpenSearch = async (
|
|
505
|
+
aws,
|
|
506
|
+
osSlug,
|
|
507
|
+
tags,
|
|
508
|
+
vpcData,
|
|
509
|
+
instanceProfileName,
|
|
510
|
+
existingResources,
|
|
511
|
+
) => {
|
|
512
|
+
const osClient = new OpenSearchClient({});
|
|
513
|
+
|
|
514
|
+
if (!existingResources.opensearch) {
|
|
515
|
+
await aws.iam.ensureOpenSearchServiceRole();
|
|
516
|
+
|
|
517
|
+
const roleArn = instanceProfileName
|
|
518
|
+
? (await aws.iam.getRole(instanceProfileName)).Role.Arn
|
|
519
|
+
: undefined;
|
|
520
|
+
|
|
521
|
+
const osSecurityGroupId = await aws.ec2.createOpenSearchSecurityGroup(
|
|
522
|
+
osSlug,
|
|
523
|
+
tags,
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
await aws.opensearch.createOpenSearchDomain(
|
|
527
|
+
osSlug,
|
|
528
|
+
osSecurityGroupId,
|
|
529
|
+
vpcData.privateSubnetIds,
|
|
530
|
+
roleArn,
|
|
531
|
+
tags,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
osData: {},
|
|
536
|
+
poll: {
|
|
537
|
+
name: "OpenSearch",
|
|
538
|
+
cb: async () => {
|
|
539
|
+
const { DomainStatus } = await osClient.send(
|
|
540
|
+
new DescribeDomainCommand({ DomainName: osSlug }),
|
|
541
|
+
);
|
|
542
|
+
return DomainStatus;
|
|
543
|
+
},
|
|
544
|
+
shouldContinue: (status) =>
|
|
545
|
+
status.Processing ||
|
|
546
|
+
(!status.Endpoint && !status.Endpoints),
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
} else {
|
|
550
|
+
const osDomain = await aws.opensearch.describeOpenSearchDomain(osSlug);
|
|
551
|
+
|
|
552
|
+
const osEndpoint =
|
|
553
|
+
osDomain.Endpoint || (osDomain.Endpoints && osDomain.Endpoints.vpc);
|
|
554
|
+
|
|
555
|
+
return { osData: { SEARCH_HOST: `https://${osEndpoint}` } };
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const provisionCache = async (
|
|
560
|
+
aws,
|
|
561
|
+
name,
|
|
562
|
+
cacheSlug,
|
|
563
|
+
tags,
|
|
564
|
+
vpcData,
|
|
565
|
+
existingResources,
|
|
566
|
+
existingSecrets,
|
|
567
|
+
) => {
|
|
568
|
+
const cacheClient = new ElastiCacheClient({});
|
|
569
|
+
|
|
570
|
+
if (!existingResources.cache) {
|
|
571
|
+
const cachePassword = generator.generate({
|
|
572
|
+
length: 16,
|
|
573
|
+
numbers: true,
|
|
574
|
+
symbols: false,
|
|
575
|
+
strict: true,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const cacheSubnetGroupName =
|
|
579
|
+
await aws.elasticache.ensureCacheSubnetGroup(
|
|
580
|
+
vpcData.privateSubnetIds,
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
const cacheSecurityGroupId = await aws.ec2.createCacheSecurityGroup(
|
|
584
|
+
cacheSlug,
|
|
585
|
+
tags,
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
await aws.elasticache.createCacheCluster(
|
|
589
|
+
cacheSlug,
|
|
590
|
+
cacheSecurityGroupId,
|
|
591
|
+
cacheSubnetGroupName,
|
|
592
|
+
cachePassword,
|
|
593
|
+
tags,
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
cacheData: { REDIS_PASSWORD: cachePassword },
|
|
598
|
+
poll: {
|
|
599
|
+
name: "ElastiCache",
|
|
600
|
+
cb: async () => {
|
|
601
|
+
const { ReplicationGroups } = await cacheClient.send(
|
|
602
|
+
new DescribeReplicationGroupsCommand({
|
|
603
|
+
ReplicationGroupId: cacheSlug,
|
|
604
|
+
}),
|
|
605
|
+
);
|
|
606
|
+
return ReplicationGroups[0];
|
|
607
|
+
},
|
|
608
|
+
shouldContinue: (group) => group.Status !== "available",
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
} else if (existingResources.cache && !existingSecrets.cache) {
|
|
612
|
+
const cluster = await aws.elasticache.describeCacheCluster(cacheSlug);
|
|
613
|
+
|
|
614
|
+
const cacheEndpoint = cluster.NodeGroups[0].PrimaryEndpoint;
|
|
367
615
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
616
|
+
return {
|
|
617
|
+
cacheData: {
|
|
618
|
+
REDIS_HOST: cacheEndpoint.Address,
|
|
619
|
+
REDIS_PORT: String(cacheEndpoint.Port),
|
|
620
|
+
REDIS_PASSWORD: "",
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
} else {
|
|
624
|
+
const secretValue = await aws.secretsmanager.getSecretValue(
|
|
625
|
+
`${name}-cache`,
|
|
626
|
+
);
|
|
627
|
+
return {
|
|
628
|
+
cacheData: {
|
|
629
|
+
REDIS_HOST: secretValue.REDIS_HOST,
|
|
630
|
+
REDIS_PORT: secretValue.REDIS_PORT,
|
|
631
|
+
REDIS_PASSWORD: secretValue.REDIS_PASSWORD,
|
|
632
|
+
},
|
|
372
633
|
};
|
|
373
634
|
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const provisionMail = async (aws, name, tags, mailData) => {
|
|
638
|
+
const {
|
|
639
|
+
IAMClient,
|
|
640
|
+
CreateUserCommand,
|
|
641
|
+
GetUserCommand,
|
|
642
|
+
CreateAccessKeyCommand,
|
|
643
|
+
ListAccessKeysCommand,
|
|
644
|
+
} = require("@aws-sdk/client-iam");
|
|
645
|
+
const { fromIni } = require("@aws-sdk/credential-providers");
|
|
646
|
+
|
|
647
|
+
const sesProfile = "fishawack";
|
|
648
|
+
const sesRegion = "us-east-1";
|
|
649
|
+
const groupName = "AWSSESSendingGroupDoNotRename";
|
|
650
|
+
|
|
651
|
+
const iamClient = new IAMClient({
|
|
652
|
+
region: sesRegion,
|
|
653
|
+
credentials: fromIni({ profile: sesProfile }),
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
const userName = `${name}-ses`;
|
|
374
657
|
|
|
658
|
+
// Create user in the fishawack account
|
|
375
659
|
try {
|
|
376
|
-
await
|
|
377
|
-
|
|
660
|
+
await Spinner.prototype.simple(
|
|
661
|
+
`Creating SES IAM user ${userName}`,
|
|
662
|
+
() => {
|
|
663
|
+
return iamClient.send(
|
|
664
|
+
new CreateUserCommand({
|
|
665
|
+
UserName: userName,
|
|
666
|
+
Tags: [
|
|
667
|
+
{ Key: "client", Value: process.env.AWS_PROFILE },
|
|
668
|
+
].concat(
|
|
669
|
+
tags.map((t) => ({
|
|
670
|
+
Key: t.Key,
|
|
671
|
+
Value: String(t.Value),
|
|
672
|
+
})),
|
|
673
|
+
),
|
|
674
|
+
}),
|
|
675
|
+
);
|
|
676
|
+
},
|
|
677
|
+
);
|
|
678
|
+
} catch {
|
|
679
|
+
await Spinner.prototype.simple(
|
|
680
|
+
`Retrieving existing SES IAM user ${userName}`,
|
|
681
|
+
() => {
|
|
682
|
+
return iamClient.send(
|
|
683
|
+
new GetUserCommand({ UserName: userName }),
|
|
684
|
+
);
|
|
685
|
+
},
|
|
378
686
|
);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Add user to SES sending group
|
|
690
|
+
await aws.iam.addUserToGroup(userName, groupName, iamClient);
|
|
691
|
+
|
|
692
|
+
// Create access key
|
|
693
|
+
const existingKeys = await iamClient.send(
|
|
694
|
+
new ListAccessKeysCommand({ UserName: userName }),
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
if (existingKeys.AccessKeyMetadata.length) {
|
|
698
|
+
// Keys already exist — we can't retrieve the secret again so return what we have in secrets
|
|
699
|
+
return {
|
|
700
|
+
MAIL_USERNAME: existingKeys.AccessKeyMetadata[0].AccessKeyId,
|
|
701
|
+
MAIL_PASSWORD: "",
|
|
702
|
+
MAIL_FROM_ADDRESS: mailData.fromAddress,
|
|
703
|
+
MAIL_FROM_NAME: mailData.fromName,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const keyRes = await Spinner.prototype.simple(
|
|
708
|
+
`Creating access key for SES user`,
|
|
709
|
+
() => {
|
|
710
|
+
return iamClient.send(
|
|
711
|
+
new CreateAccessKeyCommand({ UserName: userName }),
|
|
712
|
+
);
|
|
713
|
+
},
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
// Derive SMTP credentials
|
|
717
|
+
const smtpPassword = aws.ses.deriveSmtpPassword(
|
|
718
|
+
keyRes.AccessKey.SecretAccessKey,
|
|
719
|
+
sesRegion,
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
MAIL_USERNAME: keyRes.AccessKey.AccessKeyId,
|
|
724
|
+
MAIL_PASSWORD: smtpPassword,
|
|
725
|
+
MAIL_FROM_ADDRESS: mailData.fromAddress,
|
|
726
|
+
MAIL_FROM_NAME: mailData.fromName,
|
|
727
|
+
};
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const authorizeIngress = async (aws, name, slug, service, authorizer) => {
|
|
731
|
+
try {
|
|
732
|
+
const sg = await aws.ec2.findSecurityGroupByName(`${slug}-${service}`);
|
|
733
|
+
const ebSg = await aws.ec2.findSecurityGroupByTag([
|
|
734
|
+
{
|
|
735
|
+
Name: "tag:elasticbeanstalk:environment-name",
|
|
736
|
+
Values: [name],
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
Name: "tag:aws:cloudformation:logical-id",
|
|
740
|
+
Values: ["AWSEBSecurityGroup"],
|
|
741
|
+
},
|
|
742
|
+
]);
|
|
743
|
+
if (sg && ebSg) {
|
|
744
|
+
await authorizer(sg.GroupId, ebSg.GroupId);
|
|
745
|
+
}
|
|
379
746
|
} catch {
|
|
380
747
|
/* empty */
|
|
381
748
|
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
module.exports.fullstack = async (
|
|
752
|
+
name,
|
|
753
|
+
tags = [],
|
|
754
|
+
repo,
|
|
755
|
+
branch,
|
|
756
|
+
framework,
|
|
757
|
+
database = false,
|
|
758
|
+
storage = false,
|
|
759
|
+
opensearch = false,
|
|
760
|
+
cache = false,
|
|
761
|
+
mail = false,
|
|
762
|
+
existingSecrets = {
|
|
763
|
+
app: null,
|
|
764
|
+
storage: null,
|
|
765
|
+
database: null,
|
|
766
|
+
opensearch: null,
|
|
767
|
+
cache: null,
|
|
768
|
+
mail: null,
|
|
769
|
+
},
|
|
770
|
+
snapshot = null,
|
|
771
|
+
elbScheme = "public",
|
|
772
|
+
existingEnvironment = false,
|
|
773
|
+
existingResources = {
|
|
774
|
+
storage: false,
|
|
775
|
+
database: false,
|
|
776
|
+
opensearch: false,
|
|
777
|
+
cache: false,
|
|
778
|
+
},
|
|
779
|
+
appName,
|
|
780
|
+
appDomain,
|
|
781
|
+
certArn,
|
|
782
|
+
instanceType = "t3.micro",
|
|
783
|
+
mailData = {},
|
|
784
|
+
) => {
|
|
785
|
+
const { platform, language } = eb.configurations[framework];
|
|
786
|
+
const isLaravel = framework === "laravel";
|
|
787
|
+
const aws = module.exports;
|
|
788
|
+
|
|
789
|
+
// Compute slugs
|
|
790
|
+
const s3Slug =
|
|
791
|
+
storage || existingSecrets.storage
|
|
792
|
+
? aws.slug(repo, process.env.AWS_PROFILE, branch, "s3")
|
|
793
|
+
: undefined;
|
|
794
|
+
|
|
795
|
+
const osSlug =
|
|
796
|
+
opensearch || existingSecrets.opensearch
|
|
797
|
+
? aws.slug(repo, process.env.AWS_PROFILE, branch, "os")
|
|
798
|
+
: undefined;
|
|
799
|
+
|
|
800
|
+
const cacheSlug =
|
|
801
|
+
cache || existingSecrets.cache
|
|
802
|
+
? aws.slug(repo, process.env.AWS_PROFILE, branch, "cache")
|
|
803
|
+
: undefined;
|
|
804
|
+
|
|
805
|
+
const opensearchDomainArn = osSlug
|
|
806
|
+
? `arn:aws:es:${process.env.AWS_REGION}:*:domain/${osSlug}`
|
|
807
|
+
: undefined;
|
|
808
|
+
|
|
809
|
+
// Phase 1: IAM prerequisites (needed before OpenSearch and EB env)
|
|
810
|
+
let instanceProfileName;
|
|
811
|
+
|
|
812
|
+
if (isLaravel) {
|
|
813
|
+
instanceProfileName = await aws.iam.ensureEnvironmentInstanceProfile(
|
|
814
|
+
name,
|
|
815
|
+
s3Slug,
|
|
816
|
+
opensearchDomainArn,
|
|
817
|
+
);
|
|
818
|
+
} else {
|
|
819
|
+
await aws.iam.ensureEBInstanceProfileExists();
|
|
820
|
+
}
|
|
382
821
|
|
|
383
|
-
|
|
822
|
+
// Phase 2: VPC + EB app in parallel
|
|
823
|
+
const [vpcData] = await Promise.all([
|
|
824
|
+
aws.ec2.ensurePrivateSubnets(),
|
|
825
|
+
aws.iam.ensureEBManagedUpdateProfileExists(),
|
|
826
|
+
]);
|
|
384
827
|
|
|
385
828
|
try {
|
|
386
|
-
await
|
|
387
|
-
|
|
829
|
+
await aws.elasticbeanstalk.createElasticBeanstalkApplication(
|
|
830
|
+
repo,
|
|
831
|
+
tags,
|
|
832
|
+
);
|
|
388
833
|
} catch {
|
|
389
834
|
/* empty */
|
|
390
835
|
}
|
|
391
836
|
|
|
392
|
-
//
|
|
837
|
+
// Phase 3: Provision services in parallel
|
|
838
|
+
const servicePromises = [];
|
|
839
|
+
|
|
840
|
+
if (storage) {
|
|
841
|
+
servicePromises.push(
|
|
842
|
+
provisionStorage(aws, s3Slug, tags, isLaravel, existingResources),
|
|
843
|
+
);
|
|
844
|
+
} else {
|
|
845
|
+
servicePromises.push(Promise.resolve(s3Slug ? { s3Slug } : {}));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (database) {
|
|
849
|
+
servicePromises.push(
|
|
850
|
+
provisionRDS(
|
|
851
|
+
aws,
|
|
852
|
+
name,
|
|
853
|
+
repo,
|
|
854
|
+
branch,
|
|
855
|
+
tags,
|
|
856
|
+
vpcData,
|
|
857
|
+
existingResources,
|
|
858
|
+
existingSecrets,
|
|
859
|
+
snapshot,
|
|
860
|
+
),
|
|
861
|
+
);
|
|
862
|
+
} else {
|
|
863
|
+
servicePromises.push(Promise.resolve({ rdsData: {}, rdsSlug: null }));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (opensearch) {
|
|
867
|
+
servicePromises.push(
|
|
868
|
+
provisionOpenSearch(
|
|
869
|
+
aws,
|
|
870
|
+
osSlug,
|
|
871
|
+
tags,
|
|
872
|
+
vpcData,
|
|
873
|
+
instanceProfileName,
|
|
874
|
+
existingResources,
|
|
875
|
+
),
|
|
876
|
+
);
|
|
877
|
+
} else {
|
|
878
|
+
servicePromises.push(Promise.resolve({ osData: {} }));
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (cache) {
|
|
882
|
+
servicePromises.push(
|
|
883
|
+
provisionCache(
|
|
884
|
+
aws,
|
|
885
|
+
name,
|
|
886
|
+
cacheSlug,
|
|
887
|
+
tags,
|
|
888
|
+
vpcData,
|
|
889
|
+
existingResources,
|
|
890
|
+
existingSecrets,
|
|
891
|
+
),
|
|
892
|
+
);
|
|
893
|
+
} else {
|
|
894
|
+
servicePromises.push(Promise.resolve({ cacheData: {} }));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const [s3Data, rdsResult, osResult, cacheResult] =
|
|
898
|
+
await Promise.all(servicePromises);
|
|
899
|
+
|
|
900
|
+
// Phase 3b: Wait for all pending services with single spinner
|
|
901
|
+
const pendingPolls = [rdsResult, osResult, cacheResult]
|
|
902
|
+
.filter((r) => r && r.poll)
|
|
903
|
+
.map((r) => r.poll);
|
|
904
|
+
|
|
905
|
+
const pollResults = pendingPolls.length ? await pollAll(pendingPolls) : [];
|
|
906
|
+
|
|
907
|
+
// Extract endpoint data from poll results
|
|
908
|
+
let rdsData = rdsResult.rdsData || {};
|
|
909
|
+
|
|
910
|
+
if (rdsResult.poll) {
|
|
911
|
+
const rdsInstance = pollResults[pendingPolls.indexOf(rdsResult.poll)];
|
|
912
|
+
rdsData.RDS_HOSTNAME = rdsInstance.Endpoint.Address;
|
|
913
|
+
rdsData.RDS_PORT = String(rdsInstance.Endpoint.Port);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
let osData = osResult.osData || {};
|
|
917
|
+
|
|
918
|
+
if (osResult.poll) {
|
|
919
|
+
const osDomain = pollResults[pendingPolls.indexOf(osResult.poll)];
|
|
920
|
+
const osEndpoint =
|
|
921
|
+
osDomain.Endpoint || (osDomain.Endpoints && osDomain.Endpoints.vpc);
|
|
922
|
+
osData.SEARCH_HOST = `https://${osEndpoint}`;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
let cacheData = cacheResult.cacheData || {};
|
|
926
|
+
|
|
927
|
+
if (cacheResult.poll) {
|
|
928
|
+
const cluster = pollResults[pendingPolls.indexOf(cacheResult.poll)];
|
|
929
|
+
const cacheEndpoint = cluster.NodeGroups[0].PrimaryEndpoint;
|
|
930
|
+
cacheData.REDIS_HOST = cacheEndpoint.Address;
|
|
931
|
+
cacheData.REDIS_PORT = String(cacheEndpoint.Port);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Phase 4: Build secrets and EB environment (sequential)
|
|
393
935
|
const CNAMEPrefix = `fw-auto-${name.split("-")[name.split("-").length - 1]}`;
|
|
394
936
|
process.env.DOMAIN_LINK = `${CNAMEPrefix}.${process.env.AWS_REGION}.elasticbeanstalk.com`;
|
|
395
937
|
|
|
396
|
-
//
|
|
938
|
+
// Provision mail credentials if needed
|
|
939
|
+
let mailCreds = {};
|
|
940
|
+
if (mail && !existingSecrets.mail) {
|
|
941
|
+
mailCreds = await provisionMail(aws, name, tags, mailData);
|
|
942
|
+
}
|
|
943
|
+
|
|
397
944
|
const payloads = eb.buildSecretPayloads(
|
|
398
|
-
{
|
|
399
|
-
|
|
945
|
+
{
|
|
946
|
+
framework,
|
|
947
|
+
platform,
|
|
948
|
+
language,
|
|
949
|
+
database,
|
|
950
|
+
storage,
|
|
951
|
+
opensearch,
|
|
952
|
+
cache,
|
|
953
|
+
mail: mail && !existingSecrets.mail,
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
...s3Data,
|
|
957
|
+
...rdsData,
|
|
958
|
+
...osData,
|
|
959
|
+
...cacheData,
|
|
960
|
+
...mailCreds,
|
|
961
|
+
APP_NAME: appName,
|
|
962
|
+
APP_DOMAIN: appDomain,
|
|
963
|
+
SESSION_DOMAIN: appDomain
|
|
964
|
+
? appDomain.split(".").length > 2
|
|
965
|
+
? appDomain.split(".").slice(1).join(".")
|
|
966
|
+
: appDomain
|
|
967
|
+
: undefined,
|
|
968
|
+
},
|
|
400
969
|
);
|
|
401
970
|
|
|
402
971
|
const secretArns = {};
|
|
403
972
|
|
|
404
|
-
for (const group of
|
|
973
|
+
for (const group of secretGroups) {
|
|
405
974
|
if (existingSecrets[group]) {
|
|
406
975
|
secretArns[`${group}SecretArn`] = existingSecrets[group];
|
|
976
|
+
|
|
977
|
+
if (Object.keys(payloads[group]).length) {
|
|
978
|
+
const currentValue = await aws.secretsmanager.getSecretValue(
|
|
979
|
+
`${name}-${group}`,
|
|
980
|
+
);
|
|
981
|
+
const newKeys = Object.keys(payloads[group]).filter(
|
|
982
|
+
(k) => !(k in currentValue),
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
if (newKeys.length) {
|
|
986
|
+
const merged = { ...currentValue };
|
|
987
|
+
for (const k of newKeys) {
|
|
988
|
+
merged[k] = payloads[group][k];
|
|
989
|
+
}
|
|
990
|
+
await aws.secretsmanager.updateSecretValue(
|
|
991
|
+
`${name}-${group}`,
|
|
992
|
+
merged,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
407
996
|
} else if (Object.keys(payloads[group]).length) {
|
|
408
|
-
const res = await
|
|
997
|
+
const res = await aws.secretsmanager.createSecret(
|
|
409
998
|
`${name}-${group}`,
|
|
410
999
|
payloads[group],
|
|
411
1000
|
tags,
|
|
@@ -414,20 +1003,18 @@ module.exports.fullstack = async (
|
|
|
414
1003
|
}
|
|
415
1004
|
}
|
|
416
1005
|
|
|
417
|
-
// Auto-generate environmentsecrets entries from secret keys + ARNs
|
|
418
1006
|
const secretSettings = [];
|
|
419
1007
|
|
|
420
|
-
for (const group of
|
|
1008
|
+
for (const group of secretGroups) {
|
|
421
1009
|
const arn = secretArns[`${group}SecretArn`];
|
|
422
1010
|
if (!arn) continue;
|
|
423
1011
|
|
|
424
1012
|
let keys;
|
|
425
1013
|
|
|
426
1014
|
if (existingSecrets[group]) {
|
|
427
|
-
const secretValue =
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
);
|
|
1015
|
+
const secretValue = await aws.secretsmanager.getSecretValue(
|
|
1016
|
+
`${name}-${group}`,
|
|
1017
|
+
);
|
|
431
1018
|
keys = Object.keys(secretValue);
|
|
432
1019
|
} else {
|
|
433
1020
|
keys = Object.keys(payloads[group]);
|
|
@@ -443,115 +1030,470 @@ module.exports.fullstack = async (
|
|
|
443
1030
|
}
|
|
444
1031
|
}
|
|
445
1032
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
{
|
|
449
|
-
framework,
|
|
450
|
-
availability,
|
|
451
|
-
platform,
|
|
452
|
-
language,
|
|
453
|
-
database,
|
|
454
|
-
storage,
|
|
455
|
-
},
|
|
456
|
-
{ ...s3Data, ...rdsData },
|
|
457
|
-
);
|
|
1033
|
+
// Phase 5: Create or update EB environment
|
|
1034
|
+
let environment;
|
|
458
1035
|
|
|
459
|
-
|
|
1036
|
+
if (existingEnvironment) {
|
|
1037
|
+
const SECRETS_NAMESPACE =
|
|
1038
|
+
"aws:elasticbeanstalk:application:environmentsecrets";
|
|
1039
|
+
const currentSettings =
|
|
1040
|
+
await aws.elasticbeanstalk.describeConfigurationSettings(
|
|
1041
|
+
repo,
|
|
1042
|
+
name,
|
|
1043
|
+
);
|
|
1044
|
+
const currentSecrets = currentSettings.filter(
|
|
1045
|
+
(s) => s.Namespace === SECRETS_NAMESPACE,
|
|
1046
|
+
);
|
|
1047
|
+
|
|
1048
|
+
const currentKeys = new Set(currentSecrets.map((s) => s.OptionName));
|
|
1049
|
+
const expectedKeys = new Set(secretSettings.map((s) => s.OptionName));
|
|
460
1050
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1051
|
+
const toAdd = secretSettings.filter(
|
|
1052
|
+
(s) => !currentKeys.has(s.OptionName),
|
|
1053
|
+
);
|
|
1054
|
+
const toUpdate = secretSettings.filter((s) => {
|
|
1055
|
+
const current = currentSecrets.find(
|
|
1056
|
+
(c) => c.OptionName === s.OptionName,
|
|
1057
|
+
);
|
|
1058
|
+
return current && current.Value !== s.Value;
|
|
466
1059
|
});
|
|
467
|
-
|
|
1060
|
+
const toRemove = currentSecrets
|
|
1061
|
+
.filter((s) => !expectedKeys.has(s.OptionName))
|
|
1062
|
+
.map((s) => ({
|
|
1063
|
+
OptionName: s.OptionName,
|
|
1064
|
+
Namespace: SECRETS_NAMESPACE,
|
|
1065
|
+
}));
|
|
1066
|
+
|
|
1067
|
+
const optionSettings = [...toAdd, ...toUpdate];
|
|
1068
|
+
|
|
1069
|
+
if (optionSettings.length || toRemove.length) {
|
|
1070
|
+
await aws.elasticbeanstalk.updateEnvironmentSettings(
|
|
1071
|
+
name,
|
|
1072
|
+
optionSettings,
|
|
1073
|
+
toRemove,
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
468
1076
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1077
|
+
environment = existingEnvironment;
|
|
1078
|
+
} else {
|
|
1079
|
+
const OptionSettings = eb.merge(
|
|
1080
|
+
"environments",
|
|
1081
|
+
{
|
|
1082
|
+
framework,
|
|
1083
|
+
platform,
|
|
1084
|
+
language,
|
|
1085
|
+
database,
|
|
1086
|
+
storage,
|
|
1087
|
+
opensearch,
|
|
1088
|
+
vpcId: vpcData.vpcId,
|
|
1089
|
+
privateSubnetIds: vpcData.privateSubnetIds.join(","),
|
|
1090
|
+
publicSubnetIds: vpcData.publicSubnetIds.join(","),
|
|
1091
|
+
elbScheme,
|
|
1092
|
+
instanceProfileName,
|
|
1093
|
+
instanceType,
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
...s3Data,
|
|
1097
|
+
...rdsData,
|
|
1098
|
+
...osData,
|
|
1099
|
+
...cacheData,
|
|
1100
|
+
APP_NAME: appName,
|
|
1101
|
+
APP_DOMAIN: appDomain,
|
|
1102
|
+
SESSION_DOMAIN: appDomain
|
|
1103
|
+
? appDomain.split(".").length > 2
|
|
1104
|
+
? appDomain.split(".").slice(1).join(".")
|
|
1105
|
+
: appDomain
|
|
1106
|
+
: undefined,
|
|
1107
|
+
},
|
|
477
1108
|
);
|
|
478
1109
|
|
|
479
|
-
|
|
480
|
-
|
|
1110
|
+
if (certArn) {
|
|
1111
|
+
OptionSettings.push(
|
|
1112
|
+
{
|
|
1113
|
+
OptionName: "ListenerEnabled",
|
|
1114
|
+
Value: "true",
|
|
1115
|
+
Namespace: "aws:elbv2:listener:443",
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
OptionName: "Protocol",
|
|
1119
|
+
Value: "HTTPS",
|
|
1120
|
+
Namespace: "aws:elbv2:listener:443",
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
OptionName: "SSLCertificateArns",
|
|
1124
|
+
Value: certArn,
|
|
1125
|
+
Namespace: "aws:elbv2:listener:443",
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
OptionName: "SSLPolicy",
|
|
1129
|
+
Value: "ELBSecurityPolicy-FS-1-2-Res-2020-10",
|
|
1130
|
+
Namespace: "aws:elbv2:listener:443",
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
OptionName: "DefaultProcess",
|
|
1134
|
+
Value: "default",
|
|
1135
|
+
Namespace: "aws:elbv2:listener:443",
|
|
1136
|
+
},
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
OptionSettings.push(...secretSettings);
|
|
1141
|
+
|
|
1142
|
+
environment =
|
|
1143
|
+
await aws.elasticbeanstalk.createElasticBeanstalkEnvironment(
|
|
1144
|
+
name,
|
|
1145
|
+
{ framework, platform, language },
|
|
1146
|
+
repo,
|
|
1147
|
+
OptionSettings,
|
|
1148
|
+
CNAMEPrefix,
|
|
1149
|
+
tags,
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Phase 6: SG ingress rules in parallel
|
|
1154
|
+
const rdsSlugForIngress = aws.slug(
|
|
481
1155
|
repo,
|
|
482
1156
|
process.env.AWS_PROFILE,
|
|
483
1157
|
branch,
|
|
484
1158
|
"rds",
|
|
485
1159
|
);
|
|
486
1160
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1161
|
+
const ingressPromises = [
|
|
1162
|
+
authorizeIngress(
|
|
1163
|
+
aws,
|
|
1164
|
+
name,
|
|
1165
|
+
rdsSlugForIngress,
|
|
1166
|
+
"rds",
|
|
1167
|
+
aws.ec2.authorizeRDSIngress,
|
|
1168
|
+
),
|
|
1169
|
+
];
|
|
1170
|
+
|
|
1171
|
+
if (osSlug) {
|
|
1172
|
+
ingressPromises.push(
|
|
1173
|
+
authorizeIngress(
|
|
1174
|
+
aws,
|
|
1175
|
+
name,
|
|
1176
|
+
osSlug,
|
|
1177
|
+
"os",
|
|
1178
|
+
aws.ec2.authorizeOpenSearchIngress,
|
|
1179
|
+
),
|
|
490
1180
|
);
|
|
491
|
-
const ebSg = await module.exports.ec2.findSecurityGroupByTag([
|
|
492
|
-
{
|
|
493
|
-
Name: "tag:elasticbeanstalk:environment-name",
|
|
494
|
-
Values: [name],
|
|
495
|
-
},
|
|
496
|
-
{
|
|
497
|
-
Name: "tag:aws:cloudformation:logical-id",
|
|
498
|
-
Values: ["AWSEBSecurityGroup"],
|
|
499
|
-
},
|
|
500
|
-
]);
|
|
501
|
-
if (rdsSg && ebSg) {
|
|
502
|
-
await module.exports.ec2.authorizeRDSIngress(
|
|
503
|
-
rdsSg.GroupId,
|
|
504
|
-
ebSg.GroupId,
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
} catch {
|
|
508
|
-
/* empty */
|
|
509
1181
|
}
|
|
510
1182
|
|
|
1183
|
+
if (cacheSlug) {
|
|
1184
|
+
ingressPromises.push(
|
|
1185
|
+
authorizeIngress(
|
|
1186
|
+
aws,
|
|
1187
|
+
name,
|
|
1188
|
+
cacheSlug,
|
|
1189
|
+
"cache",
|
|
1190
|
+
aws.ec2.authorizeCacheIngress,
|
|
1191
|
+
),
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
await Promise.all(ingressPromises);
|
|
1196
|
+
|
|
1197
|
+
// Phase 7: Copy configs and return
|
|
511
1198
|
const configurations = eb.merge("configurations", {
|
|
512
1199
|
framework,
|
|
513
|
-
availability,
|
|
514
1200
|
platform,
|
|
515
1201
|
language,
|
|
516
1202
|
});
|
|
517
1203
|
|
|
518
|
-
|
|
519
|
-
configurations
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1204
|
+
await aws.elasticbeanstalk.copyElasticBeanstalkConfigs(
|
|
1205
|
+
configurations,
|
|
1206
|
+
branch,
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
let config = {
|
|
1210
|
+
url: `https://${environment.CNAME}`,
|
|
1211
|
+
environment: environment.EnvironmentName,
|
|
1212
|
+
paths: [`_IaC/${branch}/*`],
|
|
1213
|
+
copy: [
|
|
1214
|
+
{
|
|
1215
|
+
src: `_IaC/${branch}/.elasticbeanstalk/config.yml`,
|
|
1216
|
+
dest: `.elasticbeanstalk/config.yml`,
|
|
1217
|
+
},
|
|
1218
|
+
],
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
return config;
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
module.exports.fullstackClone = async (
|
|
1225
|
+
name,
|
|
1226
|
+
tags,
|
|
1227
|
+
repo,
|
|
1228
|
+
branch,
|
|
1229
|
+
applicationName,
|
|
1230
|
+
sourceEnvironmentName,
|
|
1231
|
+
sourceVersionLabel,
|
|
1232
|
+
sourceBranch,
|
|
1233
|
+
) => {
|
|
1234
|
+
const aws = module.exports;
|
|
1235
|
+
|
|
1236
|
+
// Phase 1: Read source configuration
|
|
1237
|
+
const sourceSettings =
|
|
1238
|
+
await aws.elasticbeanstalk.describeConfigurationSettings(
|
|
1239
|
+
applicationName,
|
|
1240
|
+
sourceEnvironmentName,
|
|
527
1241
|
);
|
|
1242
|
+
|
|
1243
|
+
// Phase 2: Filter settings — keep relevant namespaces, exclude immutable/auto-managed
|
|
1244
|
+
const excludedOptions = new Set([
|
|
1245
|
+
"aws:elasticbeanstalk:environment::EnvironmentName",
|
|
1246
|
+
]);
|
|
1247
|
+
|
|
1248
|
+
const includedNamespaces = new Set([
|
|
1249
|
+
"aws:elasticbeanstalk:application:environment",
|
|
1250
|
+
"aws:elasticbeanstalk:application:environmentsecrets",
|
|
1251
|
+
"aws:autoscaling:launchconfiguration",
|
|
1252
|
+
"aws:autoscaling:asg",
|
|
1253
|
+
"aws:ec2:vpc",
|
|
1254
|
+
"aws:elbv2:listener:443",
|
|
1255
|
+
"aws:elbv2:listener:default",
|
|
1256
|
+
"aws:elasticbeanstalk:environment",
|
|
1257
|
+
"aws:elasticbeanstalk:environment:process:default",
|
|
1258
|
+
"aws:elasticbeanstalk:healthreporting:system",
|
|
1259
|
+
"aws:elasticbeanstalk:managedactions",
|
|
1260
|
+
"aws:elasticbeanstalk:managedactions:platformupdate",
|
|
1261
|
+
"aws:elasticbeanstalk:cloudwatch:logs",
|
|
1262
|
+
"aws:elasticbeanstalk:cloudwatch:logs:health",
|
|
1263
|
+
]);
|
|
1264
|
+
|
|
1265
|
+
const OptionSettings = sourceSettings
|
|
1266
|
+
.filter((s) => {
|
|
1267
|
+
if (!includedNamespaces.has(s.Namespace)) return false;
|
|
1268
|
+
const key = `${s.Namespace}::${s.OptionName}`;
|
|
1269
|
+
if (excludedOptions.has(key)) return false;
|
|
1270
|
+
// Exclude empty values
|
|
1271
|
+
if (s.Value === undefined || s.Value === null || s.Value === "")
|
|
1272
|
+
return false;
|
|
1273
|
+
return true;
|
|
1274
|
+
})
|
|
1275
|
+
.map((s) => ({
|
|
1276
|
+
Namespace: s.Namespace,
|
|
1277
|
+
OptionName: s.OptionName,
|
|
1278
|
+
Value: s.Value,
|
|
1279
|
+
}));
|
|
1280
|
+
|
|
1281
|
+
// Detect which services exist from source secrets
|
|
1282
|
+
const sourceSecrets = sourceSettings.filter(
|
|
1283
|
+
(s) =>
|
|
1284
|
+
s.Namespace ===
|
|
1285
|
+
"aws:elasticbeanstalk:application:environmentsecrets",
|
|
1286
|
+
);
|
|
1287
|
+
|
|
1288
|
+
const hasOpenSearch = sourceSecrets.some(
|
|
1289
|
+
(s) => s.OptionName === "SEARCH_HOST",
|
|
1290
|
+
);
|
|
1291
|
+
|
|
1292
|
+
// Set SECRET_MIRROR_ENV so setvars.sh reads secrets from the source env
|
|
1293
|
+
OptionSettings.push({
|
|
1294
|
+
Namespace: "aws:elasticbeanstalk:application:environment",
|
|
1295
|
+
OptionName: "SECRET_MIRROR_ENV",
|
|
1296
|
+
Value: sourceEnvironmentName,
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
// Override HealthCheckGracePeriod for initial deploy window
|
|
1300
|
+
const healthGraceIdx = OptionSettings.findIndex(
|
|
1301
|
+
(s) =>
|
|
1302
|
+
s.Namespace ===
|
|
1303
|
+
"aws:elasticbeanstalk:environment:process:default" &&
|
|
1304
|
+
s.OptionName === "HealthCheckGracePeriod",
|
|
1305
|
+
);
|
|
1306
|
+
if (healthGraceIdx !== -1) {
|
|
1307
|
+
OptionSettings[healthGraceIdx].Value = "600";
|
|
528
1308
|
}
|
|
529
1309
|
|
|
530
|
-
|
|
531
|
-
|
|
1310
|
+
// Phase 4: Create the environment
|
|
1311
|
+
const CNAMEPrefix = `fw-auto-${name.split("-")[name.split("-").length - 1]}`;
|
|
1312
|
+
|
|
1313
|
+
const environment =
|
|
1314
|
+
await aws.elasticbeanstalk.createElasticBeanstalkEnvironment(
|
|
1315
|
+
name,
|
|
1316
|
+
{ language: detectLanguageFromSettings(sourceSettings) },
|
|
1317
|
+
applicationName,
|
|
1318
|
+
OptionSettings,
|
|
1319
|
+
CNAMEPrefix,
|
|
1320
|
+
tags,
|
|
1321
|
+
sourceVersionLabel,
|
|
1322
|
+
);
|
|
1323
|
+
|
|
1324
|
+
// Phase 5: Authorize SG ingress on source service security groups
|
|
1325
|
+
const rdsSlug = aws.slug(
|
|
1326
|
+
repo,
|
|
1327
|
+
process.env.AWS_PROFILE,
|
|
1328
|
+
sourceBranch,
|
|
1329
|
+
"rds",
|
|
1330
|
+
);
|
|
1331
|
+
const cacheSlug = aws.slug(
|
|
1332
|
+
repo,
|
|
1333
|
+
process.env.AWS_PROFILE,
|
|
1334
|
+
sourceBranch,
|
|
1335
|
+
"cache",
|
|
532
1336
|
);
|
|
533
1337
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
1338
|
+
const ingressPromises = [
|
|
1339
|
+
authorizeIngress(
|
|
1340
|
+
aws,
|
|
1341
|
+
name,
|
|
1342
|
+
rdsSlug,
|
|
1343
|
+
"rds",
|
|
1344
|
+
aws.ec2.authorizeRDSIngress,
|
|
1345
|
+
),
|
|
1346
|
+
];
|
|
1347
|
+
|
|
1348
|
+
if (hasOpenSearch) {
|
|
1349
|
+
const sourceOsSlug = aws.slug(
|
|
1350
|
+
repo,
|
|
1351
|
+
process.env.AWS_PROFILE,
|
|
1352
|
+
sourceBranch,
|
|
1353
|
+
"os",
|
|
1354
|
+
);
|
|
1355
|
+
ingressPromises.push(
|
|
1356
|
+
authorizeIngress(
|
|
1357
|
+
aws,
|
|
1358
|
+
name,
|
|
1359
|
+
sourceOsSlug,
|
|
1360
|
+
"os",
|
|
1361
|
+
aws.ec2.authorizeOpenSearchIngress,
|
|
1362
|
+
),
|
|
537
1363
|
);
|
|
538
1364
|
}
|
|
539
1365
|
|
|
540
|
-
|
|
1366
|
+
if (sourceSecrets.some((s) => s.OptionName === "REDIS_HOST")) {
|
|
1367
|
+
ingressPromises.push(
|
|
1368
|
+
authorizeIngress(
|
|
1369
|
+
aws,
|
|
1370
|
+
name,
|
|
1371
|
+
cacheSlug,
|
|
1372
|
+
"cache",
|
|
1373
|
+
aws.ec2.authorizeCacheIngress,
|
|
1374
|
+
),
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
await Promise.all(ingressPromises);
|
|
1379
|
+
|
|
1380
|
+
// Phase 6: Copy configs
|
|
1381
|
+
const framework = detectFrameworkFromSettings(sourceSettings);
|
|
1382
|
+
const { platform, language } = eb.configurations[framework];
|
|
1383
|
+
const configurations = eb.merge("configurations", {
|
|
1384
|
+
framework,
|
|
1385
|
+
platform,
|
|
1386
|
+
language,
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
await aws.elasticbeanstalk.copyElasticBeanstalkConfigs(
|
|
1390
|
+
configurations,
|
|
1391
|
+
branch,
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
return {
|
|
541
1395
|
url: `https://${environment.CNAME}`,
|
|
542
1396
|
environment: environment.EnvironmentName,
|
|
543
|
-
paths:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
return obj;
|
|
551
|
-
}),
|
|
1397
|
+
paths: [`_IaC/${branch}/*`],
|
|
1398
|
+
copy: [
|
|
1399
|
+
{
|
|
1400
|
+
src: `_IaC/${branch}/.elasticbeanstalk/config.yml`,
|
|
1401
|
+
dest: `.elasticbeanstalk/config.yml`,
|
|
1402
|
+
},
|
|
1403
|
+
],
|
|
552
1404
|
};
|
|
1405
|
+
};
|
|
553
1406
|
|
|
554
|
-
|
|
1407
|
+
const detectLanguageFromSettings = (settings) => {
|
|
1408
|
+
const envVars = settings.filter(
|
|
1409
|
+
(s) =>
|
|
1410
|
+
s.Namespace === "aws:elasticbeanstalk:application:environment" ||
|
|
1411
|
+
s.Namespace ===
|
|
1412
|
+
"aws:elasticbeanstalk:application:environmentsecrets",
|
|
1413
|
+
);
|
|
1414
|
+
const keys = envVars.map((s) => s.OptionName);
|
|
1415
|
+
if (keys.includes("DJANGO_SETTINGS_MODULE")) return "python";
|
|
1416
|
+
if (keys.includes("NODE_ENV")) return "node";
|
|
1417
|
+
return "php";
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
const detectFrameworkFromSettings = (settings) => {
|
|
1421
|
+
const envVars = settings.filter(
|
|
1422
|
+
(s) =>
|
|
1423
|
+
s.Namespace === "aws:elasticbeanstalk:application:environment" ||
|
|
1424
|
+
s.Namespace ===
|
|
1425
|
+
"aws:elasticbeanstalk:application:environmentsecrets",
|
|
1426
|
+
);
|
|
1427
|
+
const keys = envVars.map((s) => s.OptionName);
|
|
1428
|
+
if (keys.includes("DJANGO_SETTINGS_MODULE")) return "python";
|
|
1429
|
+
if (keys.includes("ADONIS_ACE_CWD")) return "adonis";
|
|
1430
|
+
if (keys.includes("APP_KEY")) return "laravel";
|
|
1431
|
+
return "laravel";
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
module.exports.fullstackCloneTerminate = async (
|
|
1435
|
+
name,
|
|
1436
|
+
repo,
|
|
1437
|
+
branch,
|
|
1438
|
+
sourceBranch,
|
|
1439
|
+
) => {
|
|
1440
|
+
const aws = module.exports;
|
|
1441
|
+
|
|
1442
|
+
const rdsSlug = aws.slug(
|
|
1443
|
+
repo,
|
|
1444
|
+
process.env.AWS_PROFILE,
|
|
1445
|
+
sourceBranch,
|
|
1446
|
+
"rds",
|
|
1447
|
+
);
|
|
1448
|
+
const osSlug = aws.slug(repo, process.env.AWS_PROFILE, sourceBranch, "os");
|
|
1449
|
+
const cacheSlug = aws.slug(
|
|
1450
|
+
repo,
|
|
1451
|
+
process.env.AWS_PROFILE,
|
|
1452
|
+
sourceBranch,
|
|
1453
|
+
"cache",
|
|
1454
|
+
);
|
|
1455
|
+
|
|
1456
|
+
// Phase 1: Revoke SG ingress rules
|
|
1457
|
+
await Promise.all([
|
|
1458
|
+
authorizeIngress(aws, name, rdsSlug, "rds", aws.ec2.revokeRDSIngress),
|
|
1459
|
+
authorizeIngress(
|
|
1460
|
+
aws,
|
|
1461
|
+
name,
|
|
1462
|
+
osSlug,
|
|
1463
|
+
"os",
|
|
1464
|
+
aws.ec2.revokeOpenSearchIngress,
|
|
1465
|
+
),
|
|
1466
|
+
authorizeIngress(
|
|
1467
|
+
aws,
|
|
1468
|
+
name,
|
|
1469
|
+
cacheSlug,
|
|
1470
|
+
"cache",
|
|
1471
|
+
aws.ec2.revokeCacheIngress,
|
|
1472
|
+
),
|
|
1473
|
+
]);
|
|
1474
|
+
|
|
1475
|
+
// Phase 2: Terminate EB environment
|
|
1476
|
+
try {
|
|
1477
|
+
await aws.elasticbeanstalk.removeElasticBeanstalkEnvironment(name);
|
|
1478
|
+
} catch {
|
|
1479
|
+
/* empty */
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// Phase 3: Terminate EB environment + try removing app if empty
|
|
1483
|
+
await Promise.all([
|
|
1484
|
+
(async () => {
|
|
1485
|
+
try {
|
|
1486
|
+
const envs = await aws.elasticbeanstalk.listEnvironments(repo);
|
|
1487
|
+
if (!envs.length) {
|
|
1488
|
+
await aws.elasticbeanstalk.removeElasticBeanstalkApplication(
|
|
1489
|
+
repo,
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
} catch {
|
|
1493
|
+
/* empty */
|
|
1494
|
+
}
|
|
1495
|
+
})(),
|
|
1496
|
+
]);
|
|
555
1497
|
};
|
|
556
1498
|
|
|
557
1499
|
module.exports.fullstackTerminate = async (
|
|
@@ -561,128 +1503,324 @@ module.exports.fullstackTerminate = async (
|
|
|
561
1503
|
{
|
|
562
1504
|
deleteStorage = false,
|
|
563
1505
|
deleteDatabase = false,
|
|
1506
|
+
deleteOpenSearch = false,
|
|
1507
|
+
deleteCache = false,
|
|
1508
|
+
deleteMail = false,
|
|
564
1509
|
deleteAppSecret = false,
|
|
565
1510
|
} = {},
|
|
566
1511
|
) => {
|
|
567
|
-
|
|
568
|
-
const s3Slug = module.exports.slug(
|
|
569
|
-
repo,
|
|
570
|
-
process.env.AWS_PROFILE,
|
|
571
|
-
branch,
|
|
572
|
-
"s3",
|
|
573
|
-
);
|
|
1512
|
+
const aws = module.exports;
|
|
574
1513
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
/* empty */
|
|
579
|
-
}
|
|
1514
|
+
const rdsSlug = aws.slug(repo, process.env.AWS_PROFILE, branch, "rds");
|
|
1515
|
+
const osSlug = aws.slug(repo, process.env.AWS_PROFILE, branch, "os");
|
|
1516
|
+
const cacheSlug = aws.slug(repo, process.env.AWS_PROFILE, branch, "cache");
|
|
580
1517
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
} catch {
|
|
584
|
-
/* empty */
|
|
585
|
-
}
|
|
1518
|
+
// Phase 1: Storage cleanup + revoke all SG ingress rules in parallel
|
|
1519
|
+
const phase1 = [];
|
|
586
1520
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1521
|
+
if (deleteStorage) {
|
|
1522
|
+
const s3Slug = aws.slug(repo, process.env.AWS_PROFILE, branch, "s3");
|
|
1523
|
+
|
|
1524
|
+
phase1.push(
|
|
1525
|
+
(async () => {
|
|
1526
|
+
try {
|
|
1527
|
+
await aws.iam.removeIAMUser(s3Slug);
|
|
1528
|
+
} catch {
|
|
1529
|
+
/* empty */
|
|
1530
|
+
}
|
|
1531
|
+
try {
|
|
1532
|
+
await aws.s3.emptyS3Bucket(s3Slug);
|
|
1533
|
+
} catch {
|
|
1534
|
+
/* empty */
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
await aws.s3.removeS3Bucket(s3Slug);
|
|
1538
|
+
} catch {
|
|
1539
|
+
/* empty */
|
|
1540
|
+
}
|
|
1541
|
+
})(),
|
|
1542
|
+
);
|
|
592
1543
|
}
|
|
593
1544
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
1545
|
+
phase1.push(
|
|
1546
|
+
authorizeIngress(aws, name, rdsSlug, "rds", aws.ec2.revokeRDSIngress),
|
|
1547
|
+
authorizeIngress(
|
|
1548
|
+
aws,
|
|
1549
|
+
name,
|
|
1550
|
+
osSlug,
|
|
1551
|
+
"os",
|
|
1552
|
+
aws.ec2.revokeOpenSearchIngress,
|
|
1553
|
+
),
|
|
1554
|
+
authorizeIngress(
|
|
1555
|
+
aws,
|
|
1556
|
+
name,
|
|
1557
|
+
cacheSlug,
|
|
1558
|
+
"cache",
|
|
1559
|
+
aws.ec2.revokeCacheIngress,
|
|
1560
|
+
),
|
|
600
1561
|
);
|
|
601
1562
|
|
|
1563
|
+
await Promise.all(phase1);
|
|
1564
|
+
|
|
1565
|
+
// Phase 2: Terminate EB environment (must complete before SG deletion)
|
|
602
1566
|
try {
|
|
603
|
-
|
|
604
|
-
`${rdsSlug}-rds`,
|
|
605
|
-
);
|
|
606
|
-
const ebSg = await module.exports.ec2.findSecurityGroupByTag([
|
|
607
|
-
{
|
|
608
|
-
Name: "tag:elasticbeanstalk:environment-name",
|
|
609
|
-
Values: [name],
|
|
610
|
-
},
|
|
611
|
-
{
|
|
612
|
-
Name: "tag:aws:cloudformation:logical-id",
|
|
613
|
-
Values: ["AWSEBSecurityGroup"],
|
|
614
|
-
},
|
|
615
|
-
]);
|
|
616
|
-
if (rdsSg && ebSg) {
|
|
617
|
-
await module.exports.ec2.revokeRDSIngress(
|
|
618
|
-
rdsSg.GroupId,
|
|
619
|
-
ebSg.GroupId,
|
|
620
|
-
);
|
|
621
|
-
}
|
|
1567
|
+
await aws.elasticbeanstalk.removeElasticBeanstalkEnvironment(name);
|
|
622
1568
|
} catch {
|
|
623
1569
|
/* empty */
|
|
624
1570
|
}
|
|
625
1571
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
1572
|
+
// Phase 3: EB app + instance profile + initiate service deletions in parallel
|
|
1573
|
+
const phase3 = [
|
|
1574
|
+
(async () => {
|
|
1575
|
+
try {
|
|
1576
|
+
await aws.elasticbeanstalk.removeElasticBeanstalkApplication(
|
|
1577
|
+
repo,
|
|
1578
|
+
);
|
|
1579
|
+
} catch {
|
|
1580
|
+
/* empty */
|
|
1581
|
+
}
|
|
1582
|
+
})(),
|
|
1583
|
+
(async () => {
|
|
1584
|
+
try {
|
|
1585
|
+
await aws.iam.removeEnvironmentInstanceProfile(name);
|
|
1586
|
+
} catch {
|
|
1587
|
+
/* empty */
|
|
1588
|
+
}
|
|
1589
|
+
})(),
|
|
1590
|
+
];
|
|
1591
|
+
|
|
1592
|
+
if (deleteDatabase) {
|
|
1593
|
+
phase3.push(aws.rds.deleteRDSInstance(rdsSlug).catch(() => {}));
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
if (deleteOpenSearch) {
|
|
1597
|
+
phase3.push(
|
|
1598
|
+
aws.opensearch.deleteOpenSearchDomain(osSlug).catch(() => {}),
|
|
629
1599
|
);
|
|
630
|
-
} catch {
|
|
631
|
-
/* empty */
|
|
632
1600
|
}
|
|
633
1601
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1602
|
+
if (deleteCache) {
|
|
1603
|
+
phase3.push(
|
|
1604
|
+
aws.elasticache.deleteCacheCluster(cacheSlug).catch(() => {}),
|
|
637
1605
|
);
|
|
638
|
-
} catch {
|
|
639
|
-
/* empty */
|
|
640
1606
|
}
|
|
641
1607
|
|
|
1608
|
+
await Promise.all(phase3);
|
|
1609
|
+
|
|
1610
|
+
// Phase 4: Wait for service deletions with round-robin polling
|
|
1611
|
+
const deletionPolls = [];
|
|
1612
|
+
|
|
642
1613
|
if (deleteDatabase) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1614
|
+
const rdsClient = new RDSClient({});
|
|
1615
|
+
|
|
1616
|
+
deletionPolls.push({
|
|
1617
|
+
name: "RDS",
|
|
1618
|
+
cb: async () => {
|
|
1619
|
+
try {
|
|
1620
|
+
const res = await rdsClient.send(
|
|
1621
|
+
new DescribeDBInstancesCommand({
|
|
1622
|
+
DBInstanceIdentifier: rdsSlug,
|
|
1623
|
+
}),
|
|
1624
|
+
);
|
|
1625
|
+
return res.DBInstances[0];
|
|
1626
|
+
} catch (e) {
|
|
1627
|
+
if (e.name === "DBInstanceNotFoundFault") {
|
|
1628
|
+
return { DBInstanceStatus: "deleted" };
|
|
1629
|
+
}
|
|
1630
|
+
throw e;
|
|
1631
|
+
}
|
|
1632
|
+
},
|
|
1633
|
+
shouldContinue: ({ DBInstanceStatus }) =>
|
|
1634
|
+
DBInstanceStatus !== "deleted",
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
649
1637
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1638
|
+
if (deleteOpenSearch) {
|
|
1639
|
+
const osClient = new OpenSearchClient({});
|
|
1640
|
+
|
|
1641
|
+
deletionPolls.push({
|
|
1642
|
+
name: "OpenSearch",
|
|
1643
|
+
cb: async () => {
|
|
1644
|
+
try {
|
|
1645
|
+
const { DomainStatus } = await osClient.send(
|
|
1646
|
+
new DescribeDomainCommand({ DomainName: osSlug }),
|
|
1647
|
+
);
|
|
1648
|
+
return DomainStatus;
|
|
1649
|
+
} catch (e) {
|
|
1650
|
+
if (e.name === "ResourceNotFoundException") {
|
|
1651
|
+
return { deleted: true };
|
|
1652
|
+
}
|
|
1653
|
+
throw e;
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
shouldContinue: (status) => !status.deleted,
|
|
1657
|
+
});
|
|
660
1658
|
}
|
|
661
1659
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1660
|
+
if (deleteCache) {
|
|
1661
|
+
const cacheClient = new ElastiCacheClient({});
|
|
1662
|
+
|
|
1663
|
+
deletionPolls.push({
|
|
1664
|
+
name: "ElastiCache",
|
|
1665
|
+
cb: async () => {
|
|
1666
|
+
try {
|
|
1667
|
+
const { ReplicationGroups } = await cacheClient.send(
|
|
1668
|
+
new DescribeReplicationGroupsCommand({
|
|
1669
|
+
ReplicationGroupId: cacheSlug,
|
|
1670
|
+
}),
|
|
1671
|
+
);
|
|
1672
|
+
return ReplicationGroups[0];
|
|
1673
|
+
} catch (e) {
|
|
1674
|
+
if (e.name === "ReplicationGroupNotFoundFault") {
|
|
1675
|
+
return { deleted: true };
|
|
1676
|
+
}
|
|
1677
|
+
throw e;
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
shouldContinue: (group) => !group.deleted,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
if (deletionPolls.length) {
|
|
1685
|
+
await pollAll(deletionPolls, "Waiting for services to terminate");
|
|
669
1686
|
}
|
|
670
1687
|
|
|
1688
|
+
// Phase 5: Delete security groups after services are gone
|
|
1689
|
+
const sgDeletions = [];
|
|
1690
|
+
|
|
671
1691
|
if (deleteDatabase) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
1692
|
+
sgDeletions.push(
|
|
1693
|
+
(async () => {
|
|
1694
|
+
try {
|
|
1695
|
+
const sg = await aws.ec2.findSecurityGroupByName(
|
|
1696
|
+
`${rdsSlug}-rds`,
|
|
1697
|
+
);
|
|
1698
|
+
if (sg) {
|
|
1699
|
+
await aws.ec2.deleteSecurityGroup(sg.GroupId);
|
|
1700
|
+
}
|
|
1701
|
+
} catch {
|
|
1702
|
+
/* empty */
|
|
1703
|
+
}
|
|
1704
|
+
})(),
|
|
1705
|
+
);
|
|
679
1706
|
}
|
|
680
1707
|
|
|
1708
|
+
if (deleteOpenSearch) {
|
|
1709
|
+
sgDeletions.push(
|
|
1710
|
+
(async () => {
|
|
1711
|
+
try {
|
|
1712
|
+
const sg = await aws.ec2.findSecurityGroupByName(
|
|
1713
|
+
`${osSlug}-os`,
|
|
1714
|
+
);
|
|
1715
|
+
if (sg) {
|
|
1716
|
+
await aws.ec2.deleteSecurityGroup(sg.GroupId);
|
|
1717
|
+
}
|
|
1718
|
+
} catch {
|
|
1719
|
+
/* empty */
|
|
1720
|
+
}
|
|
1721
|
+
})(),
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (deleteCache) {
|
|
1726
|
+
sgDeletions.push(
|
|
1727
|
+
(async () => {
|
|
1728
|
+
try {
|
|
1729
|
+
const sg = await aws.ec2.findSecurityGroupByName(
|
|
1730
|
+
`${cacheSlug}-cache`,
|
|
1731
|
+
);
|
|
1732
|
+
if (sg) {
|
|
1733
|
+
await aws.ec2.deleteSecurityGroup(sg.GroupId);
|
|
1734
|
+
}
|
|
1735
|
+
} catch {
|
|
1736
|
+
/* empty */
|
|
1737
|
+
}
|
|
1738
|
+
})(),
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (sgDeletions.length) {
|
|
1743
|
+
await Promise.all(sgDeletions);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// Phase 6: Delete secrets in parallel
|
|
1747
|
+
const secretDeletions = [];
|
|
1748
|
+
|
|
1749
|
+
if (deleteStorage) {
|
|
1750
|
+
secretDeletions.push(
|
|
1751
|
+
aws.secretsmanager.deleteSecret(`${name}-storage`).catch(() => {}),
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
if (deleteDatabase) {
|
|
1755
|
+
secretDeletions.push(
|
|
1756
|
+
aws.secretsmanager.deleteSecret(`${name}-database`).catch(() => {}),
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
if (deleteOpenSearch) {
|
|
1760
|
+
secretDeletions.push(
|
|
1761
|
+
aws.secretsmanager
|
|
1762
|
+
.deleteSecret(`${name}-opensearch`)
|
|
1763
|
+
.catch(() => {}),
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
if (deleteCache) {
|
|
1767
|
+
secretDeletions.push(
|
|
1768
|
+
aws.secretsmanager.deleteSecret(`${name}-cache`).catch(() => {}),
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
681
1771
|
if (deleteAppSecret) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1772
|
+
secretDeletions.push(
|
|
1773
|
+
aws.secretsmanager.deleteSecret(`${name}-app`).catch(() => {}),
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
if (deleteMail) {
|
|
1777
|
+
secretDeletions.push(
|
|
1778
|
+
aws.secretsmanager.deleteSecret(`${name}-mail`).catch(() => {}),
|
|
1779
|
+
);
|
|
1780
|
+
secretDeletions.push(
|
|
1781
|
+
(async () => {
|
|
1782
|
+
const {
|
|
1783
|
+
IAMClient,
|
|
1784
|
+
DeleteAccessKeyCommand,
|
|
1785
|
+
ListAccessKeysCommand,
|
|
1786
|
+
RemoveUserFromGroupCommand,
|
|
1787
|
+
DeleteUserCommand,
|
|
1788
|
+
} = require("@aws-sdk/client-iam");
|
|
1789
|
+
const { fromIni } = require("@aws-sdk/credential-providers");
|
|
1790
|
+
|
|
1791
|
+
const iamClient = new IAMClient({
|
|
1792
|
+
region: "us-east-1",
|
|
1793
|
+
credentials: fromIni({ profile: "fishawack" }),
|
|
1794
|
+
});
|
|
1795
|
+
const userName = `${name}-ses`;
|
|
1796
|
+
|
|
1797
|
+
try {
|
|
1798
|
+
const keys = await iamClient.send(
|
|
1799
|
+
new ListAccessKeysCommand({ UserName: userName }),
|
|
1800
|
+
);
|
|
1801
|
+
for (const key of keys.AccessKeyMetadata || []) {
|
|
1802
|
+
await iamClient.send(
|
|
1803
|
+
new DeleteAccessKeyCommand({
|
|
1804
|
+
UserName: userName,
|
|
1805
|
+
AccessKeyId: key.AccessKeyId,
|
|
1806
|
+
}),
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
await iamClient.send(
|
|
1810
|
+
new RemoveUserFromGroupCommand({
|
|
1811
|
+
UserName: userName,
|
|
1812
|
+
GroupName: "AWSSESSendingGroupDoNotRename",
|
|
1813
|
+
}),
|
|
1814
|
+
);
|
|
1815
|
+
await iamClient.send(
|
|
1816
|
+
new DeleteUserCommand({ UserName: userName }),
|
|
1817
|
+
);
|
|
1818
|
+
} catch {
|
|
1819
|
+
/* user may not exist */
|
|
1820
|
+
}
|
|
1821
|
+
})(),
|
|
1822
|
+
);
|
|
687
1823
|
}
|
|
1824
|
+
|
|
1825
|
+
await Promise.all(secretDeletions);
|
|
688
1826
|
};
|