@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.
@@ -0,0 +1,147 @@
1
+ const {
2
+ OpenSearchClient,
3
+ CreateDomainCommand,
4
+ DeleteDomainCommand,
5
+ DescribeDomainCommand,
6
+ } = require("@aws-sdk/client-opensearch");
7
+ const { Spinner, poll } = require("../../libs/utilities");
8
+
9
+ module.exports.createOpenSearchDomain = async (
10
+ name,
11
+ securityGroupId,
12
+ privateSubnetIds,
13
+ instanceProfileRole,
14
+ ) => {
15
+ const client = new OpenSearchClient({});
16
+
17
+ const accountId = instanceProfileRole.split(":")[4] || "*";
18
+ const region = process.env.AWS_REGION;
19
+
20
+ const res = await Spinner.prototype.simple(
21
+ `Creating OpenSearch domain ${name}`,
22
+ () => {
23
+ return client.send(
24
+ new CreateDomainCommand({
25
+ DomainName: name,
26
+ EngineVersion: "OpenSearch_2.19",
27
+ ClusterConfig: {
28
+ InstanceType: "t3.small.search",
29
+ InstanceCount: 2,
30
+ ZoneAwarenessEnabled: true,
31
+ ZoneAwarenessConfig: {
32
+ AvailabilityZoneCount: 2,
33
+ },
34
+ },
35
+ EBSOptions: {
36
+ EBSEnabled: true,
37
+ VolumeType: "gp3",
38
+ VolumeSize: 20,
39
+ },
40
+ VPCOptions: {
41
+ SubnetIds: privateSubnetIds,
42
+ SecurityGroupIds: [securityGroupId],
43
+ },
44
+ EncryptionAtRestOptions: {
45
+ Enabled: true,
46
+ },
47
+ NodeToNodeEncryptionOptions: {
48
+ Enabled: true,
49
+ },
50
+ DomainEndpointOptions: {
51
+ EnforceHTTPS: true,
52
+ TLSSecurityPolicy: "Policy-Min-TLS-1-2-2019-07",
53
+ },
54
+ AdvancedSecurityOptions: {
55
+ Enabled: false,
56
+ InternalUserDatabaseEnabled: false,
57
+ },
58
+ AccessPolicies: JSON.stringify({
59
+ Version: "2012-10-17",
60
+ Statement: [
61
+ {
62
+ Effect: "Allow",
63
+ Principal: {
64
+ AWS: instanceProfileRole,
65
+ },
66
+ Action: "es:ESHttp*",
67
+ Resource: `arn:aws:es:${region}:${accountId}:domain/${name}/*`,
68
+ },
69
+ ],
70
+ }),
71
+ }),
72
+ );
73
+ },
74
+ );
75
+
76
+ return res;
77
+ };
78
+
79
+ module.exports.waitForOpenSearchDomain = async (name) => {
80
+ const client = new OpenSearchClient({});
81
+
82
+ const res = await Spinner.prototype.simple(
83
+ `Waiting for OpenSearch domain to become available (this may take 15-20 minutes)`,
84
+ () => {
85
+ return poll(
86
+ async () => {
87
+ const { DomainStatus } = await client.send(
88
+ new DescribeDomainCommand({ DomainName: name }),
89
+ );
90
+ return DomainStatus;
91
+ },
92
+ (status) =>
93
+ status.Processing ||
94
+ (!status.Endpoint && !status.Endpoints),
95
+ );
96
+ },
97
+ );
98
+
99
+ return res;
100
+ };
101
+
102
+ module.exports.describeOpenSearchDomain = async (name) => {
103
+ const client = new OpenSearchClient({});
104
+
105
+ const res = await Spinner.prototype.simple(
106
+ `Describing OpenSearch domain ${name}`,
107
+ () => {
108
+ return client.send(new DescribeDomainCommand({ DomainName: name }));
109
+ },
110
+ );
111
+
112
+ return res.DomainStatus;
113
+ };
114
+
115
+ module.exports.deleteOpenSearchDomain = async (name) => {
116
+ const client = new OpenSearchClient({});
117
+
118
+ await Spinner.prototype.simple(`Deleting OpenSearch domain ${name}`, () => {
119
+ return client.send(new DeleteDomainCommand({ DomainName: name }));
120
+ });
121
+ };
122
+
123
+ module.exports.waitForOpenSearchDeletion = async (name) => {
124
+ const client = new OpenSearchClient({});
125
+
126
+ await Spinner.prototype.simple(
127
+ `Waiting for OpenSearch domain to be deleted`,
128
+ () => {
129
+ return poll(
130
+ async () => {
131
+ try {
132
+ const { DomainStatus } = await client.send(
133
+ new DescribeDomainCommand({ DomainName: name }),
134
+ );
135
+ return DomainStatus;
136
+ } catch (e) {
137
+ if (e.name === "ResourceNotFoundException") {
138
+ return { deleted: true };
139
+ }
140
+ throw e;
141
+ }
142
+ },
143
+ (status) => !status.deleted,
144
+ );
145
+ },
146
+ );
147
+ };
@@ -0,0 +1,284 @@
1
+ const {
2
+ RDSClient,
3
+ CreateDBInstanceCommand,
4
+ CreateDBSubnetGroupCommand,
5
+ DeleteDBInstanceCommand,
6
+ DescribeDBInstancesCommand,
7
+ DescribeDBSnapshotsCommand,
8
+ DescribeDBSubnetGroupsCommand,
9
+ RestoreDBInstanceFromDBSnapshotCommand,
10
+ ModifyDBInstanceCommand,
11
+ DescribeDBParameterGroupsCommand,
12
+ CreateDBParameterGroupCommand,
13
+ ModifyDBParameterGroupCommand,
14
+ } = require("@aws-sdk/client-rds");
15
+ const { Spinner, poll } = require("../../libs/utilities");
16
+
17
+ module.exports.ensureDBSubnetGroup = async (privateSubnetIds) => {
18
+ const client = new RDSClient({});
19
+ const name = "fw-auto-private-db-subnet-group";
20
+
21
+ try {
22
+ await client.send(
23
+ new DescribeDBSubnetGroupsCommand({
24
+ DBSubnetGroupName: name,
25
+ }),
26
+ );
27
+
28
+ return name;
29
+ } catch (e) {
30
+ if (e.name !== "DBSubnetGroupNotFoundFault") {
31
+ throw e;
32
+ }
33
+ }
34
+
35
+ await Spinner.prototype.simple(`Creating DB subnet group ${name}`, () => {
36
+ return client.send(
37
+ new CreateDBSubnetGroupCommand({
38
+ DBSubnetGroupName: name,
39
+ DBSubnetGroupDescription:
40
+ "Private subnets for lab-env RDS instances",
41
+ SubnetIds: privateSubnetIds,
42
+ }),
43
+ );
44
+ });
45
+
46
+ return name;
47
+ };
48
+
49
+ module.exports.createRDSInstance = async (
50
+ name,
51
+ tags = [],
52
+ password,
53
+ securityGroupId,
54
+ subnetGroupName,
55
+ ) => {
56
+ const client = new RDSClient({});
57
+
58
+ let res = await Spinner.prototype.simple(
59
+ `Creating RDS instance ${name}`,
60
+ () => {
61
+ return client.send(
62
+ new CreateDBInstanceCommand({
63
+ DBInstanceIdentifier: name,
64
+ DBInstanceClass: "db.t3.small",
65
+ Engine: "mysql",
66
+ EngineVersion: "8.0.43",
67
+ MasterUsername: "ebroot",
68
+ MasterUserPassword: password,
69
+ AllocatedStorage: 5,
70
+ StorageEncrypted: true,
71
+ PubliclyAccessible: false,
72
+ BackupRetentionPeriod: 7,
73
+ DBName: "ebdb",
74
+ DBParameterGroupName: "lab-env-mysql80-ssl-required",
75
+ DBSubnetGroupName: subnetGroupName,
76
+ VpcSecurityGroupIds: [securityGroupId],
77
+ Tags: [
78
+ { Key: "client", Value: process.env.AWS_PROFILE },
79
+ ].concat(tags),
80
+ }),
81
+ );
82
+ },
83
+ );
84
+
85
+ return res;
86
+ };
87
+
88
+ module.exports.waitForRDSInstance = async (name) => {
89
+ const client = new RDSClient({});
90
+
91
+ const res = await Spinner.prototype.simple(
92
+ `Waiting for RDS instance to become available`,
93
+ () => {
94
+ return poll(
95
+ async () =>
96
+ (
97
+ await client.send(
98
+ new DescribeDBInstancesCommand({
99
+ DBInstanceIdentifier: name,
100
+ }),
101
+ )
102
+ ).DBInstances[0],
103
+ ({ DBInstanceStatus }) => DBInstanceStatus !== "available",
104
+ );
105
+ },
106
+ );
107
+
108
+ return res;
109
+ };
110
+
111
+ module.exports.describeRDSInstance = async (name) => {
112
+ const client = new RDSClient({});
113
+
114
+ let res = await Spinner.prototype.simple(
115
+ `Describing RDS instance ${name}`,
116
+ () => {
117
+ return client.send(
118
+ new DescribeDBInstancesCommand({
119
+ DBInstanceIdentifier: name,
120
+ }),
121
+ );
122
+ },
123
+ );
124
+
125
+ return res.DBInstances[0];
126
+ };
127
+
128
+ module.exports.deleteRDSInstance = async (name) => {
129
+ const client = new RDSClient({});
130
+
131
+ await Spinner.prototype.simple(`Deleting RDS instance ${name}`, () => {
132
+ return client.send(
133
+ new DeleteDBInstanceCommand({
134
+ DBInstanceIdentifier: name,
135
+ SkipFinalSnapshot: true,
136
+ }),
137
+ );
138
+ });
139
+ };
140
+
141
+ module.exports.waitForRDSDeletion = async (name) => {
142
+ const client = new RDSClient({});
143
+
144
+ await Spinner.prototype.simple(
145
+ `Waiting for RDS instance to be deleted`,
146
+ () => {
147
+ return poll(
148
+ async () => {
149
+ try {
150
+ const res = await client.send(
151
+ new DescribeDBInstancesCommand({
152
+ DBInstanceIdentifier: name,
153
+ }),
154
+ );
155
+ return res.DBInstances[0];
156
+ } catch (e) {
157
+ if (e.name === "DBInstanceNotFoundFault") {
158
+ return { DBInstanceStatus: "deleted" };
159
+ }
160
+ throw e;
161
+ }
162
+ },
163
+ ({ DBInstanceStatus }) => DBInstanceStatus !== "deleted",
164
+ );
165
+ },
166
+ );
167
+ };
168
+
169
+ module.exports.listManualSnapshots = async () => {
170
+ const client = new RDSClient({});
171
+
172
+ const res = await Spinner.prototype.simple(
173
+ `Fetching manual RDS snapshots`,
174
+ () => {
175
+ return client.send(
176
+ new DescribeDBSnapshotsCommand({
177
+ SnapshotType: "manual",
178
+ }),
179
+ );
180
+ },
181
+ );
182
+
183
+ return (res.DBSnapshots || [])
184
+ .filter((s) => s.Status === "available")
185
+ .sort(
186
+ (a, b) =>
187
+ new Date(b.SnapshotCreateTime) - new Date(a.SnapshotCreateTime),
188
+ );
189
+ };
190
+
191
+ module.exports.restoreRDSInstanceFromSnapshot = async (
192
+ name,
193
+ snapshotId,
194
+ securityGroupId,
195
+ tags = [],
196
+ subnetGroupName,
197
+ ) => {
198
+ const client = new RDSClient({});
199
+
200
+ let res = await Spinner.prototype.simple(
201
+ `Restoring RDS instance ${name} from snapshot ${snapshotId}`,
202
+ () => {
203
+ return client.send(
204
+ new RestoreDBInstanceFromDBSnapshotCommand({
205
+ DBInstanceIdentifier: name,
206
+ DBSnapshotIdentifier: snapshotId,
207
+ DBInstanceClass: "db.t3.small",
208
+ PubliclyAccessible: false,
209
+ DBParameterGroupName: "lab-env-mysql80-ssl-required",
210
+ DBSubnetGroupName: subnetGroupName,
211
+ VpcSecurityGroupIds: [securityGroupId],
212
+ Tags: [
213
+ { Key: "client", Value: process.env.AWS_PROFILE },
214
+ ].concat(tags),
215
+ }),
216
+ );
217
+ },
218
+ );
219
+
220
+ return res;
221
+ };
222
+
223
+ module.exports.modifyRDSPassword = async (name, password) => {
224
+ const client = new RDSClient({});
225
+
226
+ await Spinner.prototype.simple(
227
+ `Updating master password for RDS instance ${name}`,
228
+ () => {
229
+ return client.send(
230
+ new ModifyDBInstanceCommand({
231
+ DBInstanceIdentifier: name,
232
+ MasterUserPassword: password,
233
+ ApplyImmediately: true,
234
+ }),
235
+ );
236
+ },
237
+ );
238
+ };
239
+
240
+ const SSL_PARAMETER_GROUP = "lab-env-mysql80-ssl-required";
241
+
242
+ module.exports.ensureSSLParameterGroup = async () => {
243
+ const client = new RDSClient({});
244
+
245
+ try {
246
+ await client.send(
247
+ new DescribeDBParameterGroupsCommand({
248
+ DBParameterGroupName: SSL_PARAMETER_GROUP,
249
+ }),
250
+ );
251
+ } catch {
252
+ await Spinner.prototype.simple(
253
+ `Creating parameter group ${SSL_PARAMETER_GROUP}`,
254
+ () => {
255
+ return client.send(
256
+ new CreateDBParameterGroupCommand({
257
+ DBParameterGroupName: SSL_PARAMETER_GROUP,
258
+ DBParameterGroupFamily: "mysql8.0",
259
+ Description:
260
+ "lab-env managed parameter group requiring SSL connections",
261
+ }),
262
+ );
263
+ },
264
+ );
265
+
266
+ await Spinner.prototype.simple(
267
+ `Enabling require_secure_transport on ${SSL_PARAMETER_GROUP}`,
268
+ () => {
269
+ return client.send(
270
+ new ModifyDBParameterGroupCommand({
271
+ DBParameterGroupName: SSL_PARAMETER_GROUP,
272
+ Parameters: [
273
+ {
274
+ ParameterName: "require_secure_transport",
275
+ ParameterValue: "1",
276
+ ApplyMethod: "immediate",
277
+ },
278
+ ],
279
+ }),
280
+ );
281
+ },
282
+ );
283
+ }
284
+ };
@@ -30,18 +30,23 @@ module.exports.createS3Bucket = async (
30
30
  },
31
31
  );
32
32
 
33
+ const publicAccessConfig =
34
+ typeof blockAccess === "object"
35
+ ? blockAccess
36
+ : {
37
+ BlockPublicAcls: blockAccess,
38
+ BlockPublicPolicy: blockAccess,
39
+ IgnorePublicAcls: blockAccess,
40
+ RestrictPublicBuckets: blockAccess,
41
+ };
42
+
33
43
  await Spinner.prototype.simple(
34
- `${blockAccess ? "Blocking" : "Allowing"} all public access to s3 bucket`,
44
+ `Configuring public access settings for s3 bucket`,
35
45
  () => {
36
46
  return client.send(
37
47
  new PutPublicAccessBlockCommand({
38
48
  Bucket: bucket,
39
- PublicAccessBlockConfiguration: {
40
- BlockPublicAcls: blockAccess,
41
- BlockPublicPolicy: blockAccess,
42
- IgnorePublicAcls: blockAccess,
43
- RestrictPublicBuckets: blockAccess,
44
- },
49
+ PublicAccessBlockConfiguration: publicAccessConfig,
45
50
  }),
46
51
  );
47
52
  },
@@ -116,6 +121,35 @@ module.exports.setS3BucketPolicy = async (bucket, OAI) => {
116
121
  return res;
117
122
  };
118
123
 
124
+ module.exports.setPublicReadPolicy = async (bucket, prefix) => {
125
+ const client = new S3Client({});
126
+
127
+ let res = await Spinner.prototype.simple(
128
+ `Setting public read policy for ${prefix}`,
129
+ () => {
130
+ return client.send(
131
+ new PutBucketPolicyCommand({
132
+ Bucket: bucket,
133
+ Policy: JSON.stringify({
134
+ Version: "2012-10-17",
135
+ Statement: [
136
+ {
137
+ Sid: "PublicReadPrefix",
138
+ Effect: "Allow",
139
+ Principal: "*",
140
+ Action: "s3:GetObject",
141
+ Resource: `arn:aws:s3:::${bucket}/${prefix}`,
142
+ },
143
+ ],
144
+ }),
145
+ }),
146
+ );
147
+ },
148
+ );
149
+
150
+ return res;
151
+ };
152
+
119
153
  module.exports.addFileToS3Bucket = async (bucket, filepath, file) => {
120
154
  const client = new S3Client({});
121
155
 
@@ -0,0 +1,89 @@
1
+ const {
2
+ SecretsManagerClient,
3
+ CreateSecretCommand,
4
+ DescribeSecretCommand,
5
+ GetSecretValueCommand,
6
+ PutSecretValueCommand,
7
+ DeleteSecretCommand,
8
+ } = require("@aws-sdk/client-secrets-manager");
9
+ const { Spinner } = require("../../libs/utilities");
10
+
11
+ module.exports.createSecret = async (name, secretObject, tags = []) => {
12
+ const client = new SecretsManagerClient({});
13
+
14
+ let res = await Spinner.prototype.simple(`Creating secret ${name}`, () => {
15
+ return client.send(
16
+ new CreateSecretCommand({
17
+ Name: name,
18
+ SecretString: JSON.stringify(secretObject),
19
+ Tags: tags.map((t) => ({ Key: t.Key, Value: String(t.Value) })),
20
+ }),
21
+ );
22
+ });
23
+
24
+ return res;
25
+ };
26
+
27
+ module.exports.describeSecret = async (name) => {
28
+ const client = new SecretsManagerClient({});
29
+
30
+ let res = await Spinner.prototype.simple(
31
+ `Checking for existing secret ${name}`,
32
+ () => {
33
+ return client.send(
34
+ new DescribeSecretCommand({
35
+ SecretId: name,
36
+ }),
37
+ );
38
+ },
39
+ );
40
+
41
+ return res;
42
+ };
43
+
44
+ module.exports.getSecretValue = async (name) => {
45
+ const client = new SecretsManagerClient({});
46
+
47
+ let res = await Spinner.prototype.simple(
48
+ `Retrieving secret ${name}`,
49
+ () => {
50
+ return client.send(
51
+ new GetSecretValueCommand({
52
+ SecretId: name,
53
+ }),
54
+ );
55
+ },
56
+ );
57
+
58
+ return JSON.parse(res.SecretString);
59
+ };
60
+
61
+ module.exports.updateSecretValue = async (name, secretObject) => {
62
+ const client = new SecretsManagerClient({});
63
+
64
+ let res = await Spinner.prototype.simple(`Updating secret ${name}`, () => {
65
+ return client.send(
66
+ new PutSecretValueCommand({
67
+ SecretId: name,
68
+ SecretString: JSON.stringify(secretObject),
69
+ }),
70
+ );
71
+ });
72
+
73
+ return res;
74
+ };
75
+
76
+ module.exports.deleteSecret = async (name) => {
77
+ const client = new SecretsManagerClient({});
78
+
79
+ let res = await Spinner.prototype.simple(`Deleting secret ${name}`, () => {
80
+ return client.send(
81
+ new DeleteSecretCommand({
82
+ SecretId: name,
83
+ ForceDeleteWithoutRecovery: true,
84
+ }),
85
+ );
86
+ });
87
+
88
+ return res;
89
+ };
@@ -0,0 +1,5 @@
1
+ commands:
2
+ 01-download-rds-cert:
3
+ command: |
4
+ mkdir -p /opt/rds-certs
5
+ curl -so /opt/rds-certs/global-bundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
@@ -5,7 +5,22 @@ option_settings:
5
5
  commands:
6
6
  setvars:
7
7
  command: |
8
- /opt/elasticbeanstalk/bin/get-config environment | jq -r "to_entries | .[] | \"export \(.key)='\(.value)'\"" > /etc/profile.d/sh.local
8
+ ENV_NAME=$(/opt/elasticbeanstalk/bin/get-config container -k environment_name)
9
+ REGION=$(TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") && curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/placement/region)
10
+
11
+ > /etc/profile.d/sh.local
12
+ > /etc/sysconfig/env
13
+
14
+ for GROUP in app storage database; do
15
+ SECRET_JSON=$(aws secretsmanager get-secret-value \
16
+ --secret-id "${ENV_NAME}-${GROUP}" \
17
+ --region "$REGION" \
18
+ --query SecretString \
19
+ --output text 2>/dev/null) || continue
20
+
21
+ echo "$SECRET_JSON" | jq -r 'to_entries | .[] | "export \(.key)=\u0027\(.value)\u0027"' >> /etc/profile.d/sh.local
22
+ echo "$SECRET_JSON" | jq -r 'to_entries | .[] | "\(.key)=\"\(.value)\""' >> /etc/sysconfig/env
23
+ done
9
24
 
10
25
  packages:
11
26
  yum:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishawack/lab-env",
3
- "version": "5.6.1",
3
+ "version": "5.7.0-beta.2",
4
4
  "description": "Docker manager for FW",
5
5
  "main": "cli.js",
6
6
  "scripts": {
@@ -21,11 +21,15 @@
21
21
  "fw": "cli.js"
22
22
  },
23
23
  "dependencies": {
24
+ "@aws-sdk/client-acm": "^3.1067.0",
24
25
  "@aws-sdk/client-cloudfront": "^3.141.0",
25
26
  "@aws-sdk/client-ec2": "^3.399.0",
26
27
  "@aws-sdk/client-elastic-beanstalk": "^3.395.0",
27
28
  "@aws-sdk/client-iam": "^3.150.0",
29
+ "@aws-sdk/client-opensearch": "^3.1065.0",
30
+ "@aws-sdk/client-rds": "^3.395.0",
28
31
  "@aws-sdk/client-s3": "^3.141.0",
32
+ "@aws-sdk/client-secrets-manager": "^3.395.0",
29
33
  "@aws-sdk/credential-providers": "^3.1011.0",
30
34
  "apache-md5": "^1.1.8",
31
35
  "axios": "^0.21.4",