@fishawack/lab-env 5.7.0-beta.2 → 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 (36) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/_Test/provision.js +1 -8
  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 +29 -0
  8. package/commands/create/cmds/key.js +60 -41
  9. package/commands/create/cmds/provision.js +425 -55
  10. package/commands/create/cmds/rekey.js +218 -0
  11. package/commands/create/libs/output-credentials.js +59 -0
  12. package/commands/create/libs/parallel-runner.js +204 -0
  13. package/commands/create/libs/resolve-operator.js +59 -0
  14. package/commands/create/libs/utilities.js +71 -8
  15. package/commands/create/libs/vars.js +70 -86
  16. package/commands/create/services/aws/acm.js +4 -2
  17. package/commands/create/services/aws/cloudfront.js +51 -45
  18. package/commands/create/services/aws/ec2.js +132 -53
  19. package/commands/create/services/aws/elasticache.js +159 -0
  20. package/commands/create/services/aws/elasticbeanstalk.js +141 -60
  21. package/commands/create/services/aws/iam.js +159 -93
  22. package/commands/create/services/aws/index.js +1209 -466
  23. package/commands/create/services/aws/opensearch.js +25 -17
  24. package/commands/create/services/aws/rds.js +22 -34
  25. package/commands/create/services/aws/s3.js +14 -27
  26. package/commands/create/services/aws/secretsmanager.js +8 -15
  27. package/commands/create/services/aws/ses.js +195 -0
  28. package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/cron.config +45 -0
  29. package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/queue-worker.config +29 -0
  30. package/commands/create/templates/elasticbeanstalk/.ebextensions/laravel/software.config +12 -0
  31. package/commands/create/templates/elasticbeanstalk/.ebextensions/misc/setvars.config +2 -16
  32. package/commands/create/templates/elasticbeanstalk/.platform/confighooks/postdeploy/rewrite-flush.sh +1 -1
  33. package/commands/create/templates/elasticbeanstalk/.platform/confighooks/postdeploy/setvars.sh +19 -0
  34. package/commands/create/templates/elasticbeanstalk/.platform/hooks/postdeploy/restart_queue.sh +4 -0
  35. package/commands/create/templates/elasticbeanstalk/.platform/hooks/prebuild/setvars.sh +19 -0
  36. package/package.json +5 -2
@@ -0,0 +1,195 @@
1
+ const crypto = require("crypto");
2
+ const os = require("os");
3
+ const { readFileSync, writeFileSync } = require("fs");
4
+ const { SESClient, ListIdentitiesCommand } = require("@aws-sdk/client-ses");
5
+ const {
6
+ IAMClient,
7
+ CreateUserCommand,
8
+ GetUserCommand,
9
+ CreateAccessKeyCommand,
10
+ ListAccessKeysCommand,
11
+ DeleteAccessKeyCommand,
12
+ AddUserToGroupCommand,
13
+ } = require("@aws-sdk/client-iam");
14
+ const { fromIni } = require("@aws-sdk/credential-providers");
15
+ const { Spinner } = require("../../libs/utilities");
16
+
17
+ const SES_REGION = "us-east-1";
18
+ const SES_PROFILE = "fishawack";
19
+
20
+ const getClient = () =>
21
+ new SESClient({
22
+ region: SES_REGION,
23
+ credentials: fromIni({ profile: SES_PROFILE }),
24
+ });
25
+
26
+ module.exports.listVerifiedIdentities = async () => {
27
+ const client = getClient();
28
+
29
+ const res = await Spinner.prototype.simple(
30
+ "Retrieving verified SES identities",
31
+ () => {
32
+ return client.send(
33
+ new ListIdentitiesCommand({ IdentityType: "Domain" }),
34
+ );
35
+ },
36
+ );
37
+
38
+ return res.Identities || [];
39
+ };
40
+
41
+ /**
42
+ * Derives an SES SMTP password from an IAM secret access key.
43
+ * Uses the standard AWS SES SMTP credential derivation algorithm.
44
+ * https://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.html
45
+ */
46
+ module.exports.deriveSmtpPassword = (secretAccessKey, region = SES_REGION) => {
47
+ const VERSION = Buffer.from([0x04]);
48
+ const date = "11111111";
49
+ const service = "ses";
50
+ const terminal = "aws4_request";
51
+ const message = "SendRawEmail";
52
+
53
+ let signature = Buffer.from(`AWS4${secretAccessKey}`, "utf8");
54
+ for (const element of [date, region, service, terminal, message]) {
55
+ signature = crypto
56
+ .createHmac("sha256", signature)
57
+ .update(element)
58
+ .digest();
59
+ }
60
+
61
+ return Buffer.concat([VERSION, signature]).toString("base64");
62
+ };
63
+
64
+ const SES_SMTP_HOST = "email-smtp.us-east-1.amazonaws.com";
65
+ const SES_GROUP = "AWSSESSendingGroupDoNotRename";
66
+
67
+ const getIAMClient = () =>
68
+ new IAMClient({
69
+ region: SES_REGION,
70
+ credentials: fromIni({ profile: SES_PROFILE }),
71
+ });
72
+
73
+ module.exports.ensureOperatorSESCredentials = async (username) => {
74
+ const UserName = `fw-auto-${username}-operator`;
75
+ const client = getIAMClient();
76
+
77
+ // Create user (or confirm it already exists)
78
+ try {
79
+ await Spinner.prototype.simple(
80
+ `Creating SES IAM user ${UserName}`,
81
+ () => client.send(new CreateUserCommand({ UserName })),
82
+ );
83
+ } catch {
84
+ await Spinner.prototype.simple(
85
+ `Retrieving existing SES IAM user ${UserName}`,
86
+ () => client.send(new GetUserCommand({ UserName })),
87
+ );
88
+ }
89
+
90
+ // Add user to SES sending group
91
+ await Spinner.prototype.simple(`Adding ${UserName} to ${SES_GROUP}`, () =>
92
+ client.send(
93
+ new AddUserToGroupCommand({ UserName, GroupName: SES_GROUP }),
94
+ ),
95
+ );
96
+
97
+ // Check for existing keys
98
+ const existing = await client.send(new ListAccessKeysCommand({ UserName }));
99
+
100
+ if (existing.AccessKeyMetadata.length) {
101
+ return null;
102
+ }
103
+
104
+ // Create access key and derive SMTP password
105
+ const keyRes = await Spinner.prototype.simple(
106
+ "Creating access key for SES user",
107
+ () => client.send(new CreateAccessKeyCommand({ UserName })),
108
+ );
109
+
110
+ const smtpPassword = module.exports.deriveSmtpPassword(
111
+ keyRes.AccessKey.SecretAccessKey,
112
+ );
113
+
114
+ // Wait for IAM credentials to propagate across AWS services
115
+ await Spinner.prototype.simple(
116
+ "Waiting for IAM credentials to propagate",
117
+ () => new Promise((r) => setTimeout(r, 10000)),
118
+ );
119
+
120
+ return {
121
+ username: keyRes.AccessKey.AccessKeyId,
122
+ password: smtpPassword,
123
+ host: SES_SMTP_HOST,
124
+ };
125
+ };
126
+
127
+ module.exports.rotateOperatorSESCredentials = async (username) => {
128
+ const UserName = `fw-auto-${username}-operator`;
129
+ const client = getIAMClient();
130
+
131
+ // List and delete existing keys
132
+ const existing = await Spinner.prototype.simple(
133
+ `Listing access keys for ${UserName}`,
134
+ () => client.send(new ListAccessKeysCommand({ UserName })),
135
+ );
136
+
137
+ for (const key of existing.AccessKeyMetadata) {
138
+ await Spinner.prototype.simple(
139
+ `Removing old access key ${key.AccessKeyId}`,
140
+ () =>
141
+ client.send(
142
+ new DeleteAccessKeyCommand({
143
+ UserName,
144
+ AccessKeyId: key.AccessKeyId,
145
+ }),
146
+ ),
147
+ );
148
+ }
149
+
150
+ // Create new access key and derive SMTP password
151
+ const keyRes = await Spinner.prototype.simple(
152
+ "Creating new access key for SES user",
153
+ () => client.send(new CreateAccessKeyCommand({ UserName })),
154
+ );
155
+
156
+ const smtpPassword = module.exports.deriveSmtpPassword(
157
+ keyRes.AccessKey.SecretAccessKey,
158
+ );
159
+
160
+ // Wait for IAM credentials to propagate across AWS services
161
+ await Spinner.prototype.simple(
162
+ "Waiting for IAM credentials to propagate",
163
+ () => new Promise((r) => setTimeout(r, 10000)),
164
+ );
165
+
166
+ return {
167
+ username: keyRes.AccessKey.AccessKeyId,
168
+ password: smtpPassword,
169
+ host: SES_SMTP_HOST,
170
+ };
171
+ };
172
+
173
+ const SES_FROM = "digitalautomation@avalerehealth.com";
174
+ const MISC_PATH = `${os.homedir()}/targets/misc.json`;
175
+
176
+ module.exports.updateMiscNodemailer = (creds) => {
177
+ const { misc } = require("../../libs/vars");
178
+
179
+ const file = JSON.parse(readFileSync(MISC_PATH, { encoding: "utf8" }));
180
+
181
+ file.nodemailer = {
182
+ driver: "AWSSES",
183
+ AWSSES: {
184
+ username: creds.username,
185
+ password: creds.password,
186
+ host: creds.host,
187
+ },
188
+ from: SES_FROM,
189
+ };
190
+
191
+ writeFileSync(MISC_PATH, JSON.stringify(file, null, 4));
192
+
193
+ // Mutate the in-memory misc object so email.js picks up fresh creds
194
+ misc.nodemailer = file.nodemailer;
195
+ };
@@ -0,0 +1,45 @@
1
+ files:
2
+ "/usr/local/bin/is_first_instance.sh":
3
+ mode: "000755"
4
+ owner: root
5
+ group: root
6
+ content: |
7
+ #!/bin/bash
8
+ # Get IMDSv2 token (required on Amazon Linux 2023)
9
+ TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" 2>/dev/null)
10
+
11
+ INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null)
12
+ REGION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/dynamic/instance-identity/document 2>/dev/null | jq -r .region)
13
+
14
+ # Find the Auto Scaling Group name from the Elastic Beanstalk environment
15
+ ASG=$(aws ec2 describe-tags --filters "Name=resource-id,Values=$INSTANCE_ID" \
16
+ --region "$REGION" --output json | jq -r '.[][] | select(.Key=="aws:autoscaling:groupName") | .Value')
17
+
18
+ # Find the first instance in the Auto Scaling Group
19
+ FIRST=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names "$ASG" \
20
+ --region "$REGION" --output json | \
21
+ jq -r '.AutoScalingGroups[].Instances[] | select(.LifecycleState=="InService") | .InstanceId' | sort | head -1)
22
+
23
+ # If the instance ids are the same exit 0
24
+ [ "$FIRST" = "$INSTANCE_ID" ]
25
+
26
+ "/usr/local/bin/laravel_cron.sh":
27
+ mode: "000755"
28
+ owner: root
29
+ group: root
30
+ content: |
31
+ #!/bin/bash
32
+ /usr/local/bin/is_first_instance.sh || exit
33
+ cd /var/app/current && echo $(date -u) $(php artisan schedule:run)
34
+
35
+ "/etc/cron.d/laravel_cron":
36
+ mode: "000644"
37
+ owner: root
38
+ group: root
39
+ content: |
40
+ * * * * * root bash -l /usr/local/bin/laravel_cron.sh >> /var/log/laravel_cron.log 2>&1
41
+
42
+ commands:
43
+ rm_old_cron:
44
+ command: "rm -f /etc/cron.d/laravel_cron.bak"
45
+ ignoreErrors: true
@@ -0,0 +1,29 @@
1
+ files:
2
+ "/etc/systemd/system/queue-worker.service":
3
+ mode: "000644"
4
+ owner: root
5
+ group: root
6
+ content: |
7
+ [Unit]
8
+ Description=Laravel Queue Worker
9
+ After=network.target php-fpm.service
10
+
11
+ [Service]
12
+ User=webapp
13
+ Group=webapp
14
+ WorkingDirectory=/var/app/current
15
+ EnvironmentFile=/etc/sysconfig/env
16
+ ExecStart=/usr/bin/php /var/app/current/artisan queue:work database --queue=streaming,default --timeout=620 --tries=1
17
+ Restart=always
18
+ RestartSec=5
19
+ StandardOutput=append:/var/log/queue-worker.log
20
+ StandardError=append:/var/log/queue-worker.log
21
+
22
+ [Install]
23
+ WantedBy=multi-user.target
24
+
25
+ commands:
26
+ 01_systemd_reload:
27
+ command: systemctl daemon-reload
28
+ 02_enable_queue:
29
+ command: systemctl enable queue-worker
@@ -1,4 +1,16 @@
1
+ packages:
2
+ yum:
3
+ jq: []
4
+ mariadb105: []
5
+
6
+ commands:
7
+ 01_install_phpredis:
8
+ command: "yum install -y php$(php -r 'echo PHP_MAJOR_VERSION.\".\".PHP_MINOR_VERSION;')-pecl-redis6"
9
+ test: "! php -m | grep -q redis"
10
+
1
11
  option_settings:
12
+ aws:elasticbeanstalk:application:environment:
13
+ BASH_ENV: /etc/profile.d/sh.local
2
14
  aws:elasticbeanstalk:container:php:phpini:
3
15
  document_root: /public
4
16
  aws:elasticbeanstalk:environment:proxy:
@@ -5,22 +5,8 @@ option_settings:
5
5
  commands:
6
6
  setvars:
7
7
  command: |
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
8
+ /opt/elasticbeanstalk/bin/get-config environment | jq -r "to_entries | .[] | \"export \(.key)='\(.value)'\"" > /etc/profile.d/sh.local
9
+ /opt/elasticbeanstalk/bin/get-config environment | jq -r 'to_entries | .[] | "\(.key)=\"\(.value)\""' > /etc/sysconfig/env
24
10
 
25
11
  packages:
26
12
  yum:
@@ -1,4 +1,4 @@
1
1
  #!/bin/bash
2
2
 
3
3
  # Flush rewrites to recreate .htaccess files for existing permalink structure
4
- wp rewrite flush --hard || true
4
+ wp rewrite flush --hard || true
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+
3
+ ENV_NAME=$(/opt/elasticbeanstalk/bin/get-config container -k environment_name)
4
+ SECRET_ENV=${SECRET_MIRROR_ENV:-$ENV_NAME}
5
+ 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)
6
+
7
+ > /etc/profile.d/sh.local
8
+ > /etc/sysconfig/env
9
+
10
+ for GROUP in <%= SECRET_GROUPS %>; do
11
+ SECRET_JSON=$(aws secretsmanager get-secret-value \
12
+ --secret-id "${SECRET_ENV}-${GROUP}" \
13
+ --region "$REGION" \
14
+ --query SecretString \
15
+ --output text 2>/dev/null) || continue
16
+
17
+ echo "$SECRET_JSON" | jq -r 'to_entries | .[] | "export \(.key)='"'"'\(.value)'"'"'"' >> /etc/profile.d/sh.local
18
+ echo "$SECRET_JSON" | jq -r 'to_entries | .[] | "\(.key)=\"\(.value)\""' >> /etc/sysconfig/env
19
+ done
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ systemctl restart queue-worker
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+
3
+ ENV_NAME=$(/opt/elasticbeanstalk/bin/get-config container -k environment_name)
4
+ SECRET_ENV=${SECRET_MIRROR_ENV:-$ENV_NAME}
5
+ 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)
6
+
7
+ > /etc/profile.d/sh.local
8
+ > /etc/sysconfig/env
9
+
10
+ for GROUP in <%= SECRET_GROUPS %>; do
11
+ SECRET_JSON=$(aws secretsmanager get-secret-value \
12
+ --secret-id "${SECRET_ENV}-${GROUP}" \
13
+ --region "$REGION" \
14
+ --query SecretString \
15
+ --output text 2>/dev/null) || continue
16
+
17
+ echo "$SECRET_JSON" | jq -r 'to_entries | .[] | "export \(.key)='"'"'\(.value)'"'"'"' >> /etc/profile.d/sh.local
18
+ echo "$SECRET_JSON" | jq -r 'to_entries | .[] | "\(.key)=\"\(.value)\""' >> /etc/sysconfig/env
19
+ done
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishawack/lab-env",
3
- "version": "5.7.0-beta.2",
3
+ "version": "5.7.0-beta.3",
4
4
  "description": "Docker manager for FW",
5
5
  "main": "cli.js",
6
6
  "scripts": {
@@ -25,12 +25,14 @@
25
25
  "@aws-sdk/client-cloudfront": "^3.141.0",
26
26
  "@aws-sdk/client-ec2": "^3.399.0",
27
27
  "@aws-sdk/client-elastic-beanstalk": "^3.395.0",
28
+ "@aws-sdk/client-elasticache": "^3.1075.0",
28
29
  "@aws-sdk/client-iam": "^3.150.0",
29
30
  "@aws-sdk/client-opensearch": "^3.1065.0",
30
31
  "@aws-sdk/client-rds": "^3.395.0",
31
32
  "@aws-sdk/client-s3": "^3.141.0",
32
33
  "@aws-sdk/client-secrets-manager": "^3.395.0",
33
- "@aws-sdk/credential-providers": "^3.1011.0",
34
+ "@aws-sdk/client-ses": "^3.1075.0",
35
+ "@aws-sdk/credential-providers": "^3.1075.0",
34
36
  "apache-md5": "^1.1.8",
35
37
  "axios": "^0.21.4",
36
38
  "chalk": "4.1.0",
@@ -44,6 +46,7 @@
44
46
  "glob": "^13.0.6",
45
47
  "inquirer": "8.1.2",
46
48
  "lodash": "^4.17.21",
49
+ "log-update": "^4.0.0",
47
50
  "nodemailer": "^6.9.15",
48
51
  "ora": "5.4.1",
49
52
  "p-limit": "^7.3.0",