@fishawack/lab-env 5.6.1-beta.1 → 5.7.0-beta.1

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 CHANGED
@@ -1,5 +1,30 @@
1
1
  ## Changelog
2
2
 
3
+ ### 5.7.0-beta.1 (2026-06-08)
4
+
5
+ #### Features
6
+
7
+ * added a sync secrets command ([c98c66d](https://bitbucket.org/fishawackdigital/lab-env/commits/c98c66dc0d92bdac9dd79ae544362e60bae4ac88))
8
+ * added the ability to restore from snapshot ([c0f3c6c](https://bitbucket.org/fishawackdigital/lab-env/commits/c0f3c6c5ee841121e78bc38047e40336c5880ef0))
9
+ * elasticbeanstlak now fully utilizes secrets manager and has options to persist after delete ([bbb252c](https://bitbucket.org/fishawackdigital/lab-env/commits/bbb252cf5e2f07551c63df6fcebd574c1a84d9ab))
10
+ * elb instance profile and managed update profile now include reading secretmanager permissions ([9059b01](https://bitbucket.org/fishawackdigital/lab-env/commits/9059b010ce781e07f3630aba5bb0b88556bed929))
11
+ * encryption in transit for db connections by default for laravel applications and enforcement ([cac6590](https://bitbucket.org/fishawackdigital/lab-env/commits/cac659083dfef46eb047b943451be854d4daf94f))
12
+ * 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))
13
+ * setvars now utilizes secret manager secrets instead of plain text ([462ef90](https://bitbucket.org/fishawackdigital/lab-env/commits/462ef9077dad31b420355038c3264abf471164b9))
14
+
15
+ #### Bug Fixes
16
+
17
+ * added secret manager permission to fullstack deploy permissions ([6432d2d](https://bitbucket.org/fishawackdigital/lab-env/commits/6432d2d756e19e96c62d8c29dd79828811ecf1ac))
18
+ * disable public access to RDS instances ([d4759e8](https://bitbucket.org/fishawackdigital/lab-env/commits/d4759e8ac11ecfbb22b0c3c80c96d5781ae8bda9))
19
+ * session driver now uses database by default ([d745615](https://bitbucket.org/fishawackdigital/lab-env/commits/d7456154920ab380e2c01424e065c9aca6edf750))
20
+ * use correct spinner class for logging ([592d7d5](https://bitbucket.org/fishawackdigital/lab-env/commits/592d7d56cf83ebfd62ce9d025450a8717222e8e0))
21
+
22
+ ### 5.6.1 (2026-06-03)
23
+
24
+ #### Bug Fixes
25
+
26
+ * trigger release ([fe4907d](https://bitbucket.org/fishawackdigital/lab-env/commits/fe4907daabd6c670e9b970f475f1b6b032871ea5))
27
+
3
28
  ### 5.6.1-beta.1 (2026-06-03)
4
29
 
5
30
  #### Bug Fixes
package/cli.js CHANGED
@@ -171,6 +171,7 @@ const args = hideBin(process.argv);
171
171
  "key",
172
172
  "dekey",
173
173
  "prune",
174
+ "sync-secrets",
174
175
  ].forEach((d) =>
175
176
  cli.command(...require(`./commands/create/cmds/${d}.js`)),
176
177
  );
@@ -78,11 +78,35 @@ module.exports = [
78
78
  // Set AWS client defaults before any AWS operations
79
79
  setAWSClientDefaults(answers.client, answers.region);
80
80
 
81
- await aws[`${answers.stack}Terminate`](
82
- slug,
83
- _.repoSafe,
84
- branch,
85
- answers.id,
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: "deleteAppSecret",
98
+ message: "Delete the app secret (APP_KEY, salts, etc.)?",
99
+ default: false,
100
+ },
101
+ ]);
102
+
103
+ await aws.fullstackTerminate(slug, _.repoSafe, branch, {
104
+ deleteStorage: resourceAnswers.deleteStorage,
105
+ deleteDatabase: resourceAnswers.deleteDatabase,
106
+ deleteAppSecret: resourceAnswers.deleteAppSecret,
107
+ });
108
+ } else {
109
+ await aws.staticTerminate(slug, _.repoSafe, branch, answers.id);
110
+ }
87
111
  },
88
112
  ];
@@ -213,13 +213,19 @@ 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
- region,
223
+ {
224
+ type: "confirm",
225
+ name: "storage",
226
+ message: "Does this site need S3 storage?",
227
+ default: true,
228
+ },
223
229
  ])),
224
230
  };
225
231
  }
@@ -290,6 +296,112 @@ module.exports = [
290
296
  // Set AWS client defaults before any AWS operations
291
297
  setAWSClientDefaults(answers.client, answers.region);
292
298
 
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
+ // Pre-flight check for existing resources
339
+ let existingSecrets = { app: null, storage: null, database: null };
340
+
341
+ if (answers.stack === "fullstack") {
342
+ const { conflicts, existing } = await aws.fullstackPreflight(
343
+ slug,
344
+ _.repoSafe,
345
+ branch,
346
+ {
347
+ storage: answers.storage,
348
+ database: answers.database,
349
+ },
350
+ );
351
+
352
+ if (conflicts.length) {
353
+ conflicts.forEach((c) =>
354
+ console.log(utilities.colorize(`\n ✗ ${c}`, "error")),
355
+ );
356
+ console.log(
357
+ utilities.colorize(
358
+ "\nProvisioning aborted due to existing resources.\n",
359
+ "error",
360
+ ),
361
+ );
362
+ process.exit(1);
363
+ }
364
+
365
+ if (existing.storage) {
366
+ new utilities.Spinner(
367
+ "S3 bucket already exists, skipping storage creation",
368
+ true,
369
+ ).ora.info();
370
+ answers.storage = false;
371
+ }
372
+
373
+ if (existing.database) {
374
+ new utilities.Spinner(
375
+ "RDS instance already exists, skipping database creation",
376
+ true,
377
+ ).ora.info();
378
+ answers.database = false;
379
+ }
380
+
381
+ existingSecrets = existing.secrets;
382
+
383
+ if (existingSecrets.app) {
384
+ new utilities.Spinner(
385
+ "App secret already exists, reusing existing values",
386
+ true,
387
+ ).ora.info();
388
+ }
389
+
390
+ if (existingSecrets.storage) {
391
+ new utilities.Spinner(
392
+ "Storage secret already exists, reusing existing values",
393
+ true,
394
+ ).ora.info();
395
+ }
396
+
397
+ if (existingSecrets.database) {
398
+ new utilities.Spinner(
399
+ "Database secret already exists, reusing existing values",
400
+ true,
401
+ ).ora.info();
402
+ }
403
+ }
404
+
293
405
  try {
294
406
  if (answers.stack === "static") {
295
407
  infastructure = await aws.static(
@@ -319,6 +431,9 @@ module.exports = [
319
431
  answers.framework,
320
432
  answers.availability,
321
433
  answers.database,
434
+ answers.storage,
435
+ existingSecrets,
436
+ snapshot,
322
437
  );
323
438
  }
324
439
  } catch (e) {
@@ -0,0 +1,197 @@
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
+
8
+ const SECRETS_NAMESPACE = "aws:elasticbeanstalk:application:environmentsecrets";
9
+
10
+ module.exports = [
11
+ ["sync-secrets", "syncs"],
12
+ _.config.preset === "devops"
13
+ ? "Sync Secrets Manager keys with EB environment secrets"
14
+ : false,
15
+ (yargs) => {
16
+ yargs.option("branch", {
17
+ alias: "b",
18
+ describe: "Branch to configure",
19
+ type: "string",
20
+ });
21
+ },
22
+ async (argv) => {
23
+ let branch = argv.branch || _.branch;
24
+
25
+ let answer = await inquirer.prompt([
26
+ {
27
+ type: "confirm",
28
+ name: "check",
29
+ message: `Syncing secrets for the ${utilities.colorize(branch, "success")} branch, is this correct?`,
30
+ default: "Y",
31
+ },
32
+ ]);
33
+
34
+ if (!answer.check) {
35
+ process.exit(1);
36
+ }
37
+
38
+ let answers = await inquirer.prompt([client, region]);
39
+
40
+ const slug = aws.slug(_.repoSafe, answers.client, branch, "eb");
41
+
42
+ setAWSClientDefaults(answers.client, answers.region);
43
+
44
+ // Verify EB environment exists
45
+ let appName;
46
+
47
+ try {
48
+ const { Environments } =
49
+ await aws.elasticbeanstalk.describeEnvironment(slug);
50
+ const active = Environments.filter(
51
+ (e) => e.Status !== "Terminated",
52
+ );
53
+ if (!active.length) {
54
+ console.log(
55
+ utilities.colorize(
56
+ `\n ✗ No active EB environment found for "${slug}"\n`,
57
+ "error",
58
+ ),
59
+ );
60
+ process.exit(1);
61
+ }
62
+ appName = active[0].ApplicationName;
63
+ } catch {
64
+ console.log(
65
+ utilities.colorize(
66
+ `\n ✗ No EB environment found for "${slug}"\n`,
67
+ "error",
68
+ ),
69
+ );
70
+ process.exit(1);
71
+ }
72
+
73
+ // Build expected environmentsecrets from Secrets Manager
74
+ const expectedSettings = [];
75
+ const groups = ["app", "storage", "database"];
76
+
77
+ for (const group of groups) {
78
+ const secretName = `${slug}-${group}`;
79
+
80
+ try {
81
+ const secret =
82
+ await aws.secretsmanager.describeSecret(secretName);
83
+ const secretValue =
84
+ await aws.secretsmanager.getSecretValue(secretName);
85
+ const keys = Object.keys(secretValue);
86
+
87
+ for (const key of keys) {
88
+ expectedSettings.push({
89
+ OptionName: key,
90
+ Value: `${secret.ARN}:${key}`,
91
+ Namespace: SECRETS_NAMESPACE,
92
+ });
93
+ }
94
+ } catch {
95
+ /* secret does not exist for this group */
96
+ }
97
+ }
98
+
99
+ // Get current EB configuration
100
+ const currentSettings =
101
+ await aws.elasticbeanstalk.describeConfigurationSettings(
102
+ appName,
103
+ slug,
104
+ );
105
+
106
+ const currentSecrets = currentSettings.filter(
107
+ (s) => s.Namespace === SECRETS_NAMESPACE,
108
+ );
109
+
110
+ // Diff: what to add and what to remove
111
+ const currentKeys = new Set(currentSecrets.map((s) => s.OptionName));
112
+ const expectedKeys = new Set(expectedSettings.map((s) => s.OptionName));
113
+
114
+ const toAdd = expectedSettings.filter(
115
+ (s) => !currentKeys.has(s.OptionName),
116
+ );
117
+ const toUpdate = expectedSettings.filter((s) => {
118
+ const current = currentSecrets.find(
119
+ (c) => c.OptionName === s.OptionName,
120
+ );
121
+ return current && current.Value !== s.Value;
122
+ });
123
+ const toRemove = currentSecrets
124
+ .filter((s) => !expectedKeys.has(s.OptionName))
125
+ .map((s) => ({
126
+ OptionName: s.OptionName,
127
+ Namespace: SECRETS_NAMESPACE,
128
+ }));
129
+
130
+ const optionSettings = [...toAdd, ...toUpdate];
131
+
132
+ if (!optionSettings.length && !toRemove.length) {
133
+ new utilities.Spinner(
134
+ "Environment secrets are in sync",
135
+ true,
136
+ ).ora.info();
137
+ return;
138
+ }
139
+
140
+ // Log summary
141
+ if (toAdd.length) {
142
+ console.log(
143
+ utilities.colorize(
144
+ `\n Adding ${toAdd.length} secret(s): ${toAdd.map((s) => s.OptionName).join(", ")}`,
145
+ "success",
146
+ ),
147
+ );
148
+ }
149
+
150
+ if (toUpdate.length) {
151
+ console.log(
152
+ utilities.colorize(
153
+ `\n Updating ${toUpdate.length} secret(s): ${toUpdate.map((s) => s.OptionName).join(", ")}`,
154
+ "info",
155
+ ),
156
+ );
157
+ }
158
+
159
+ if (toRemove.length) {
160
+ console.log(
161
+ utilities.colorize(
162
+ `\n Removing ${toRemove.length} secret(s): ${toRemove.map((s) => s.OptionName).join(", ")}`,
163
+ "warning",
164
+ ),
165
+ );
166
+ }
167
+
168
+ console.log("");
169
+
170
+ answer = await inquirer.prompt([
171
+ {
172
+ type: "confirm",
173
+ name: "proceed",
174
+ message: "Apply these changes?",
175
+ default: true,
176
+ },
177
+ ]);
178
+
179
+ if (!answer.proceed) {
180
+ process.exit(0);
181
+ }
182
+
183
+ await aws.elasticbeanstalk.updateEnvironmentSettings(
184
+ slug,
185
+ optionSettings,
186
+ toRemove,
187
+ );
188
+
189
+ console.log(
190
+ // eslint-disable-next-line prettier/prettier
191
+ utilities.colorize(
192
+ "\n ✔ Environment secrets synced\n",
193
+ "success",
194
+ ),
195
+ );
196
+ },
197
+ ];