@fishawack/lab-env 5.6.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 +35 -0
- package/_Test/provision.js +3 -1
- package/cli.js +1 -0
- package/commands/create/cmds/deprovision.js +37 -6
- package/commands/create/cmds/provision.js +250 -1
- package/commands/create/cmds/sync-secrets.js +198 -0
- package/commands/create/libs/utilities.js +2 -0
- package/commands/create/libs/vars.js +218 -303
- package/commands/create/services/aws/acm.js +59 -0
- package/commands/create/services/aws/ec2.js +699 -2
- package/commands/create/services/aws/elasticbeanstalk.js +70 -0
- package/commands/create/services/aws/iam.js +268 -0
- package/commands/create/services/aws/index.js +804 -52
- package/commands/create/services/aws/opensearch.js +147 -0
- package/commands/create/services/aws/rds.js +284 -0
- package/commands/create/services/aws/s3.js +41 -7
- package/commands/create/services/aws/secretsmanager.js +89 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/rds-ssl.config +5 -0
- package/commands/create/templates/elasticbeanstalk/.ebextensions/misc/setvars.config +16 -1
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
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
|
+
|
|
19
|
+
### 5.7.0-beta.1 (2026-06-08)
|
|
20
|
+
|
|
21
|
+
#### Features
|
|
22
|
+
|
|
23
|
+
* added a sync secrets command ([c98c66d](https://bitbucket.org/fishawackdigital/lab-env/commits/c98c66dc0d92bdac9dd79ae544362e60bae4ac88))
|
|
24
|
+
* added the ability to restore from snapshot ([c0f3c6c](https://bitbucket.org/fishawackdigital/lab-env/commits/c0f3c6c5ee841121e78bc38047e40336c5880ef0))
|
|
25
|
+
* elasticbeanstlak now fully utilizes secrets manager and has options to persist after delete ([bbb252c](https://bitbucket.org/fishawackdigital/lab-env/commits/bbb252cf5e2f07551c63df6fcebd574c1a84d9ab))
|
|
26
|
+
* elb instance profile and managed update profile now include reading secretmanager permissions ([9059b01](https://bitbucket.org/fishawackdigital/lab-env/commits/9059b010ce781e07f3630aba5bb0b88556bed929))
|
|
27
|
+
* encryption in transit for db connections by default for laravel applications and enforcement ([cac6590](https://bitbucket.org/fishawackdigital/lab-env/commits/cac659083dfef46eb047b943451be854d4daf94f))
|
|
28
|
+
* rds and s3 no fully decoupled from fullstack and can be reattached to recreated elb instances ([72d3361](https://bitbucket.org/fishawackdigital/lab-env/commits/72d3361441e37d70f8b78dc98850828a92ea48a1))
|
|
29
|
+
* setvars now utilizes secret manager secrets instead of plain text ([462ef90](https://bitbucket.org/fishawackdigital/lab-env/commits/462ef9077dad31b420355038c3264abf471164b9))
|
|
30
|
+
|
|
31
|
+
#### Bug Fixes
|
|
32
|
+
|
|
33
|
+
* added secret manager permission to fullstack deploy permissions ([6432d2d](https://bitbucket.org/fishawackdigital/lab-env/commits/6432d2d756e19e96c62d8c29dd79828811ecf1ac))
|
|
34
|
+
* disable public access to RDS instances ([d4759e8](https://bitbucket.org/fishawackdigital/lab-env/commits/d4759e8ac11ecfbb22b0c3c80c96d5781ae8bda9))
|
|
35
|
+
* session driver now uses database by default ([d745615](https://bitbucket.org/fishawackdigital/lab-env/commits/d7456154920ab380e2c01424e065c9aca6edf750))
|
|
36
|
+
* use correct spinner class for logging ([592d7d5](https://bitbucket.org/fishawackdigital/lab-env/commits/592d7d56cf83ebfd62ce9d025450a8717222e8e0))
|
|
37
|
+
|
|
3
38
|
### 5.6.1 (2026-06-03)
|
|
4
39
|
|
|
5
40
|
#### Bug Fixes
|
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",
|
package/cli.js
CHANGED
|
@@ -78,11 +78,42 @@ module.exports = [
|
|
|
78
78
|
// Set AWS client defaults before any AWS operations
|
|
79
79
|
setAWSClientDefaults(answers.client, answers.region);
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
if (answers.stack === "fullstack") {
|
|
82
|
+
const resourceAnswers = await inquirer.prompt([
|
|
83
|
+
{
|
|
84
|
+
type: "confirm",
|
|
85
|
+
name: "deleteStorage",
|
|
86
|
+
message: "Delete the S3 storage bucket?",
|
|
87
|
+
default: false,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: "confirm",
|
|
91
|
+
name: "deleteDatabase",
|
|
92
|
+
message: "Delete the RDS database?",
|
|
93
|
+
default: false,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: "confirm",
|
|
97
|
+
name: "deleteOpenSearch",
|
|
98
|
+
message: "Delete the OpenSearch domain?",
|
|
99
|
+
default: false,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
type: "confirm",
|
|
103
|
+
name: "deleteAppSecret",
|
|
104
|
+
message: "Delete the app secret (APP_KEY, salts, etc.)?",
|
|
105
|
+
default: false,
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
await aws.fullstackTerminate(slug, _.repoSafe, branch, {
|
|
110
|
+
deleteStorage: resourceAnswers.deleteStorage,
|
|
111
|
+
deleteDatabase: resourceAnswers.deleteDatabase,
|
|
112
|
+
deleteOpenSearch: resourceAnswers.deleteOpenSearch,
|
|
113
|
+
deleteAppSecret: resourceAnswers.deleteAppSecret,
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
await aws.staticTerminate(slug, _.repoSafe, branch, answers.id);
|
|
117
|
+
}
|
|
87
118
|
},
|
|
88
119
|
];
|
|
@@ -213,15 +213,35 @@ module.exports = [
|
|
|
213
213
|
message: "What availability is required?",
|
|
214
214
|
choices: ["low", "high"],
|
|
215
215
|
},
|
|
216
|
+
region,
|
|
216
217
|
{
|
|
217
218
|
type: "confirm",
|
|
218
219
|
name: "database",
|
|
219
220
|
message: "Does this site need a database?",
|
|
220
221
|
default: true,
|
|
221
222
|
},
|
|
222
|
-
|
|
223
|
+
{
|
|
224
|
+
type: "confirm",
|
|
225
|
+
name: "storage",
|
|
226
|
+
message: "Does this site need S3 storage?",
|
|
227
|
+
default: true,
|
|
228
|
+
},
|
|
223
229
|
])),
|
|
224
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
|
+
}
|
|
225
245
|
}
|
|
226
246
|
|
|
227
247
|
answers = {
|
|
@@ -290,6 +310,225 @@ module.exports = [
|
|
|
290
310
|
// Set AWS client defaults before any AWS operations
|
|
291
311
|
setAWSClientDefaults(answers.client, answers.region);
|
|
292
312
|
|
|
313
|
+
// Pre-flight check for existing resources
|
|
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;
|
|
327
|
+
|
|
328
|
+
if (answers.stack === "fullstack") {
|
|
329
|
+
const { existing } = await aws.fullstackPreflight(
|
|
330
|
+
slug,
|
|
331
|
+
_.repoSafe,
|
|
332
|
+
branch,
|
|
333
|
+
{
|
|
334
|
+
storage: answers.storage,
|
|
335
|
+
database: answers.database,
|
|
336
|
+
opensearch: answers.opensearch,
|
|
337
|
+
},
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
if (
|
|
341
|
+
existing.environment &&
|
|
342
|
+
existing.environment.Status !== "Ready"
|
|
343
|
+
) {
|
|
344
|
+
console.log(
|
|
345
|
+
utilities.colorize(
|
|
346
|
+
`\n ✗ EB environment "${slug}" is currently ${existing.environment.Status}, cannot re-provision\n`,
|
|
347
|
+
"error",
|
|
348
|
+
),
|
|
349
|
+
);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
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
|
+
|
|
361
|
+
if (existing.storage) {
|
|
362
|
+
new utilities.Spinner(
|
|
363
|
+
"S3 bucket already exists, skipping storage creation",
|
|
364
|
+
true,
|
|
365
|
+
).ora.info();
|
|
366
|
+
existingResources.storage = true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (existing.database) {
|
|
370
|
+
new utilities.Spinner(
|
|
371
|
+
"RDS instance already exists, skipping database creation",
|
|
372
|
+
true,
|
|
373
|
+
).ora.info();
|
|
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
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
existingSecrets = existing.secrets;
|
|
423
|
+
|
|
424
|
+
if (existingSecrets.app) {
|
|
425
|
+
new utilities.Spinner(
|
|
426
|
+
"App secret already exists, reusing existing values",
|
|
427
|
+
true,
|
|
428
|
+
).ora.info();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (existingSecrets.storage) {
|
|
432
|
+
new utilities.Spinner(
|
|
433
|
+
"Storage secret already exists, reusing existing values",
|
|
434
|
+
true,
|
|
435
|
+
).ora.info();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (existingSecrets.database) {
|
|
439
|
+
new utilities.Spinner(
|
|
440
|
+
"Database secret already exists, reusing existing values",
|
|
441
|
+
true,
|
|
442
|
+
).ora.info();
|
|
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
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
293
532
|
try {
|
|
294
533
|
if (answers.stack === "static") {
|
|
295
534
|
infastructure = await aws.static(
|
|
@@ -319,6 +558,16 @@ module.exports = [
|
|
|
319
558
|
answers.framework,
|
|
320
559
|
answers.availability,
|
|
321
560
|
answers.database,
|
|
561
|
+
answers.storage,
|
|
562
|
+
answers.opensearch,
|
|
563
|
+
existingSecrets,
|
|
564
|
+
snapshot,
|
|
565
|
+
answers.elbScheme || "public",
|
|
566
|
+
existingEnvironment,
|
|
567
|
+
existingResources,
|
|
568
|
+
answers.appName,
|
|
569
|
+
answers.appDomain,
|
|
570
|
+
answers.certArn,
|
|
322
571
|
);
|
|
323
572
|
}
|
|
324
573
|
} catch (e) {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const _ = require("../../../globals.js");
|
|
2
|
+
const inquirer = require("inquirer");
|
|
3
|
+
const aws = require("../services/aws/index.js");
|
|
4
|
+
const { setAWSClientDefaults } = require("../services/aws/misc.js");
|
|
5
|
+
const utilities = require("../libs/utilities");
|
|
6
|
+
const { client, region } = require("../libs/prompts.js");
|
|
7
|
+
const { secretGroups } = require("../libs/vars.js");
|
|
8
|
+
|
|
9
|
+
const SECRETS_NAMESPACE = "aws:elasticbeanstalk:application:environmentsecrets";
|
|
10
|
+
|
|
11
|
+
module.exports = [
|
|
12
|
+
["sync-secrets", "syncs"],
|
|
13
|
+
_.config.preset === "devops"
|
|
14
|
+
? "Sync Secrets Manager keys with EB environment secrets"
|
|
15
|
+
: false,
|
|
16
|
+
(yargs) => {
|
|
17
|
+
yargs.option("branch", {
|
|
18
|
+
alias: "b",
|
|
19
|
+
describe: "Branch to configure",
|
|
20
|
+
type: "string",
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
async (argv) => {
|
|
24
|
+
let branch = argv.branch || _.branch;
|
|
25
|
+
|
|
26
|
+
let answer = await inquirer.prompt([
|
|
27
|
+
{
|
|
28
|
+
type: "confirm",
|
|
29
|
+
name: "check",
|
|
30
|
+
message: `Syncing secrets for the ${utilities.colorize(branch, "success")} branch, is this correct?`,
|
|
31
|
+
default: "Y",
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
if (!answer.check) {
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let answers = await inquirer.prompt([client, region]);
|
|
40
|
+
|
|
41
|
+
const slug = aws.slug(_.repoSafe, answers.client, branch, "eb");
|
|
42
|
+
|
|
43
|
+
setAWSClientDefaults(answers.client, answers.region);
|
|
44
|
+
|
|
45
|
+
// Verify EB environment exists
|
|
46
|
+
let appName;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const { Environments } =
|
|
50
|
+
await aws.elasticbeanstalk.describeEnvironment(slug);
|
|
51
|
+
const active = Environments.filter(
|
|
52
|
+
(e) => e.Status !== "Terminated",
|
|
53
|
+
);
|
|
54
|
+
if (!active.length) {
|
|
55
|
+
console.log(
|
|
56
|
+
utilities.colorize(
|
|
57
|
+
`\n ✗ No active EB environment found for "${slug}"\n`,
|
|
58
|
+
"error",
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
appName = active[0].ApplicationName;
|
|
64
|
+
} catch {
|
|
65
|
+
console.log(
|
|
66
|
+
utilities.colorize(
|
|
67
|
+
`\n ✗ No EB environment found for "${slug}"\n`,
|
|
68
|
+
"error",
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Build expected environmentsecrets from Secrets Manager
|
|
75
|
+
const expectedSettings = [];
|
|
76
|
+
const groups = secretGroups;
|
|
77
|
+
|
|
78
|
+
for (const group of groups) {
|
|
79
|
+
const secretName = `${slug}-${group}`;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const secret =
|
|
83
|
+
await aws.secretsmanager.describeSecret(secretName);
|
|
84
|
+
const secretValue =
|
|
85
|
+
await aws.secretsmanager.getSecretValue(secretName);
|
|
86
|
+
const keys = Object.keys(secretValue);
|
|
87
|
+
|
|
88
|
+
for (const key of keys) {
|
|
89
|
+
expectedSettings.push({
|
|
90
|
+
OptionName: key,
|
|
91
|
+
Value: `${secret.ARN}:${key}`,
|
|
92
|
+
Namespace: SECRETS_NAMESPACE,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
/* secret does not exist for this group */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Get current EB configuration
|
|
101
|
+
const currentSettings =
|
|
102
|
+
await aws.elasticbeanstalk.describeConfigurationSettings(
|
|
103
|
+
appName,
|
|
104
|
+
slug,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const currentSecrets = currentSettings.filter(
|
|
108
|
+
(s) => s.Namespace === SECRETS_NAMESPACE,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Diff: what to add and what to remove
|
|
112
|
+
const currentKeys = new Set(currentSecrets.map((s) => s.OptionName));
|
|
113
|
+
const expectedKeys = new Set(expectedSettings.map((s) => s.OptionName));
|
|
114
|
+
|
|
115
|
+
const toAdd = expectedSettings.filter(
|
|
116
|
+
(s) => !currentKeys.has(s.OptionName),
|
|
117
|
+
);
|
|
118
|
+
const toUpdate = expectedSettings.filter((s) => {
|
|
119
|
+
const current = currentSecrets.find(
|
|
120
|
+
(c) => c.OptionName === s.OptionName,
|
|
121
|
+
);
|
|
122
|
+
return current && current.Value !== s.Value;
|
|
123
|
+
});
|
|
124
|
+
const toRemove = currentSecrets
|
|
125
|
+
.filter((s) => !expectedKeys.has(s.OptionName))
|
|
126
|
+
.map((s) => ({
|
|
127
|
+
OptionName: s.OptionName,
|
|
128
|
+
Namespace: SECRETS_NAMESPACE,
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
const optionSettings = [...toAdd, ...toUpdate];
|
|
132
|
+
|
|
133
|
+
if (!optionSettings.length && !toRemove.length) {
|
|
134
|
+
new utilities.Spinner(
|
|
135
|
+
"Environment secrets are in sync",
|
|
136
|
+
true,
|
|
137
|
+
).ora.info();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Log summary
|
|
142
|
+
if (toAdd.length) {
|
|
143
|
+
console.log(
|
|
144
|
+
utilities.colorize(
|
|
145
|
+
`\n Adding ${toAdd.length} secret(s): ${toAdd.map((s) => s.OptionName).join(", ")}`,
|
|
146
|
+
"success",
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (toUpdate.length) {
|
|
152
|
+
console.log(
|
|
153
|
+
utilities.colorize(
|
|
154
|
+
`\n Updating ${toUpdate.length} secret(s): ${toUpdate.map((s) => s.OptionName).join(", ")}`,
|
|
155
|
+
"info",
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (toRemove.length) {
|
|
161
|
+
console.log(
|
|
162
|
+
utilities.colorize(
|
|
163
|
+
`\n Removing ${toRemove.length} secret(s): ${toRemove.map((s) => s.OptionName).join(", ")}`,
|
|
164
|
+
"warning",
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log("");
|
|
170
|
+
|
|
171
|
+
answer = await inquirer.prompt([
|
|
172
|
+
{
|
|
173
|
+
type: "confirm",
|
|
174
|
+
name: "proceed",
|
|
175
|
+
message: "Apply these changes?",
|
|
176
|
+
default: true,
|
|
177
|
+
},
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
if (!answer.proceed) {
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await aws.elasticbeanstalk.updateEnvironmentSettings(
|
|
185
|
+
slug,
|
|
186
|
+
optionSettings,
|
|
187
|
+
toRemove,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
console.log(
|
|
191
|
+
// eslint-disable-next-line prettier/prettier
|
|
192
|
+
utilities.colorize(
|
|
193
|
+
"\n ✔ Environment secrets synced\n",
|
|
194
|
+
"success",
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
];
|