@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
|
@@ -18,6 +18,11 @@ module.exports = [
|
|
|
18
18
|
describe: "Branch to configure",
|
|
19
19
|
type: "string",
|
|
20
20
|
});
|
|
21
|
+
yargs.option("green-blue", {
|
|
22
|
+
alias: "g",
|
|
23
|
+
describe: "Clone an existing environment (pass source branch name)",
|
|
24
|
+
type: "string",
|
|
25
|
+
});
|
|
21
26
|
},
|
|
22
27
|
async (argv) => {
|
|
23
28
|
let branch = argv.branch || _.branch;
|
|
@@ -38,6 +43,190 @@ module.exports = [
|
|
|
38
43
|
process.exit(1);
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
// Green/Blue clone flow — entirely separate path
|
|
47
|
+
if (argv.greenBlue) {
|
|
48
|
+
let gbAnswers = await inquirer.prompt([client, region]);
|
|
49
|
+
|
|
50
|
+
setAWSClientDefaults(gbAnswers.client, gbAnswers.region);
|
|
51
|
+
|
|
52
|
+
// List existing EB applications
|
|
53
|
+
const applications = await aws.elasticbeanstalk.listApplications();
|
|
54
|
+
|
|
55
|
+
if (!applications.length) {
|
|
56
|
+
console.log(
|
|
57
|
+
utilities.colorize(
|
|
58
|
+
"\n ✗ No EB applications found in this account/region\n",
|
|
59
|
+
"error",
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// List all environments across applications
|
|
66
|
+
let allEnvironments = [];
|
|
67
|
+
for (const app of applications) {
|
|
68
|
+
const envs = await aws.elasticbeanstalk.listEnvironments(
|
|
69
|
+
app.ApplicationName,
|
|
70
|
+
);
|
|
71
|
+
allEnvironments = allEnvironments.concat(
|
|
72
|
+
envs.map((e) => ({
|
|
73
|
+
...e,
|
|
74
|
+
ApplicationName: app.ApplicationName,
|
|
75
|
+
})),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!allEnvironments.length) {
|
|
80
|
+
console.log(
|
|
81
|
+
utilities.colorize(
|
|
82
|
+
"\n ✗ No active EB environments found\n",
|
|
83
|
+
"error",
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { sourceEnv } = await inquirer.prompt([
|
|
90
|
+
{
|
|
91
|
+
type: "list",
|
|
92
|
+
name: "sourceEnv",
|
|
93
|
+
message: "Which environment should be cloned?",
|
|
94
|
+
choices: allEnvironments.map((e) => ({
|
|
95
|
+
name: `${e.EnvironmentName} (${e.Status})`,
|
|
96
|
+
value: e,
|
|
97
|
+
})),
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
// Validate branch doesn't conflict with source
|
|
102
|
+
const sourceSlug = sourceEnv.EnvironmentName;
|
|
103
|
+
const newSlug = aws.slug(
|
|
104
|
+
_.repoSafe,
|
|
105
|
+
gbAnswers.client,
|
|
106
|
+
branch,
|
|
107
|
+
"eb",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (newSlug === sourceSlug) {
|
|
111
|
+
const { newBranch } = await inquirer.prompt([
|
|
112
|
+
{
|
|
113
|
+
type: "input",
|
|
114
|
+
name: "newBranch",
|
|
115
|
+
message:
|
|
116
|
+
"Branch conflicts with source. Enter a different branch name:",
|
|
117
|
+
default: `${branch}-green`,
|
|
118
|
+
validate: (input) => {
|
|
119
|
+
const slug = aws.slug(
|
|
120
|
+
_.repoSafe,
|
|
121
|
+
gbAnswers.client,
|
|
122
|
+
input,
|
|
123
|
+
"eb",
|
|
124
|
+
);
|
|
125
|
+
if (slug === sourceSlug) {
|
|
126
|
+
return "Branch still conflicts with source environment";
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
branch = newBranch;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const slug = aws.slug(_.repoSafe, gbAnswers.client, branch, "eb");
|
|
136
|
+
|
|
137
|
+
// Check the clone doesn't already exist
|
|
138
|
+
const { Environments } =
|
|
139
|
+
await aws.elasticbeanstalk.describeEnvironment(slug);
|
|
140
|
+
const active = (Environments || []).filter(
|
|
141
|
+
(e) => e.Status !== "Terminated",
|
|
142
|
+
);
|
|
143
|
+
if (active.length) {
|
|
144
|
+
console.log(
|
|
145
|
+
utilities.colorize(
|
|
146
|
+
`\n ✗ Environment "${slug}" already exists (${active[0].Status})\n`,
|
|
147
|
+
"error",
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const environmentChoices = ["production", "staging", "test"];
|
|
154
|
+
const defaultEnvironment =
|
|
155
|
+
environmentChoices.find((e) =>
|
|
156
|
+
branch.toLowerCase().includes(e),
|
|
157
|
+
) || "test";
|
|
158
|
+
|
|
159
|
+
// Get environment tag from source
|
|
160
|
+
const sourceTags = await aws.elasticbeanstalk.getEnvironmentTags(
|
|
161
|
+
sourceEnv.EnvironmentArn,
|
|
162
|
+
);
|
|
163
|
+
const sourceEnvTag = sourceTags.find(
|
|
164
|
+
(t) => t.Key === "environment",
|
|
165
|
+
);
|
|
166
|
+
const envTagValue = sourceEnvTag
|
|
167
|
+
? sourceEnvTag.Value
|
|
168
|
+
: defaultEnvironment;
|
|
169
|
+
|
|
170
|
+
const tags = [
|
|
171
|
+
{ Key: "repository", Value: _.repo },
|
|
172
|
+
{ Key: "environment", Value: envTagValue },
|
|
173
|
+
{ Key: "branch", Value: branch },
|
|
174
|
+
{ Key: "automated", Value: "true" },
|
|
175
|
+
{ Key: "cloned-from", Value: sourceEnv.EnvironmentName },
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const infastructure = await aws.fullstackClone(
|
|
180
|
+
slug,
|
|
181
|
+
tags,
|
|
182
|
+
_.repoSafe,
|
|
183
|
+
branch,
|
|
184
|
+
sourceEnv.ApplicationName,
|
|
185
|
+
sourceEnv.EnvironmentName,
|
|
186
|
+
sourceEnv.VersionLabel,
|
|
187
|
+
argv.greenBlue,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const config = {};
|
|
191
|
+
config[branch] = {
|
|
192
|
+
deploy: {
|
|
193
|
+
url: infastructure.url,
|
|
194
|
+
location: "/var/www/html",
|
|
195
|
+
"aws-eb": infastructure.environment,
|
|
196
|
+
environment: envTagValue,
|
|
197
|
+
paths: infastructure.paths,
|
|
198
|
+
"cloned-from": sourceEnv.EnvironmentName,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (infastructure.copy) {
|
|
203
|
+
config[branch].copy = infastructure.copy;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let stringify = JSON.stringify(config, null, 4);
|
|
207
|
+
let output = stringify
|
|
208
|
+
.substring(1, stringify.length - 1)
|
|
209
|
+
.trim();
|
|
210
|
+
utilities.copyToClipboard(output);
|
|
211
|
+
console.log(
|
|
212
|
+
utilities.colorize(
|
|
213
|
+
`\n${output}\n\n(copied to clipboard)`,
|
|
214
|
+
"title",
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
console.log(e.message);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const environmentChoices = ["production", "staging", "test"];
|
|
226
|
+
const defaultEnvironment =
|
|
227
|
+
environmentChoices.find((e) => branch.toLowerCase().includes(e)) ||
|
|
228
|
+
"test";
|
|
229
|
+
|
|
41
230
|
let answers = await inquirer.prompt([stack]);
|
|
42
231
|
|
|
43
232
|
// Prompt for redirects if static stack and no existing redirects
|
|
@@ -207,12 +396,6 @@ module.exports = [
|
|
|
207
396
|
value: "default",
|
|
208
397
|
}),
|
|
209
398
|
},
|
|
210
|
-
{
|
|
211
|
-
type: "list",
|
|
212
|
-
name: "availability",
|
|
213
|
-
message: "What availability is required?",
|
|
214
|
-
choices: ["low", "high"],
|
|
215
|
-
},
|
|
216
399
|
region,
|
|
217
400
|
{
|
|
218
401
|
type: "confirm",
|
|
@@ -228,19 +411,49 @@ module.exports = [
|
|
|
228
411
|
},
|
|
229
412
|
])),
|
|
230
413
|
};
|
|
414
|
+
|
|
415
|
+
if (answers.framework === "laravel") {
|
|
416
|
+
answers = {
|
|
417
|
+
...answers,
|
|
418
|
+
...(await inquirer.prompt([
|
|
419
|
+
{
|
|
420
|
+
type: "confirm",
|
|
421
|
+
name: "opensearch",
|
|
422
|
+
message: "Does this site need OpenSearch?",
|
|
423
|
+
default: false,
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
type: "confirm",
|
|
427
|
+
name: "cache",
|
|
428
|
+
message: "Does this site need cache (Valkey)?",
|
|
429
|
+
default: false,
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
type: "confirm",
|
|
433
|
+
name: "mail",
|
|
434
|
+
message: "Does this site need email (SES)?",
|
|
435
|
+
default: false,
|
|
436
|
+
},
|
|
437
|
+
])),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
231
440
|
}
|
|
232
441
|
|
|
233
442
|
answers = {
|
|
234
443
|
...answers,
|
|
235
|
-
...(await inquirer.prompt(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
444
|
+
...(await inquirer.prompt(
|
|
445
|
+
answers.stack === "static"
|
|
446
|
+
? [
|
|
447
|
+
client,
|
|
448
|
+
{
|
|
449
|
+
type: "confirm",
|
|
450
|
+
name: "protected",
|
|
451
|
+
message: "Should the site be password protected?",
|
|
452
|
+
default: true,
|
|
453
|
+
},
|
|
454
|
+
]
|
|
455
|
+
: [client],
|
|
456
|
+
)),
|
|
244
457
|
};
|
|
245
458
|
|
|
246
459
|
let credentials = [];
|
|
@@ -296,78 +509,64 @@ module.exports = [
|
|
|
296
509
|
// Set AWS client defaults before any AWS operations
|
|
297
510
|
setAWSClientDefaults(answers.client, answers.region);
|
|
298
511
|
|
|
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
512
|
// Pre-flight check for existing resources
|
|
339
|
-
let existingSecrets = {
|
|
513
|
+
let existingSecrets = {
|
|
514
|
+
app: null,
|
|
515
|
+
storage: null,
|
|
516
|
+
database: null,
|
|
517
|
+
opensearch: null,
|
|
518
|
+
cache: null,
|
|
519
|
+
mail: null,
|
|
520
|
+
};
|
|
521
|
+
let existingEnvironment = false;
|
|
522
|
+
let existingResources = {
|
|
523
|
+
storage: false,
|
|
524
|
+
database: false,
|
|
525
|
+
opensearch: false,
|
|
526
|
+
cache: false,
|
|
527
|
+
};
|
|
528
|
+
let snapshot = null;
|
|
340
529
|
|
|
341
530
|
if (answers.stack === "fullstack") {
|
|
342
|
-
const {
|
|
531
|
+
const { existing } = await aws.fullstackPreflight(
|
|
343
532
|
slug,
|
|
344
533
|
_.repoSafe,
|
|
345
534
|
branch,
|
|
346
535
|
{
|
|
347
536
|
storage: answers.storage,
|
|
348
537
|
database: answers.database,
|
|
538
|
+
opensearch: answers.opensearch,
|
|
539
|
+
cache: answers.cache,
|
|
349
540
|
},
|
|
350
541
|
);
|
|
351
542
|
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
543
|
+
if (
|
|
544
|
+
existing.environment &&
|
|
545
|
+
existing.environment.Status !== "Ready"
|
|
546
|
+
) {
|
|
356
547
|
console.log(
|
|
357
548
|
utilities.colorize(
|
|
358
|
-
"
|
|
549
|
+
`\n ✗ EB environment "${slug}" is currently ${existing.environment.Status}, cannot re-provision\n`,
|
|
359
550
|
"error",
|
|
360
551
|
),
|
|
361
552
|
);
|
|
362
553
|
process.exit(1);
|
|
363
554
|
}
|
|
364
555
|
|
|
556
|
+
if (existing.environment) {
|
|
557
|
+
new utilities.Spinner(
|
|
558
|
+
"EB environment already exists, skipping creation",
|
|
559
|
+
true,
|
|
560
|
+
).ora.info();
|
|
561
|
+
existingEnvironment = existing.environment;
|
|
562
|
+
}
|
|
563
|
+
|
|
365
564
|
if (existing.storage) {
|
|
366
565
|
new utilities.Spinner(
|
|
367
566
|
"S3 bucket already exists, skipping storage creation",
|
|
368
567
|
true,
|
|
369
568
|
).ora.info();
|
|
370
|
-
|
|
569
|
+
existingResources.storage = true;
|
|
371
570
|
}
|
|
372
571
|
|
|
373
572
|
if (existing.database) {
|
|
@@ -375,7 +574,60 @@ module.exports = [
|
|
|
375
574
|
"RDS instance already exists, skipping database creation",
|
|
376
575
|
true,
|
|
377
576
|
).ora.info();
|
|
378
|
-
|
|
577
|
+
existingResources.database = true;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (existing.opensearch) {
|
|
581
|
+
new utilities.Spinner(
|
|
582
|
+
"OpenSearch domain already exists, skipping creation",
|
|
583
|
+
true,
|
|
584
|
+
).ora.info();
|
|
585
|
+
existingResources.opensearch = true;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (existing.cache) {
|
|
589
|
+
new utilities.Spinner(
|
|
590
|
+
"ElastiCache cluster already exists, skipping creation",
|
|
591
|
+
true,
|
|
592
|
+
).ora.info();
|
|
593
|
+
existingResources.cache = true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Snapshot restore prompt — only if database is needed and doesn't already exist
|
|
597
|
+
if (answers.database && !existing.database) {
|
|
598
|
+
const { restore } = await inquirer.prompt([
|
|
599
|
+
{
|
|
600
|
+
type: "confirm",
|
|
601
|
+
name: "restore",
|
|
602
|
+
message: "Restore database from an existing snapshot?",
|
|
603
|
+
default: false,
|
|
604
|
+
},
|
|
605
|
+
]);
|
|
606
|
+
|
|
607
|
+
if (restore) {
|
|
608
|
+
const snapshots = await aws.rds.listManualSnapshots();
|
|
609
|
+
|
|
610
|
+
if (snapshots.length) {
|
|
611
|
+
const { selected } = await inquirer.prompt([
|
|
612
|
+
{
|
|
613
|
+
type: "list",
|
|
614
|
+
name: "selected",
|
|
615
|
+
message: "Select a snapshot to restore from",
|
|
616
|
+
choices: snapshots.map((s) => ({
|
|
617
|
+
name: `${s.DBSnapshotIdentifier} (source: ${s.DBInstanceIdentifier}, created: ${new Date(s.SnapshotCreateTime).toLocaleDateString()})`,
|
|
618
|
+
value: s.DBSnapshotIdentifier,
|
|
619
|
+
})),
|
|
620
|
+
},
|
|
621
|
+
]);
|
|
622
|
+
|
|
623
|
+
snapshot = selected;
|
|
624
|
+
} else {
|
|
625
|
+
new utilities.Spinner(
|
|
626
|
+
"No manual snapshots found, creating a fresh database",
|
|
627
|
+
true,
|
|
628
|
+
).ora.info();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
379
631
|
}
|
|
380
632
|
|
|
381
633
|
existingSecrets = existing.secrets;
|
|
@@ -400,6 +652,241 @@ module.exports = [
|
|
|
400
652
|
true,
|
|
401
653
|
).ora.info();
|
|
402
654
|
}
|
|
655
|
+
|
|
656
|
+
if (existingSecrets.opensearch) {
|
|
657
|
+
new utilities.Spinner(
|
|
658
|
+
"OpenSearch secret already exists, reusing existing values",
|
|
659
|
+
true,
|
|
660
|
+
).ora.info();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (existingSecrets.cache) {
|
|
664
|
+
new utilities.Spinner(
|
|
665
|
+
"Cache secret already exists, reusing existing values",
|
|
666
|
+
true,
|
|
667
|
+
).ora.info();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (existingSecrets.mail) {
|
|
671
|
+
new utilities.Spinner(
|
|
672
|
+
"Mail secret already exists, reusing existing values",
|
|
673
|
+
true,
|
|
674
|
+
).ora.info();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// SES detail prompts — only if mail requested and not already configured
|
|
678
|
+
if (answers.mail && !existingSecrets.mail) {
|
|
679
|
+
const identities = await aws.ses.listVerifiedIdentities();
|
|
680
|
+
|
|
681
|
+
if (!identities.length) {
|
|
682
|
+
new utilities.Spinner(
|
|
683
|
+
"No verified SES identities found, skipping email setup",
|
|
684
|
+
true,
|
|
685
|
+
).ora.warn();
|
|
686
|
+
answers.mail = false;
|
|
687
|
+
} else {
|
|
688
|
+
const defaultIdentity = answers.appDomain
|
|
689
|
+
? identities.find(
|
|
690
|
+
(id) =>
|
|
691
|
+
answers.appDomain.endsWith(id) ||
|
|
692
|
+
id === answers.appDomain,
|
|
693
|
+
) || identities[0]
|
|
694
|
+
: identities[0];
|
|
695
|
+
|
|
696
|
+
const { sesIdentity } = await inquirer.prompt([
|
|
697
|
+
{
|
|
698
|
+
type: "list",
|
|
699
|
+
name: "sesIdentity",
|
|
700
|
+
message:
|
|
701
|
+
"Which verified SES domain should be used?",
|
|
702
|
+
choices: identities,
|
|
703
|
+
default: defaultIdentity,
|
|
704
|
+
},
|
|
705
|
+
]);
|
|
706
|
+
|
|
707
|
+
const { mailFromAddress } = await inquirer.prompt([
|
|
708
|
+
{
|
|
709
|
+
type: "input",
|
|
710
|
+
name: "mailFromAddress",
|
|
711
|
+
message: "Mail from address:",
|
|
712
|
+
default: `noreply@${sesIdentity}`,
|
|
713
|
+
validate: (input) => {
|
|
714
|
+
if (!input.includes("@")) {
|
|
715
|
+
return "Must be a valid email address";
|
|
716
|
+
}
|
|
717
|
+
const domain = input.split("@")[1];
|
|
718
|
+
if (
|
|
719
|
+
domain === sesIdentity ||
|
|
720
|
+
domain.endsWith(`.${sesIdentity}`)
|
|
721
|
+
) {
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
return `Address must be under the verified identity ${sesIdentity}`;
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
]);
|
|
728
|
+
|
|
729
|
+
const { mailFromName } = await inquirer.prompt([
|
|
730
|
+
{
|
|
731
|
+
type: "input",
|
|
732
|
+
name: "mailFromName",
|
|
733
|
+
message: "Mail from name:",
|
|
734
|
+
default:
|
|
735
|
+
answers.appName ||
|
|
736
|
+
_.repoSafe
|
|
737
|
+
.replace(/-/g, " ")
|
|
738
|
+
.replace(/^./, (c) => c.toUpperCase()),
|
|
739
|
+
validate: (input) =>
|
|
740
|
+
!!input.length || "From name is required",
|
|
741
|
+
},
|
|
742
|
+
]);
|
|
743
|
+
|
|
744
|
+
answers.mailData = {
|
|
745
|
+
fromAddress: mailFromAddress,
|
|
746
|
+
fromName: mailFromName,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Resolve environment tag
|
|
752
|
+
if (existingEnvironment) {
|
|
753
|
+
const envTags = await aws.elasticbeanstalk.getEnvironmentTags(
|
|
754
|
+
existingEnvironment.EnvironmentArn,
|
|
755
|
+
);
|
|
756
|
+
const envTag = envTags.find((t) => t.Key === "environment");
|
|
757
|
+
answers.environment = envTag
|
|
758
|
+
? envTag.Value
|
|
759
|
+
: defaultEnvironment;
|
|
760
|
+
new utilities.Spinner(
|
|
761
|
+
`Using existing environment tag: ${answers.environment}`,
|
|
762
|
+
true,
|
|
763
|
+
).ora.info();
|
|
764
|
+
} else {
|
|
765
|
+
answers = {
|
|
766
|
+
...answers,
|
|
767
|
+
...(await inquirer.prompt([
|
|
768
|
+
{
|
|
769
|
+
type: "list",
|
|
770
|
+
name: "environment",
|
|
771
|
+
message: "What environment is this?",
|
|
772
|
+
choices: environmentChoices,
|
|
773
|
+
default: defaultEnvironment,
|
|
774
|
+
},
|
|
775
|
+
])),
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// EB-specific prompts — only for new environments
|
|
780
|
+
if (!existingEnvironment) {
|
|
781
|
+
answers = {
|
|
782
|
+
...answers,
|
|
783
|
+
...(await inquirer.prompt([
|
|
784
|
+
{
|
|
785
|
+
type: "list",
|
|
786
|
+
name: "instanceType",
|
|
787
|
+
message: "What instance size is required?",
|
|
788
|
+
choices: [
|
|
789
|
+
{ name: "Small", value: "t3.small" },
|
|
790
|
+
{ name: "Medium", value: "t3.medium" },
|
|
791
|
+
{ name: "Large", value: "t3.large" },
|
|
792
|
+
{ name: "XLarge", value: "t3.xlarge" },
|
|
793
|
+
],
|
|
794
|
+
default: "t3.small",
|
|
795
|
+
},
|
|
796
|
+
])),
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (!existingEnvironment) {
|
|
801
|
+
answers = {
|
|
802
|
+
...answers,
|
|
803
|
+
...(await inquirer.prompt([
|
|
804
|
+
{
|
|
805
|
+
type: "list",
|
|
806
|
+
name: "elbScheme",
|
|
807
|
+
message: "Load balancer visibility?",
|
|
808
|
+
choices: ["public", "internal"],
|
|
809
|
+
},
|
|
810
|
+
])),
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// App name and domain prompts — only for new Laravel environments
|
|
815
|
+
if (!existingEnvironment && answers.framework === "laravel") {
|
|
816
|
+
answers = {
|
|
817
|
+
...answers,
|
|
818
|
+
...(await inquirer.prompt([
|
|
819
|
+
{
|
|
820
|
+
type: "input",
|
|
821
|
+
name: "appName",
|
|
822
|
+
message: "What is the app name?",
|
|
823
|
+
default: _.repoSafe
|
|
824
|
+
.replace(/-/g, " ")
|
|
825
|
+
.replace(/^./, (c) => c.toUpperCase()),
|
|
826
|
+
validate: (v) =>
|
|
827
|
+
/^[a-zA-Z0-9 ]+$/.test(v) ||
|
|
828
|
+
"Alphanumeric characters and spaces only",
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
type: "input",
|
|
832
|
+
name: "appDomain",
|
|
833
|
+
message:
|
|
834
|
+
"What domain will this site be served at? (DNS must be configured manually)",
|
|
835
|
+
filter: (v) =>
|
|
836
|
+
v
|
|
837
|
+
.replace(/^https?:\/\//, "")
|
|
838
|
+
.replace(/\/+$/, ""),
|
|
839
|
+
validate: (v) =>
|
|
840
|
+
v.length > 0 || "Domain is required",
|
|
841
|
+
},
|
|
842
|
+
])),
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
const matchingCerts = await aws.acm.findCertificatesForDomain(
|
|
846
|
+
answers.appDomain,
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
if (matchingCerts.length) {
|
|
850
|
+
answers = {
|
|
851
|
+
...answers,
|
|
852
|
+
...(await inquirer.prompt([
|
|
853
|
+
{
|
|
854
|
+
type: "list",
|
|
855
|
+
name: "certArn",
|
|
856
|
+
message:
|
|
857
|
+
"Which ACM certificate should be used for HTTPS?",
|
|
858
|
+
choices: [
|
|
859
|
+
...matchingCerts.map((c) => ({
|
|
860
|
+
name: c.DomainName,
|
|
861
|
+
value: c.CertificateArn,
|
|
862
|
+
})),
|
|
863
|
+
{ name: "None", value: null },
|
|
864
|
+
],
|
|
865
|
+
},
|
|
866
|
+
])),
|
|
867
|
+
};
|
|
868
|
+
} else {
|
|
869
|
+
new utilities.Spinner(
|
|
870
|
+
`No ACM certificates match ${answers.appDomain}, HTTPS listener must be configured manually`,
|
|
871
|
+
true,
|
|
872
|
+
).ora.info();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (answers.stack === "static") {
|
|
878
|
+
answers = {
|
|
879
|
+
...answers,
|
|
880
|
+
...(await inquirer.prompt([
|
|
881
|
+
{
|
|
882
|
+
type: "list",
|
|
883
|
+
name: "environment",
|
|
884
|
+
message: "What environment is this?",
|
|
885
|
+
choices: environmentChoices,
|
|
886
|
+
default: defaultEnvironment,
|
|
887
|
+
},
|
|
888
|
+
])),
|
|
889
|
+
};
|
|
403
890
|
}
|
|
404
891
|
|
|
405
892
|
try {
|
|
@@ -408,8 +895,9 @@ module.exports = [
|
|
|
408
895
|
slug,
|
|
409
896
|
[
|
|
410
897
|
{ Key: "repository", Value: _.repo },
|
|
411
|
-
{ Key: "environment", Value:
|
|
412
|
-
{ Key: "
|
|
898
|
+
{ Key: "environment", Value: answers.environment },
|
|
899
|
+
{ Key: "branch", Value: branch },
|
|
900
|
+
{ Key: "automated", Value: "true" },
|
|
413
901
|
],
|
|
414
902
|
credentials,
|
|
415
903
|
_.repoSafe,
|
|
@@ -422,18 +910,28 @@ module.exports = [
|
|
|
422
910
|
slug,
|
|
423
911
|
[
|
|
424
912
|
{ Key: "repository", Value: _.repo },
|
|
425
|
-
{ Key: "environment", Value:
|
|
426
|
-
{ Key: "
|
|
913
|
+
{ Key: "environment", Value: answers.environment },
|
|
914
|
+
{ Key: "branch", Value: branch },
|
|
915
|
+
{ Key: "automated", Value: "true" },
|
|
427
916
|
],
|
|
428
|
-
credentials,
|
|
429
917
|
_.repoSafe,
|
|
430
918
|
branch,
|
|
431
919
|
answers.framework,
|
|
432
|
-
answers.availability,
|
|
433
920
|
answers.database,
|
|
434
921
|
answers.storage,
|
|
922
|
+
answers.opensearch,
|
|
923
|
+
answers.cache,
|
|
924
|
+
answers.mail,
|
|
435
925
|
existingSecrets,
|
|
436
926
|
snapshot,
|
|
927
|
+
answers.elbScheme || "public",
|
|
928
|
+
existingEnvironment,
|
|
929
|
+
existingResources,
|
|
930
|
+
answers.appName,
|
|
931
|
+
answers.appDomain,
|
|
932
|
+
answers.certArn,
|
|
933
|
+
answers.instanceType,
|
|
934
|
+
answers.mailData,
|
|
437
935
|
);
|
|
438
936
|
}
|
|
439
937
|
} catch (e) {
|
|
@@ -448,16 +946,22 @@ module.exports = [
|
|
|
448
946
|
location: infastructure.bucket.slice(1), // Remove / from start
|
|
449
947
|
"aws-s3": answers.client,
|
|
450
948
|
"aws-cloudfront": infastructure.cloudfront,
|
|
949
|
+
environment: answers.environment,
|
|
451
950
|
}
|
|
452
951
|
: {
|
|
453
952
|
url: infastructure.url,
|
|
454
953
|
location: "/var/www/html",
|
|
455
954
|
"aws-eb": infastructure.environment,
|
|
955
|
+
environment: answers.environment,
|
|
456
956
|
paths: infastructure.paths,
|
|
457
957
|
};
|
|
458
958
|
|
|
459
959
|
config[branch] = { deploy };
|
|
460
960
|
|
|
961
|
+
if (infastructure.copy) {
|
|
962
|
+
config[branch].copy = infastructure.copy;
|
|
963
|
+
}
|
|
964
|
+
|
|
461
965
|
if (credentials.length) {
|
|
462
966
|
config[branch].deploy.users = credentials;
|
|
463
967
|
}
|