@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/_Test/provision.js +4 -9
  3. package/_Test/prune.js +8 -5
  4. package/bitbucket-pipelines.yml +2 -2
  5. package/cli.js +1 -0
  6. package/commands/create/cmds/dekey.js +13 -9
  7. package/commands/create/cmds/deprovision.js +36 -0
  8. package/commands/create/cmds/key.js +60 -41
  9. package/commands/create/cmds/provision.js +573 -69
  10. package/commands/create/cmds/rekey.js +218 -0
  11. package/commands/create/cmds/sync-secrets.js +2 -1
  12. package/commands/create/libs/output-credentials.js +59 -0
  13. package/commands/create/libs/parallel-runner.js +204 -0
  14. package/commands/create/libs/resolve-operator.js +59 -0
  15. package/commands/create/libs/utilities.js +73 -8
  16. package/commands/create/libs/vars.js +120 -136
  17. package/commands/create/services/aws/acm.js +61 -0
  18. package/commands/create/services/aws/cloudfront.js +51 -45
  19. package/commands/create/services/aws/ec2.js +675 -48
  20. package/commands/create/services/aws/elasticache.js +159 -0
  21. package/commands/create/services/aws/elasticbeanstalk.js +154 -57
  22. package/commands/create/services/aws/iam.js +402 -82
  23. package/commands/create/services/aws/index.js +1398 -260
  24. package/commands/create/services/aws/opensearch.js +155 -0
  25. package/commands/create/services/aws/rds.js +57 -31
  26. package/commands/create/services/aws/s3.js +52 -31
  27. package/commands/create/services/aws/secretsmanager.js +21 -12
  28. package/commands/create/services/aws/ses.js +195 -0
  29. package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/cron.config +45 -0
  30. package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/queue-worker.config +29 -0
  31. package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/software.config +12 -0
  32. package/commands/create/templates/elasticbeanstalk/.ebextensions/misc/setvars.config +2 -16
  33. package/commands/create/templates/elasticbeanstalk/.platform/confighooks/postdeploy/rewrite-flush.sh +1 -1
  34. package/commands/create/templates/elasticbeanstalk/.platform/confighooks/postdeploy/setvars.sh +19 -0
  35. package/commands/create/templates/elasticbeanstalk/.platform/hooks/postdeploy/restart_queue.sh +4 -0
  36. package/commands/create/templates/elasticbeanstalk/.platform/hooks/prebuild/setvars.sh +19 -0
  37. package/package.json +7 -2
@@ -0,0 +1,159 @@
1
+ const {
2
+ ElastiCacheClient,
3
+ CreateCacheSubnetGroupCommand,
4
+ CreateReplicationGroupCommand,
5
+ DeleteReplicationGroupCommand,
6
+ DescribeCacheSubnetGroupsCommand,
7
+ DescribeReplicationGroupsCommand,
8
+ } = require("@aws-sdk/client-elasticache");
9
+ const { Spinner, poll } = require("../../libs/utilities");
10
+ const { awsClientConfig } = require("../../libs/vars");
11
+
12
+ const getClient = () => new ElastiCacheClient(awsClientConfig);
13
+
14
+ module.exports.ensureCacheSubnetGroup = async (privateSubnetIds) => {
15
+ const name = "fw-auto-private-cache-subnet-group";
16
+
17
+ try {
18
+ await getClient().send(
19
+ new DescribeCacheSubnetGroupsCommand({
20
+ CacheSubnetGroupName: name,
21
+ }),
22
+ );
23
+
24
+ return name;
25
+ } catch (e) {
26
+ if (e.name !== "CacheSubnetGroupNotFoundFault") {
27
+ throw e;
28
+ }
29
+ }
30
+
31
+ await Spinner.prototype.simple(
32
+ `Creating cache subnet group ${name}`,
33
+ () => {
34
+ return getClient().send(
35
+ new CreateCacheSubnetGroupCommand({
36
+ CacheSubnetGroupName: name,
37
+ CacheSubnetGroupDescription:
38
+ "Private subnets for lab-env ElastiCache clusters",
39
+ SubnetIds: privateSubnetIds,
40
+ }),
41
+ );
42
+ },
43
+ );
44
+
45
+ return name;
46
+ };
47
+
48
+ module.exports.createCacheCluster = async (
49
+ name,
50
+ securityGroupId,
51
+ subnetGroupName,
52
+ authToken,
53
+ tags = [],
54
+ ) => {
55
+ const res = await Spinner.prototype.simple(
56
+ `Creating ElastiCache cluster ${name}`,
57
+ () => {
58
+ return getClient().send(
59
+ new CreateReplicationGroupCommand({
60
+ ReplicationGroupId: name,
61
+ ReplicationGroupDescription: `lab-env Valkey cluster for ${name}`,
62
+ Engine: "valkey",
63
+ EngineVersion: "8.0",
64
+ CacheNodeType: "cache.t3.small",
65
+ NumCacheClusters: 1,
66
+ Port: 6380,
67
+ TransitEncryptionEnabled: true,
68
+ AuthToken: authToken,
69
+ CacheSubnetGroupName: subnetGroupName,
70
+ SecurityGroupIds: [securityGroupId],
71
+ Tags: [
72
+ { Key: "client", Value: process.env.AWS_PROFILE },
73
+ ].concat(tags),
74
+ }),
75
+ );
76
+ },
77
+ );
78
+
79
+ return res;
80
+ };
81
+
82
+ module.exports.waitForCacheCluster = async (name) => {
83
+ const res = await Spinner.prototype.simple(
84
+ `Waiting for ElastiCache cluster to become available`,
85
+ (spinner) => {
86
+ return poll(
87
+ async () => {
88
+ const { ReplicationGroups } = await getClient().send(
89
+ new DescribeReplicationGroupsCommand({
90
+ ReplicationGroupId: name,
91
+ }),
92
+ );
93
+ return ReplicationGroups[0];
94
+ },
95
+ (group) => group.Status !== "available",
96
+ undefined,
97
+ spinner,
98
+ );
99
+ },
100
+ );
101
+
102
+ return res;
103
+ };
104
+
105
+ module.exports.describeCacheCluster = async (name) => {
106
+ const res = await Spinner.prototype.simple(
107
+ `Describing ElastiCache cluster ${name}`,
108
+ () => {
109
+ return getClient().send(
110
+ new DescribeReplicationGroupsCommand({
111
+ ReplicationGroupId: name,
112
+ }),
113
+ );
114
+ },
115
+ );
116
+
117
+ return res.ReplicationGroups[0];
118
+ };
119
+
120
+ module.exports.deleteCacheCluster = async (name) => {
121
+ await Spinner.prototype.simple(
122
+ `Deleting ElastiCache cluster ${name}`,
123
+ () => {
124
+ return getClient().send(
125
+ new DeleteReplicationGroupCommand({
126
+ ReplicationGroupId: name,
127
+ }),
128
+ );
129
+ },
130
+ );
131
+ };
132
+
133
+ module.exports.waitForCacheDeletion = async (name) => {
134
+ await Spinner.prototype.simple(
135
+ `Waiting for ElastiCache cluster to be deleted`,
136
+ (spinner) => {
137
+ return poll(
138
+ async () => {
139
+ try {
140
+ const { ReplicationGroups } = await getClient().send(
141
+ new DescribeReplicationGroupsCommand({
142
+ ReplicationGroupId: name,
143
+ }),
144
+ );
145
+ return ReplicationGroups[0];
146
+ } catch (e) {
147
+ if (e.name === "ReplicationGroupNotFoundFault") {
148
+ return { deleted: true };
149
+ }
150
+ throw e;
151
+ }
152
+ },
153
+ (group) => !group.deleted,
154
+ undefined,
155
+ spinner,
156
+ );
157
+ },
158
+ );
159
+ };
@@ -1,5 +1,7 @@
1
1
  const fs = require("fs-extra");
2
+ const crypto = require("crypto");
2
3
  const glob = require("glob");
4
+ const inquirer = require("inquirer");
3
5
  const {
4
6
  ElasticBeanstalkClient,
5
7
  CreateApplicationCommand,
@@ -14,18 +16,26 @@ const {
14
16
  UpdateEnvironmentCommand,
15
17
  CreateApplicationVersionCommand,
16
18
  ListAvailableSolutionStacksCommand,
19
+ ListTagsForResourceCommand,
20
+ RestartAppServerCommand,
17
21
  } = require("@aws-sdk/client-elastic-beanstalk");
18
22
  const { Spinner, poll } = require("../../libs/utilities");
19
- const { eb } = require("../../libs/vars");
23
+ const { eb, secretGroups, awsClientConfig } = require("../../libs/vars");
24
+ const { template } = require("lodash");
20
25
 
21
- module.exports.createElasticBeanstalkApplication = async (name) => {
22
- const client = new ElasticBeanstalkClient({});
26
+ const getClient = () => new ElasticBeanstalkClient(awsClientConfig);
23
27
 
28
+ module.exports.createElasticBeanstalkApplication = async (name, tags = []) => {
24
29
  let res = await Spinner.prototype.simple(
25
30
  `Creating elasticbeanstalk application ${name}`,
26
31
  () => {
27
- return client.send(
28
- new CreateApplicationCommand({ ApplicationName: name }),
32
+ return getClient().send(
33
+ new CreateApplicationCommand({
34
+ ApplicationName: name,
35
+ Tags: [
36
+ { Key: "client", Value: process.env.AWS_PROFILE },
37
+ ].concat(tags),
38
+ }),
29
39
  );
30
40
  },
31
41
  );
@@ -34,15 +44,36 @@ module.exports.createElasticBeanstalkApplication = async (name) => {
34
44
  };
35
45
 
36
46
  module.exports.describeEnvironment = async (name) => {
37
- const client = new ElasticBeanstalkClient({});
38
-
39
- return client.send(
47
+ return getClient().send(
40
48
  new DescribeEnvironmentsCommand({
41
49
  EnvironmentNames: [name],
42
50
  }),
43
51
  );
44
52
  };
45
53
 
54
+ module.exports.listEnvironments = async (applicationName) => {
55
+ const res = await Spinner.prototype.simple(
56
+ `Retrieving environments for ${applicationName}`,
57
+ () => {
58
+ return getClient().send(
59
+ new DescribeEnvironmentsCommand({
60
+ ApplicationName: applicationName,
61
+ IncludeDeleted: false,
62
+ }),
63
+ );
64
+ },
65
+ );
66
+
67
+ return (res.Environments || []).filter((e) => e.Status !== "Terminated");
68
+ };
69
+
70
+ module.exports.getEnvironmentTags = async (environmentArn) => {
71
+ const res = await getClient().send(
72
+ new ListTagsForResourceCommand({ ResourceArn: environmentArn }),
73
+ );
74
+ return res.ResourceTags || [];
75
+ };
76
+
46
77
  module.exports.createElasticBeanstalkEnvironment = async (
47
78
  name,
48
79
  { language },
@@ -50,13 +81,12 @@ module.exports.createElasticBeanstalkEnvironment = async (
50
81
  OptionSettings,
51
82
  CNAMEPrefix,
52
83
  tags = [],
84
+ versionLabel,
53
85
  ) => {
54
- const client = new ElasticBeanstalkClient({});
55
-
56
86
  const solutions = await Spinner.prototype.simple(
57
87
  `Retrieving available solution stacks`,
58
88
  () => {
59
- return client.send(new ListAvailableSolutionStacksCommand());
89
+ return getClient().send(new ListAvailableSolutionStacksCommand());
60
90
  },
61
91
  );
62
92
 
@@ -74,21 +104,23 @@ module.exports.createElasticBeanstalkEnvironment = async (
74
104
  d.includes(stackName),
75
105
  )[0];
76
106
 
107
+ const createParams = {
108
+ ApplicationName,
109
+ EnvironmentName: name,
110
+ SolutionStackName,
111
+ OptionSettings,
112
+ CNAMEPrefix,
113
+ Tags: [{ Key: "client", Value: process.env.AWS_PROFILE }].concat(tags),
114
+ };
115
+
116
+ if (versionLabel) {
117
+ createParams.VersionLabel = versionLabel;
118
+ }
119
+
77
120
  await Spinner.prototype.simple(
78
121
  `Creating elasticbeanstalk environment ${name}`,
79
122
  () => {
80
- return client.send(
81
- new CreateEnvironmentCommand({
82
- ApplicationName,
83
- EnvironmentName: name,
84
- SolutionStackName,
85
- OptionSettings,
86
- CNAMEPrefix,
87
- Tags: [
88
- { Key: "client", Value: process.env.AWS_PROFILE },
89
- ].concat(tags),
90
- }),
91
- );
123
+ return getClient().send(new CreateEnvironmentCommand(createParams));
92
124
  },
93
125
  );
94
126
 
@@ -111,12 +143,10 @@ module.exports.waitForElasticBeanstalkEnvironment = async (
111
143
  waitFor = "Ready",
112
144
  failIf,
113
145
  ) => {
114
- const client = new ElasticBeanstalkClient({});
115
-
116
146
  const res = await poll(
117
147
  async () =>
118
148
  (
119
- await client.send(
149
+ await getClient().send(
120
150
  new DescribeEnvironmentsCommand({
121
151
  EnvironmentNames: [name],
122
152
  }),
@@ -130,12 +160,10 @@ module.exports.waitForElasticBeanstalkEnvironment = async (
130
160
  };
131
161
 
132
162
  module.exports.removeElasticBeanstalkApplication = async (name) => {
133
- const client = new ElasticBeanstalkClient({});
134
-
135
163
  await Spinner.prototype.simple(
136
164
  `Removing elasticbeanstalk application ${name}`,
137
165
  () => {
138
- return client.send(
166
+ return getClient().send(
139
167
  new DeleteApplicationCommand({ ApplicationName: name }),
140
168
  );
141
169
  },
@@ -143,12 +171,10 @@ module.exports.removeElasticBeanstalkApplication = async (name) => {
143
171
  };
144
172
 
145
173
  module.exports.removeElasticBeanstalkEnvironment = async (name) => {
146
- const client = new ElasticBeanstalkClient({});
147
-
148
174
  await Spinner.prototype.simple(
149
175
  `Removing elasticbeanstalk environment ${name}`,
150
176
  () => {
151
- return client.send(
177
+ return getClient().send(
152
178
  new TerminateEnvironmentCommand({ EnvironmentName: name }),
153
179
  );
154
180
  },
@@ -165,25 +191,93 @@ module.exports.removeElasticBeanstalkEnvironment = async (name) => {
165
191
  );
166
192
  };
167
193
 
168
- module.exports.copyElasticBeanstalkConfigs = async (configurations) => {
194
+ module.exports.copyElasticBeanstalkConfigs = async (configurations, branch) => {
169
195
  const templates = `${__dirname}/../../templates/elasticbeanstalk`;
196
+ const templateData = {
197
+ ...process.env,
198
+ SECRET_GROUPS: secretGroups.join(" "),
199
+ };
200
+
201
+ const checksumPath = `_IaC/${branch}/.lab-env-checksums.json`;
202
+ const checksums = fs.existsSync(checksumPath)
203
+ ? JSON.parse(fs.readFileSync(checksumPath, "utf8"))
204
+ : {};
205
+
206
+ for (const globbed of configurations) {
207
+ const files = glob.sync(globbed, { cwd: `${templates}` });
208
+
209
+ for (const config of files) {
210
+ const content = fs.readFileSync(`${templates}/${config}`, "utf8");
211
+ const output = content.includes("<%=")
212
+ ? template(content, { interpolate: /<%=([\s\S]+?)%>/g })(
213
+ templateData,
214
+ )
215
+ : content;
216
+
217
+ const dest = `_IaC/${branch}/${config}`;
218
+ const newHash = crypto
219
+ .createHash("md5")
220
+ .update(output)
221
+ .digest("hex");
222
+
223
+ if (fs.existsSync(dest)) {
224
+ const existing = fs.readFileSync(dest, "utf8");
225
+
226
+ if (existing === output) {
227
+ checksums[config] = newHash;
228
+ continue;
229
+ }
230
+
231
+ const existingHash = crypto
232
+ .createHash("md5")
233
+ .update(existing)
234
+ .digest("hex");
235
+
236
+ let shouldOverwrite;
237
+
238
+ if (checksums[config] && existingHash === checksums[config]) {
239
+ const { overwrite } = await inquirer.prompt([
240
+ {
241
+ type: "confirm",
242
+ name: "overwrite",
243
+ message: `${dest} has an updated template, overwrite?`,
244
+ default: true,
245
+ },
246
+ ]);
247
+ shouldOverwrite = overwrite;
248
+ } else {
249
+ const { overwrite } = await inquirer.prompt([
250
+ {
251
+ type: "confirm",
252
+ name: "overwrite",
253
+ message: `${dest} has been modified, overwrite?`,
254
+ default: false,
255
+ },
256
+ ]);
257
+ shouldOverwrite = overwrite;
258
+ }
259
+
260
+ if (!shouldOverwrite) continue;
261
+ }
262
+
263
+ fs.outputFileSync(dest, output);
264
+ checksums[config] = newHash;
265
+ }
266
+ }
170
267
 
171
- configurations.forEach((globbed) => {
172
- glob.sync(globbed, { cwd: `${templates}` }).forEach((config) =>
173
- fs.copySync(`${templates}/${config}`, config),
174
- );
175
- });
268
+ fs.outputFileSync(checksumPath, JSON.stringify(checksums, null, 2));
176
269
 
177
- fs.outputFileSync(`.elasticbeanstalk/config.yml`, eb.config());
270
+ fs.outputFileSync(
271
+ `_IaC/${branch}/.elasticbeanstalk/config.yml`,
272
+ eb.config(),
273
+ );
178
274
  };
179
275
 
180
276
  module.exports.listApplications = async () => {
181
- const client = new ElasticBeanstalkClient({});
182
-
183
277
  const res = await Spinner.prototype.simple(
184
278
  `Retrieving elasticbeanstalk applications`,
185
279
  () => {
186
- return client.send(new DescribeApplicationsCommand({}));
280
+ return getClient().send(new DescribeApplicationsCommand({}));
187
281
  },
188
282
  );
189
283
 
@@ -191,13 +285,11 @@ module.exports.listApplications = async () => {
191
285
  };
192
286
 
193
287
  module.exports.listApplicationVersions = async (applicationName) => {
194
- const client = new ElasticBeanstalkClient({});
195
-
196
288
  let versions = [];
197
289
  let nextToken;
198
290
 
199
291
  do {
200
- const res = await client.send(
292
+ const res = await getClient().send(
201
293
  new DescribeApplicationVersionsCommand({
202
294
  ApplicationName: applicationName,
203
295
  ...(nextToken ? { NextToken: nextToken } : {}),
@@ -215,9 +307,7 @@ module.exports.createApplicationVersion = async (
215
307
  applicationName,
216
308
  versionLabel,
217
309
  ) => {
218
- const client = new ElasticBeanstalkClient({});
219
-
220
- const res = await client.send(
310
+ const res = await getClient().send(
221
311
  new CreateApplicationVersionCommand({
222
312
  ApplicationName: applicationName,
223
313
  VersionLabel: versionLabel,
@@ -232,9 +322,7 @@ module.exports.deleteApplicationVersion = async (
232
322
  applicationName,
233
323
  versionLabel,
234
324
  ) => {
235
- const client = new ElasticBeanstalkClient({});
236
-
237
- await client.send(
325
+ await getClient().send(
238
326
  new DeleteApplicationVersionCommand({
239
327
  ApplicationName: applicationName,
240
328
  VersionLabel: versionLabel,
@@ -247,12 +335,10 @@ module.exports.describeConfigurationSettings = async (
247
335
  applicationName,
248
336
  environmentName,
249
337
  ) => {
250
- const client = new ElasticBeanstalkClient({});
251
-
252
338
  const res = await Spinner.prototype.simple(
253
339
  `Retrieving configuration for ${environmentName}`,
254
340
  () => {
255
- return client.send(
341
+ return getClient().send(
256
342
  new DescribeConfigurationSettingsCommand({
257
343
  ApplicationName: applicationName,
258
344
  EnvironmentName: environmentName,
@@ -269,12 +355,10 @@ module.exports.updateEnvironmentSettings = async (
269
355
  optionSettings = [],
270
356
  optionsToRemove = [],
271
357
  ) => {
272
- const client = new ElasticBeanstalkClient({});
273
-
274
358
  await Spinner.prototype.simple(
275
359
  `Updating environment ${environmentName}`,
276
360
  () => {
277
- return client.send(
361
+ return getClient().send(
278
362
  new UpdateEnvironmentCommand({
279
363
  EnvironmentName: environmentName,
280
364
  OptionSettings: optionSettings,
@@ -284,3 +368,16 @@ module.exports.updateEnvironmentSettings = async (
284
368
  },
285
369
  );
286
370
  };
371
+
372
+ module.exports.restartAppServer = async (environmentName) => {
373
+ await Spinner.prototype.simple(
374
+ `Restarting app server for ${environmentName}`,
375
+ () => {
376
+ return getClient().send(
377
+ new RestartAppServerCommand({
378
+ EnvironmentName: environmentName,
379
+ }),
380
+ );
381
+ },
382
+ );
383
+ };