@fishawack/lab-env 5.7.0-beta.1 → 5.7.0-beta.2
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 +16 -0
- package/_Test/provision.js +3 -1
- package/commands/create/cmds/deprovision.js +7 -0
- package/commands/create/cmds/provision.js +182 -48
- package/commands/create/cmds/sync-secrets.js +2 -1
- package/commands/create/libs/utilities.js +2 -0
- package/commands/create/libs/vars.js +53 -53
- package/commands/create/services/aws/acm.js +59 -0
- package/commands/create/services/aws/ec2.js +576 -28
- package/commands/create/services/aws/elasticbeanstalk.js +16 -0
- package/commands/create/services/aws/iam.js +254 -0
- package/commands/create/services/aws/index.js +462 -67
- package/commands/create/services/aws/opensearch.js +147 -0
- package/commands/create/services/aws/rds.js +38 -0
- package/commands/create/services/aws/s3.js +41 -7
- package/commands/create/services/aws/secretsmanager.js +16 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
+
### 5.7.0-beta.2 (2026-06-11)
|
|
4
|
+
|
|
5
|
+
#### Features
|
|
6
|
+
|
|
7
|
+
* ability to add opensearch to laravel fullstack provisions ([6218c78](https://bitbucket.org/fishawackdigital/lab-env/commits/6218c7835124941fd63ef8056c462a8775f6d7ae))
|
|
8
|
+
* added ssm permission to ec2 role ([7b0195b](https://bitbucket.org/fishawackdigital/lab-env/commits/7b0195b98c59dd4fdcc8ca6848847dddda9ffb61))
|
|
9
|
+
* auto configuration of the acm cert and port 443 listener if cert found ([878ce17](https://bitbucket.org/fishawackdigital/lab-env/commits/878ce175b6031a7ee6cd9f84420c8ace4d858093))
|
|
10
|
+
* ec2 instances always in private and prompt for load balancer visibility + python now dynamic ([bd651cb](https://bitbucket.org/fishawackdigital/lab-env/commits/bd651cb78733d45bf1601e24dbd9c5a5ffc458e0))
|
|
11
|
+
* ensure private subnets now a standard part of the prov command ([22f9876](https://bitbucket.org/fishawackdigital/lab-env/commits/22f9876e390689f7e94939be3839d0a2312f5f10))
|
|
12
|
+
* rds now also creates in private subnet ([e7fb89d](https://bitbucket.org/fishawackdigital/lab-env/commits/e7fb89d9dfbb3fe62c34e8a07c6ba1c7fc732964))
|
|
13
|
+
* use instance role and bucket policy for access now and move restore to after preflight checks ([c4bc090](https://bitbucket.org/fishawackdigital/lab-env/commits/c4bc090927a8dd9d43a52b93f06926e7167003b2))
|
|
14
|
+
|
|
15
|
+
#### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* fullstack provision now indempotent and search enforces search role ([ca374ca](https://bitbucket.org/fishawackdigital/lab-env/commits/ca374caf5c1efa0fa216f62db776591de228e8d5))
|
|
18
|
+
|
|
3
19
|
### 5.7.0-beta.1 (2026-06-08)
|
|
4
20
|
|
|
5
21
|
#### Features
|
package/_Test/provision.js
CHANGED
|
@@ -66,7 +66,9 @@ describe("provision", () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
after(async () => {
|
|
69
|
-
await aws.fullstackTerminate(repo, repo, branch
|
|
69
|
+
await aws.fullstackTerminate(repo, repo, branch, {
|
|
70
|
+
deleteAppSecret: true,
|
|
71
|
+
});
|
|
70
72
|
|
|
71
73
|
execSync(`rm -rf .elasticbeanstalk`, {
|
|
72
74
|
encoding: "utf8",
|
|
@@ -92,6 +92,12 @@ module.exports = [
|
|
|
92
92
|
message: "Delete the RDS database?",
|
|
93
93
|
default: false,
|
|
94
94
|
},
|
|
95
|
+
{
|
|
96
|
+
type: "confirm",
|
|
97
|
+
name: "deleteOpenSearch",
|
|
98
|
+
message: "Delete the OpenSearch domain?",
|
|
99
|
+
default: false,
|
|
100
|
+
},
|
|
95
101
|
{
|
|
96
102
|
type: "confirm",
|
|
97
103
|
name: "deleteAppSecret",
|
|
@@ -103,6 +109,7 @@ module.exports = [
|
|
|
103
109
|
await aws.fullstackTerminate(slug, _.repoSafe, branch, {
|
|
104
110
|
deleteStorage: resourceAnswers.deleteStorage,
|
|
105
111
|
deleteDatabase: resourceAnswers.deleteDatabase,
|
|
112
|
+
deleteOpenSearch: resourceAnswers.deleteOpenSearch,
|
|
106
113
|
deleteAppSecret: resourceAnswers.deleteAppSecret,
|
|
107
114
|
});
|
|
108
115
|
} else {
|
|
@@ -228,6 +228,20 @@ module.exports = [
|
|
|
228
228
|
},
|
|
229
229
|
])),
|
|
230
230
|
};
|
|
231
|
+
|
|
232
|
+
if (answers.framework === "laravel") {
|
|
233
|
+
answers = {
|
|
234
|
+
...answers,
|
|
235
|
+
...(await inquirer.prompt([
|
|
236
|
+
{
|
|
237
|
+
type: "confirm",
|
|
238
|
+
name: "opensearch",
|
|
239
|
+
message: "Does this site need OpenSearch?",
|
|
240
|
+
default: false,
|
|
241
|
+
},
|
|
242
|
+
])),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
231
245
|
}
|
|
232
246
|
|
|
233
247
|
answers = {
|
|
@@ -296,78 +310,60 @@ module.exports = [
|
|
|
296
310
|
// Set AWS client defaults before any AWS operations
|
|
297
311
|
setAWSClientDefaults(answers.client, answers.region);
|
|
298
312
|
|
|
299
|
-
// Snapshot restore prompt — only if database requested, before preflight
|
|
300
|
-
let snapshot = null;
|
|
301
|
-
|
|
302
|
-
if (answers.database) {
|
|
303
|
-
const { restore } = await inquirer.prompt([
|
|
304
|
-
{
|
|
305
|
-
type: "confirm",
|
|
306
|
-
name: "restore",
|
|
307
|
-
message: "Restore database from an existing snapshot?",
|
|
308
|
-
default: false,
|
|
309
|
-
},
|
|
310
|
-
]);
|
|
311
|
-
|
|
312
|
-
if (restore) {
|
|
313
|
-
const snapshots = await aws.rds.listManualSnapshots();
|
|
314
|
-
|
|
315
|
-
if (snapshots.length) {
|
|
316
|
-
const { selected } = await inquirer.prompt([
|
|
317
|
-
{
|
|
318
|
-
type: "list",
|
|
319
|
-
name: "selected",
|
|
320
|
-
message: "Select a snapshot to restore from",
|
|
321
|
-
choices: snapshots.map((s) => ({
|
|
322
|
-
name: `${s.DBSnapshotIdentifier} (source: ${s.DBInstanceIdentifier}, created: ${new Date(s.SnapshotCreateTime).toLocaleDateString()})`,
|
|
323
|
-
value: s.DBSnapshotIdentifier,
|
|
324
|
-
})),
|
|
325
|
-
},
|
|
326
|
-
]);
|
|
327
|
-
|
|
328
|
-
snapshot = selected;
|
|
329
|
-
} else {
|
|
330
|
-
new utilities.Spinner(
|
|
331
|
-
"No manual snapshots found, creating a fresh database",
|
|
332
|
-
true,
|
|
333
|
-
).ora.info();
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
313
|
// Pre-flight check for existing resources
|
|
339
|
-
let existingSecrets = {
|
|
314
|
+
let existingSecrets = {
|
|
315
|
+
app: null,
|
|
316
|
+
storage: null,
|
|
317
|
+
database: null,
|
|
318
|
+
opensearch: null,
|
|
319
|
+
};
|
|
320
|
+
let existingEnvironment = false;
|
|
321
|
+
let existingResources = {
|
|
322
|
+
storage: false,
|
|
323
|
+
database: false,
|
|
324
|
+
opensearch: false,
|
|
325
|
+
};
|
|
326
|
+
let snapshot = null;
|
|
340
327
|
|
|
341
328
|
if (answers.stack === "fullstack") {
|
|
342
|
-
const {
|
|
329
|
+
const { existing } = await aws.fullstackPreflight(
|
|
343
330
|
slug,
|
|
344
331
|
_.repoSafe,
|
|
345
332
|
branch,
|
|
346
333
|
{
|
|
347
334
|
storage: answers.storage,
|
|
348
335
|
database: answers.database,
|
|
336
|
+
opensearch: answers.opensearch,
|
|
349
337
|
},
|
|
350
338
|
);
|
|
351
339
|
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
340
|
+
if (
|
|
341
|
+
existing.environment &&
|
|
342
|
+
existing.environment.Status !== "Ready"
|
|
343
|
+
) {
|
|
356
344
|
console.log(
|
|
357
345
|
utilities.colorize(
|
|
358
|
-
"
|
|
346
|
+
`\n ✗ EB environment "${slug}" is currently ${existing.environment.Status}, cannot re-provision\n`,
|
|
359
347
|
"error",
|
|
360
348
|
),
|
|
361
349
|
);
|
|
362
350
|
process.exit(1);
|
|
363
351
|
}
|
|
364
352
|
|
|
353
|
+
if (existing.environment) {
|
|
354
|
+
new utilities.Spinner(
|
|
355
|
+
"EB environment already exists, skipping creation",
|
|
356
|
+
true,
|
|
357
|
+
).ora.info();
|
|
358
|
+
existingEnvironment = existing.environment;
|
|
359
|
+
}
|
|
360
|
+
|
|
365
361
|
if (existing.storage) {
|
|
366
362
|
new utilities.Spinner(
|
|
367
363
|
"S3 bucket already exists, skipping storage creation",
|
|
368
364
|
true,
|
|
369
365
|
).ora.info();
|
|
370
|
-
|
|
366
|
+
existingResources.storage = true;
|
|
371
367
|
}
|
|
372
368
|
|
|
373
369
|
if (existing.database) {
|
|
@@ -375,7 +371,52 @@ module.exports = [
|
|
|
375
371
|
"RDS instance already exists, skipping database creation",
|
|
376
372
|
true,
|
|
377
373
|
).ora.info();
|
|
378
|
-
|
|
374
|
+
existingResources.database = true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (existing.opensearch) {
|
|
378
|
+
new utilities.Spinner(
|
|
379
|
+
"OpenSearch domain already exists, skipping creation",
|
|
380
|
+
true,
|
|
381
|
+
).ora.info();
|
|
382
|
+
existingResources.opensearch = true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Snapshot restore prompt — only if database is needed and doesn't already exist
|
|
386
|
+
if (answers.database && !existing.database) {
|
|
387
|
+
const { restore } = await inquirer.prompt([
|
|
388
|
+
{
|
|
389
|
+
type: "confirm",
|
|
390
|
+
name: "restore",
|
|
391
|
+
message: "Restore database from an existing snapshot?",
|
|
392
|
+
default: false,
|
|
393
|
+
},
|
|
394
|
+
]);
|
|
395
|
+
|
|
396
|
+
if (restore) {
|
|
397
|
+
const snapshots = await aws.rds.listManualSnapshots();
|
|
398
|
+
|
|
399
|
+
if (snapshots.length) {
|
|
400
|
+
const { selected } = await inquirer.prompt([
|
|
401
|
+
{
|
|
402
|
+
type: "list",
|
|
403
|
+
name: "selected",
|
|
404
|
+
message: "Select a snapshot to restore from",
|
|
405
|
+
choices: snapshots.map((s) => ({
|
|
406
|
+
name: `${s.DBSnapshotIdentifier} (source: ${s.DBInstanceIdentifier}, created: ${new Date(s.SnapshotCreateTime).toLocaleDateString()})`,
|
|
407
|
+
value: s.DBSnapshotIdentifier,
|
|
408
|
+
})),
|
|
409
|
+
},
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
snapshot = selected;
|
|
413
|
+
} else {
|
|
414
|
+
new utilities.Spinner(
|
|
415
|
+
"No manual snapshots found, creating a fresh database",
|
|
416
|
+
true,
|
|
417
|
+
).ora.info();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
379
420
|
}
|
|
380
421
|
|
|
381
422
|
existingSecrets = existing.secrets;
|
|
@@ -400,6 +441,92 @@ module.exports = [
|
|
|
400
441
|
true,
|
|
401
442
|
).ora.info();
|
|
402
443
|
}
|
|
444
|
+
|
|
445
|
+
if (existingSecrets.opensearch) {
|
|
446
|
+
new utilities.Spinner(
|
|
447
|
+
"OpenSearch secret already exists, reusing existing values",
|
|
448
|
+
true,
|
|
449
|
+
).ora.info();
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// EB-specific prompts — only for new environments
|
|
453
|
+
if (!existingEnvironment && answers.availability === "high") {
|
|
454
|
+
answers = {
|
|
455
|
+
...answers,
|
|
456
|
+
...(await inquirer.prompt([
|
|
457
|
+
{
|
|
458
|
+
type: "list",
|
|
459
|
+
name: "elbScheme",
|
|
460
|
+
message: "Load balancer visibility?",
|
|
461
|
+
choices: ["public", "internal"],
|
|
462
|
+
},
|
|
463
|
+
])),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// App name and domain prompts — only for new Laravel environments
|
|
468
|
+
if (!existingEnvironment && answers.framework === "laravel") {
|
|
469
|
+
answers = {
|
|
470
|
+
...answers,
|
|
471
|
+
...(await inquirer.prompt([
|
|
472
|
+
{
|
|
473
|
+
type: "input",
|
|
474
|
+
name: "appName",
|
|
475
|
+
message: "What is the app name?",
|
|
476
|
+
default: _.repoSafe,
|
|
477
|
+
validate: (v) =>
|
|
478
|
+
/^[a-zA-Z0-9 ]+$/.test(v) ||
|
|
479
|
+
"Alphanumeric characters and spaces only",
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
type: "input",
|
|
483
|
+
name: "appDomain",
|
|
484
|
+
message:
|
|
485
|
+
"What domain will this site be served at? (DNS must be configured manually)",
|
|
486
|
+
filter: (v) =>
|
|
487
|
+
v
|
|
488
|
+
.replace(/^https?:\/\//, "")
|
|
489
|
+
.replace(/\/+$/, ""),
|
|
490
|
+
validate: (v) =>
|
|
491
|
+
v.length > 0 || "Domain is required",
|
|
492
|
+
},
|
|
493
|
+
])),
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// ACM cert selection — only for high availability (ALB)
|
|
497
|
+
if (answers.availability === "high") {
|
|
498
|
+
const matchingCerts =
|
|
499
|
+
await aws.acm.findCertificatesForDomain(
|
|
500
|
+
answers.appDomain,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (matchingCerts.length) {
|
|
504
|
+
answers = {
|
|
505
|
+
...answers,
|
|
506
|
+
...(await inquirer.prompt([
|
|
507
|
+
{
|
|
508
|
+
type: "list",
|
|
509
|
+
name: "certArn",
|
|
510
|
+
message:
|
|
511
|
+
"Which ACM certificate should be used for HTTPS?",
|
|
512
|
+
choices: [
|
|
513
|
+
...matchingCerts.map((c) => ({
|
|
514
|
+
name: c.DomainName,
|
|
515
|
+
value: c.CertificateArn,
|
|
516
|
+
})),
|
|
517
|
+
{ name: "None", value: null },
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
])),
|
|
521
|
+
};
|
|
522
|
+
} else {
|
|
523
|
+
new utilities.Spinner(
|
|
524
|
+
`No ACM certificates match ${answers.appDomain}, HTTPS listener must be configured manually`,
|
|
525
|
+
true,
|
|
526
|
+
).ora.info();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
403
530
|
}
|
|
404
531
|
|
|
405
532
|
try {
|
|
@@ -432,8 +559,15 @@ module.exports = [
|
|
|
432
559
|
answers.availability,
|
|
433
560
|
answers.database,
|
|
434
561
|
answers.storage,
|
|
562
|
+
answers.opensearch,
|
|
435
563
|
existingSecrets,
|
|
436
564
|
snapshot,
|
|
565
|
+
answers.elbScheme || "public",
|
|
566
|
+
existingEnvironment,
|
|
567
|
+
existingResources,
|
|
568
|
+
answers.appName,
|
|
569
|
+
answers.appDomain,
|
|
570
|
+
answers.certArn,
|
|
437
571
|
);
|
|
438
572
|
}
|
|
439
573
|
} catch (e) {
|
|
@@ -4,6 +4,7 @@ const aws = require("../services/aws/index.js");
|
|
|
4
4
|
const { setAWSClientDefaults } = require("../services/aws/misc.js");
|
|
5
5
|
const utilities = require("../libs/utilities");
|
|
6
6
|
const { client, region } = require("../libs/prompts.js");
|
|
7
|
+
const { secretGroups } = require("../libs/vars.js");
|
|
7
8
|
|
|
8
9
|
const SECRETS_NAMESPACE = "aws:elasticbeanstalk:application:environmentsecrets";
|
|
9
10
|
|
|
@@ -72,7 +73,7 @@ module.exports = [
|
|
|
72
73
|
|
|
73
74
|
// Build expected environmentsecrets from Secrets Manager
|
|
74
75
|
const expectedSettings = [];
|
|
75
|
-
const groups =
|
|
76
|
+
const groups = secretGroups;
|
|
76
77
|
|
|
77
78
|
for (const group of groups) {
|
|
78
79
|
const secretName = `${slug}-${group}`;
|
|
@@ -178,6 +178,8 @@ module.exports.templates = [
|
|
|
178
178
|
},
|
|
179
179
|
];
|
|
180
180
|
|
|
181
|
+
module.exports.secretGroups = ["app", "storage", "database", "opensearch"];
|
|
182
|
+
|
|
181
183
|
module.exports.frameworks = [
|
|
182
184
|
{
|
|
183
185
|
name: "wordpress",
|
|
@@ -207,50 +209,7 @@ module.exports.eb = {
|
|
|
207
209
|
python: {
|
|
208
210
|
shared: [],
|
|
209
211
|
low: [],
|
|
210
|
-
high: [
|
|
211
|
-
{
|
|
212
|
-
OptionName: "ELBScheme",
|
|
213
|
-
Value: "internal",
|
|
214
|
-
Namespace: "aws:ec2:vpc",
|
|
215
|
-
},
|
|
216
|
-
() => {
|
|
217
|
-
const regions = {
|
|
218
|
-
"us-east-1": {
|
|
219
|
-
VPCId: "vpc-d30bcca8",
|
|
220
|
-
Subnets:
|
|
221
|
-
"subnet-00bf5eda896fdd5c5,subnet-0aa7e40d4701683ae",
|
|
222
|
-
},
|
|
223
|
-
"eu-west-1": {
|
|
224
|
-
VPCId: "vpc-cc0d9aa9",
|
|
225
|
-
Subnets:
|
|
226
|
-
"subnet-0e357f06e047e1c10,subnet-0956ffb043535455d",
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
return [
|
|
231
|
-
{
|
|
232
|
-
OptionName: "VPCId",
|
|
233
|
-
Value: regions[process.env.AWS_REGION].VPCId,
|
|
234
|
-
Namespace: "aws:ec2:vpc",
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
OptionName: "Subnets",
|
|
238
|
-
Value: regions[process.env.AWS_REGION].Subnets,
|
|
239
|
-
Namespace: "aws:ec2:vpc",
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
OptionName: "ELBSubnets",
|
|
243
|
-
Value: regions[process.env.AWS_REGION].Subnets,
|
|
244
|
-
Namespace: "aws:ec2:vpc",
|
|
245
|
-
},
|
|
246
|
-
];
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
OptionName: "AssociatePublicIpAddress",
|
|
250
|
-
Value: "false",
|
|
251
|
-
Namespace: "aws:ec2:vpc",
|
|
252
|
-
},
|
|
253
|
-
],
|
|
212
|
+
high: [],
|
|
254
213
|
},
|
|
255
214
|
php: {
|
|
256
215
|
shared: [],
|
|
@@ -304,11 +263,11 @@ module.exports.eb = {
|
|
|
304
263
|
Value: "lab-env-aws-elasticbeanstalk-service-role",
|
|
305
264
|
Namespace: "aws:elasticbeanstalk:environment",
|
|
306
265
|
},
|
|
307
|
-
{
|
|
266
|
+
({ instanceProfileName }) => ({
|
|
308
267
|
OptionName: "IamInstanceProfile",
|
|
309
|
-
Value: "aws-elasticbeanstalk-ec2-role",
|
|
268
|
+
Value: instanceProfileName || "aws-elasticbeanstalk-ec2-role",
|
|
310
269
|
Namespace: "aws:autoscaling:launchconfiguration",
|
|
311
|
-
},
|
|
270
|
+
}),
|
|
312
271
|
],
|
|
313
272
|
low: [
|
|
314
273
|
{
|
|
@@ -358,6 +317,43 @@ module.exports.eb = {
|
|
|
358
317
|
Value: "application",
|
|
359
318
|
Namespace: "aws:elasticbeanstalk:environment",
|
|
360
319
|
},
|
|
320
|
+
({ vpcId, privateSubnetIds, publicSubnetIds, elbScheme }) => {
|
|
321
|
+
const settings = [
|
|
322
|
+
{
|
|
323
|
+
OptionName: "VPCId",
|
|
324
|
+
Value: vpcId,
|
|
325
|
+
Namespace: "aws:ec2:vpc",
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
OptionName: "Subnets",
|
|
329
|
+
Value: privateSubnetIds,
|
|
330
|
+
Namespace: "aws:ec2:vpc",
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
OptionName: "ELBSubnets",
|
|
334
|
+
Value:
|
|
335
|
+
elbScheme === "internal"
|
|
336
|
+
? privateSubnetIds
|
|
337
|
+
: publicSubnetIds,
|
|
338
|
+
Namespace: "aws:ec2:vpc",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
OptionName: "AssociatePublicIpAddress",
|
|
342
|
+
Value: "false",
|
|
343
|
+
Namespace: "aws:ec2:vpc",
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
if (elbScheme === "internal") {
|
|
348
|
+
settings.push({
|
|
349
|
+
OptionName: "ELBScheme",
|
|
350
|
+
Value: "internal",
|
|
351
|
+
Namespace: "aws:ec2:vpc",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return settings;
|
|
356
|
+
},
|
|
361
357
|
],
|
|
362
358
|
},
|
|
363
359
|
secretPayloads: {
|
|
@@ -435,20 +431,24 @@ module.exports.eb = {
|
|
|
435
431
|
laravel: {
|
|
436
432
|
shared: {
|
|
437
433
|
app: {
|
|
434
|
+
APP_NAME: "<%= APP_NAME %>",
|
|
438
435
|
APP_ENV: "production",
|
|
439
436
|
APP_KEY: `base64:${Buffer.from(generator.generate({ length: 32, numbers: true, symbols: true })).toString("base64")}`,
|
|
440
437
|
APP_DEBUG: "false",
|
|
441
|
-
APP_URL: "https://<%=
|
|
438
|
+
APP_URL: "https://<%= APP_DOMAIN %>",
|
|
442
439
|
},
|
|
443
440
|
storage: ({ storage }) =>
|
|
444
441
|
storage && {
|
|
445
442
|
AWS_BUCKET: "<%= s3Slug %>",
|
|
446
443
|
AWS_DEFAULT_REGION: "<%= AWS_REGION %>",
|
|
447
|
-
|
|
448
|
-
AWS_SECRET_ACCESS_KEY: "<%= SecretAccessKey %>",
|
|
444
|
+
AWS_ACL_VISIBILITY: "private",
|
|
449
445
|
FILESYSTEM_DISK: "s3",
|
|
450
446
|
FILESYSTEM_DISK_PUBLIC: "s3-public",
|
|
451
447
|
},
|
|
448
|
+
opensearch: ({ opensearch }) =>
|
|
449
|
+
opensearch && {
|
|
450
|
+
SEARCH_HOST: "<%= SEARCH_HOST %>",
|
|
451
|
+
},
|
|
452
452
|
},
|
|
453
453
|
high: {
|
|
454
454
|
app: {
|
|
@@ -488,7 +488,7 @@ module.exports.eb = {
|
|
|
488
488
|
},
|
|
489
489
|
buildSecretPayloads(config, data = {}) {
|
|
490
490
|
const { framework, availability = "low", platform, language } = config;
|
|
491
|
-
const result = { app: {}, storage: {}, database: {} };
|
|
491
|
+
const result = { app: {}, storage: {}, database: {}, opensearch: {} };
|
|
492
492
|
|
|
493
493
|
const sources = [
|
|
494
494
|
this.secretPayloads.shared,
|
|
@@ -503,7 +503,7 @@ module.exports.eb = {
|
|
|
503
503
|
|
|
504
504
|
for (const source of sources) {
|
|
505
505
|
if (!source) continue;
|
|
506
|
-
for (const group of
|
|
506
|
+
for (const group of module.exports.secretGroups) {
|
|
507
507
|
let values = source[group];
|
|
508
508
|
if (typeof values === "function") {
|
|
509
509
|
values = values(config, data);
|
|
@@ -516,7 +516,7 @@ module.exports.eb = {
|
|
|
516
516
|
|
|
517
517
|
// Apply lodash template interpolation
|
|
518
518
|
const templateData = { ...process.env, ...data };
|
|
519
|
-
for (const group of
|
|
519
|
+
for (const group of module.exports.secretGroups) {
|
|
520
520
|
if (Object.keys(result[group]).length) {
|
|
521
521
|
result[group] = JSON.parse(
|
|
522
522
|
template(JSON.stringify(result[group]))(templateData),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { ACMClient, ListCertificatesCommand } = require("@aws-sdk/client-acm");
|
|
2
|
+
const { Spinner } = require("../../libs/utilities");
|
|
3
|
+
|
|
4
|
+
module.exports.listCertificates = async () => {
|
|
5
|
+
const client = new ACMClient({});
|
|
6
|
+
const certs = [];
|
|
7
|
+
let nextToken;
|
|
8
|
+
|
|
9
|
+
do {
|
|
10
|
+
const res = await client.send(
|
|
11
|
+
new ListCertificatesCommand({
|
|
12
|
+
CertificateStatuses: ["ISSUED"],
|
|
13
|
+
NextToken: nextToken,
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
certs.push(...(res.CertificateSummaryList || []));
|
|
17
|
+
nextToken = res.NextToken;
|
|
18
|
+
} while (nextToken);
|
|
19
|
+
|
|
20
|
+
return certs;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
module.exports.findCertificatesForDomain = async (domain) => {
|
|
24
|
+
const certs = await Spinner.prototype.simple(
|
|
25
|
+
`Looking up ACM certificates for ${domain}`,
|
|
26
|
+
() => module.exports.listCertificates(),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (!certs.length) return [];
|
|
30
|
+
|
|
31
|
+
// Extract apex domain (e.g. "test.avh-det.com" → "avh-det.com")
|
|
32
|
+
const parts = domain.split(".");
|
|
33
|
+
const apex = parts.length > 2 ? parts.slice(-2).join(".") : domain;
|
|
34
|
+
const wildcard = `*.${apex}`;
|
|
35
|
+
|
|
36
|
+
// Check all cert domains (DomainName + SANs)
|
|
37
|
+
const getCertDomains = (cert) => {
|
|
38
|
+
const domains = [cert.DomainName];
|
|
39
|
+
if (cert.SubjectAlternativeNameSummaries) {
|
|
40
|
+
domains.push(...cert.SubjectAlternativeNameSummaries);
|
|
41
|
+
}
|
|
42
|
+
return domains;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const candidates = [domain, wildcard];
|
|
46
|
+
if (apex !== domain) candidates.push(apex);
|
|
47
|
+
|
|
48
|
+
const matches = certs.filter((c) =>
|
|
49
|
+
getCertDomains(c).some((d) => candidates.includes(d)),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Deduplicate by ARN
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
return matches.filter((c) => {
|
|
55
|
+
if (seen.has(c.CertificateArn)) return false;
|
|
56
|
+
seen.add(c.CertificateArn);
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
};
|