@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 +25 -0
- package/cli.js +1 -0
- package/commands/create/cmds/deprovision.js +30 -6
- package/commands/create/cmds/provision.js +116 -1
- package/commands/create/cmds/sync-secrets.js +197 -0
- package/commands/create/libs/vars.js +171 -256
- package/commands/create/services/aws/ec2.js +150 -1
- package/commands/create/services/aws/elasticbeanstalk.js +54 -0
- package/commands/create/services/aws/iam.js +14 -0
- package/commands/create/services/aws/index.js +399 -42
- package/commands/create/services/aws/rds.js +246 -0
- package/commands/create/services/aws/secretsmanager.js +73 -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 +3 -1
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
|
@@ -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
|
-
|
|
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: "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
|
-
|
|
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
|
+
];
|