@medplum/cdk 2.0.7

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,232 @@
1
+ import { unlinkSync, writeFileSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { main } from './index';
4
+
5
+ describe('Infra', () => {
6
+ beforeEach(() => {
7
+ console.log = jest.fn();
8
+ });
9
+
10
+ test('Missing config', () => {
11
+ expect(() => main()).not.toThrow();
12
+ });
13
+
14
+ test('Synth stack', () => {
15
+ // Create a temp config file
16
+ const filename = resolve('./medplum.test.config.json');
17
+ writeFileSync(
18
+ filename,
19
+ JSON.stringify({
20
+ name: 'unittest',
21
+ stackName: 'MedplumUnitTestStack',
22
+ accountNumber: '647991932601',
23
+ region: 'us-east-1',
24
+ domainName: 'medplum.com',
25
+ apiPort: 8103,
26
+ apiDomainName: 'api.medplum.com',
27
+ apiSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/08bf1daf-3a2b-4cbe-91a0-739b4364a1ec',
28
+ appDomainName: 'app.medplum.com',
29
+ appSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/fd21b628-b2c0-4a5d-b4f5-b5c9a6d63b1a',
30
+ storageBucketName: 'medplum-storage',
31
+ storageDomainName: 'storage.medplum.com',
32
+ storageSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/19d85245-0a1d-4bf5-9789-23082b1a15fc',
33
+ storagePublicKey: '-----BEGIN PUBLIC KEY-----\n-----END PUBLIC KEY-----',
34
+ maxAzs: 2,
35
+ rdsInstances: 1,
36
+ desiredServerCount: 1,
37
+ serverImage: 'medplum/medplum-server:staging',
38
+ serverMemory: 512,
39
+ serverCpu: 256,
40
+ loadBalancerLoggingEnabled: true,
41
+ loadBalancerLoggingBucket: 'medplum-logs-us-east-1',
42
+ loadBalancerLoggingPrefix: 'elb',
43
+ clamscanEnabled: true,
44
+ clamscanLoggingBucket: 'medplum-logs-us-east-1',
45
+ clamscanLoggingPrefix: 'clamscan',
46
+ }),
47
+ { encoding: 'utf-8' }
48
+ );
49
+
50
+ expect(() => main({ config: filename })).not.toThrow();
51
+ unlinkSync(filename);
52
+ });
53
+
54
+ test('Multi region stack', () => {
55
+ const filename = resolve('./medplum.multiregion.config.json');
56
+ writeFileSync(
57
+ filename,
58
+ JSON.stringify({
59
+ name: 'multiregion',
60
+ stackName: 'MedplumMultiRegionStack',
61
+ accountNumber: '647991932601',
62
+ region: 'ap-southeast-1',
63
+ domainName: 'ap-southeast-1.medplum.com',
64
+ apiPort: 8103,
65
+ apiDomainName: 'api.medplum.com',
66
+ apiSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/08bf1daf-3a2b-4cbe-91a0-739b4364a1ec',
67
+ appDomainName: 'app.medplum.com',
68
+ appSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/fd21b628-b2c0-4a5d-b4f5-b5c9a6d63b1a',
69
+ storageBucketName: 'medplum-storage',
70
+ storageDomainName: 'storage.medplum.com',
71
+ storageSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/19d85245-0a1d-4bf5-9789-23082b1a15fc',
72
+ storagePublicKey: '-----BEGIN PUBLIC KEY-----\n-----END PUBLIC KEY-----',
73
+ maxAzs: 2,
74
+ rdsInstances: 1,
75
+ desiredServerCount: 1,
76
+ serverImage: 'medplum/medplum-server:staging',
77
+ serverMemory: 512,
78
+ serverCpu: 256,
79
+ loadBalancerLoggingEnabled: true,
80
+ loadBalancerLoggingBucket: 'medplum-logs-us-east-1',
81
+ loadBalancerLoggingPrefix: 'elb',
82
+ clamscanEnabled: true,
83
+ clamscanLoggingBucket: 'medplum-logs-us-east-1',
84
+ clamscanLoggingPrefix: 'clamscan',
85
+ }),
86
+ { encoding: 'utf-8' }
87
+ );
88
+
89
+ expect(() => main({ config: filename })).not.toThrow();
90
+ unlinkSync(filename);
91
+ });
92
+
93
+ test('ECR image', () => {
94
+ // Create a temp config file
95
+ const filename = resolve('./medplum.customvpc.config.json');
96
+ writeFileSync(
97
+ filename,
98
+ JSON.stringify({
99
+ name: 'customvpc',
100
+ stackName: 'MedplumCustomVpcStack',
101
+ accountNumber: '647991932601',
102
+ region: 'us-east-1',
103
+ domainName: 'medplum.com',
104
+ apiPort: 8103,
105
+ apiDomainName: 'api.medplum.com',
106
+ apiSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/08bf1daf-3a2b-4cbe-91a0-739b4364a1ec',
107
+ appDomainName: 'app.medplum.com',
108
+ appSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/fd21b628-b2c0-4a5d-b4f5-b5c9a6d63b1a',
109
+ storageBucketName: 'medplum-storage',
110
+ storageDomainName: 'storage.medplum.com',
111
+ storageSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/19d85245-0a1d-4bf5-9789-23082b1a15fc',
112
+ storagePublicKey: '-----BEGIN PUBLIC KEY-----\n-----END PUBLIC KEY-----',
113
+ maxAzs: 2,
114
+ rdsInstances: 1,
115
+ rdsInstanceType: 't3.micro',
116
+ desiredServerCount: 1,
117
+ serverImage: '647991932601.dkr.ecr.us-east-1.amazonaws.com/medplum-server:staging',
118
+ serverMemory: 512,
119
+ serverCpu: 256,
120
+ }),
121
+ { encoding: 'utf-8' }
122
+ );
123
+
124
+ expect(() => main({ config: filename })).not.toThrow();
125
+ unlinkSync(filename);
126
+ });
127
+
128
+ test('Custom VPC', () => {
129
+ // Create a temp config file
130
+ const filename = resolve('./medplum.customvpc.config.json');
131
+ writeFileSync(
132
+ filename,
133
+ JSON.stringify({
134
+ name: 'customvpc',
135
+ stackName: 'MedplumCustomVpcStack',
136
+ accountNumber: '647991932601',
137
+ region: 'us-east-1',
138
+ domainName: 'medplum.com',
139
+ apiPort: 8103,
140
+ vpcId: 'vpc-0fc3a4d0600000000',
141
+ apiDomainName: 'api.medplum.com',
142
+ apiSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/08bf1daf-3a2b-4cbe-91a0-739b4364a1ec',
143
+ appDomainName: 'app.medplum.com',
144
+ appSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/fd21b628-b2c0-4a5d-b4f5-b5c9a6d63b1a',
145
+ storageBucketName: 'medplum-storage',
146
+ storageDomainName: 'storage.medplum.com',
147
+ storageSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/19d85245-0a1d-4bf5-9789-23082b1a15fc',
148
+ storagePublicKey: '-----BEGIN PUBLIC KEY-----\n-----END PUBLIC KEY-----',
149
+ maxAzs: 2,
150
+ rdsInstances: 1,
151
+ desiredServerCount: 1,
152
+ serverImage: 'medplum/medplum-server:latest',
153
+ serverMemory: 512,
154
+ serverCpu: 256,
155
+ }),
156
+ { encoding: 'utf-8' }
157
+ );
158
+
159
+ expect(() => main({ config: filename })).not.toThrow();
160
+ unlinkSync(filename);
161
+ });
162
+
163
+ test('Custom RDS instance type', () => {
164
+ // Create a temp config file
165
+ const filename = resolve('./medplum.customRdsInstanceType.config.json');
166
+ writeFileSync(
167
+ filename,
168
+ JSON.stringify({
169
+ name: 'customRdsInstanceType',
170
+ stackName: 'MedplumCustomRdsInstanceTypeStack',
171
+ accountNumber: '647991932601',
172
+ region: 'us-east-1',
173
+ domainName: 'medplum.com',
174
+ apiPort: 8103,
175
+ apiDomainName: 'api.medplum.com',
176
+ apiSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/08bf1daf-3a2b-4cbe-91a0-739b4364a1ec',
177
+ appDomainName: 'app.medplum.com',
178
+ appSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/fd21b628-b2c0-4a5d-b4f5-b5c9a6d63b1a',
179
+ storageBucketName: 'medplum-storage',
180
+ storageDomainName: 'storage.medplum.com',
181
+ storageSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/19d85245-0a1d-4bf5-9789-23082b1a15fc',
182
+ storagePublicKey: '-----BEGIN PUBLIC KEY-----\n-----END PUBLIC KEY-----',
183
+ maxAzs: 2,
184
+ rdsInstances: 1,
185
+ rdsInstanceType: 't3.micro',
186
+ desiredServerCount: 1,
187
+ serverImage: 'medplum/medplum-server:latest',
188
+ serverMemory: 512,
189
+ serverCpu: 256,
190
+ }),
191
+ { encoding: 'utf-8' }
192
+ );
193
+
194
+ expect(() => main({ config: filename })).not.toThrow();
195
+ unlinkSync(filename);
196
+ });
197
+
198
+ test('Custom RDS secrets', () => {
199
+ // Create a temp config file
200
+ const filename = resolve('./medplum.customRdsSecrets.config.json');
201
+ const rdsSecretsArn = 'arn:aws:secretsmanager:s-east-1:647991932601:secret:SecretName-6RandomCharacters';
202
+ writeFileSync(
203
+ filename,
204
+ JSON.stringify({
205
+ name: 'customRdsSecrets',
206
+ stackName: 'MedplumCustomRdsSecretsStack',
207
+ accountNumber: '647991932601',
208
+ region: 'us-east-1',
209
+ domainName: 'medplum.com',
210
+ apiPort: 8103,
211
+ apiDomainName: 'api.medplum.com',
212
+ apiSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/08bf1daf-3a2b-4cbe-91a0-739b4364a1ec',
213
+ appDomainName: 'app.medplum.com',
214
+ appSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/fd21b628-b2c0-4a5d-b4f5-b5c9a6d63b1a',
215
+ storageBucketName: 'medplum-storage',
216
+ storageDomainName: 'storage.medplum.com',
217
+ storageSslCertArn: 'arn:aws:acm:us-east-1:647991932601:certificate/19d85245-0a1d-4bf5-9789-23082b1a15fc',
218
+ storagePublicKey: '-----BEGIN PUBLIC KEY-----\n-----END PUBLIC KEY-----',
219
+ maxAzs: 2,
220
+ rdsSecretsArn,
221
+ desiredServerCount: 1,
222
+ serverImage: 'medplum/medplum-server:latest',
223
+ serverMemory: 512,
224
+ serverCpu: 256,
225
+ }),
226
+ { encoding: 'utf-8' }
227
+ );
228
+
229
+ expect(() => main({ config: filename })).not.toThrow();
230
+ unlinkSync(filename);
231
+ });
232
+ });
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { App, Stack } from 'aws-cdk-lib';
2
+ import { readFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { BackEnd } from './backend';
5
+ import { MedplumInfraConfig } from './config';
6
+ import { FrontEnd } from './frontend';
7
+ import { Storage } from './storage';
8
+
9
+ class MedplumStack {
10
+ primaryStack: Stack;
11
+ backEnd: BackEnd;
12
+ frontEnd: FrontEnd;
13
+ storage: Storage;
14
+
15
+ constructor(scope: App, config: MedplumInfraConfig) {
16
+ this.primaryStack = new Stack(scope, config.stackName, {
17
+ env: {
18
+ region: config.region,
19
+ account: config.accountNumber,
20
+ },
21
+ });
22
+
23
+ this.backEnd = new BackEnd(this.primaryStack, config);
24
+ this.frontEnd = new FrontEnd(this.primaryStack, config, config.region);
25
+ this.storage = new Storage(this.primaryStack, config, config.region);
26
+
27
+ if (config.region !== 'us-east-1') {
28
+ // Some resources must be created in us-east-1
29
+ // For example, CloudFront distributions and ACM certificates
30
+ // If the primary region is not us-east-1, create these resources in us-east-1
31
+ const usEast1Stack = new Stack(scope, config.stackName + '-us-east-1', {
32
+ env: {
33
+ region: 'us-east-1',
34
+ account: config.accountNumber,
35
+ },
36
+ });
37
+
38
+ this.frontEnd = new FrontEnd(usEast1Stack, config, 'us-east-1');
39
+ this.storage = new Storage(usEast1Stack, config, 'us-east-1');
40
+ }
41
+ }
42
+ }
43
+
44
+ export function main(context?: Record<string, string>): void {
45
+ const app = new App({ context });
46
+
47
+ const configFileName = app.node.tryGetContext('config');
48
+ if (!configFileName) {
49
+ console.log('Missing "config" context variable');
50
+ console.log('Usage: cdk deploy -c config=my-config.json');
51
+ return;
52
+ }
53
+
54
+ const config = JSON.parse(readFileSync(resolve(configFileName), 'utf-8')) as MedplumInfraConfig;
55
+
56
+ const stack = new MedplumStack(app, config);
57
+
58
+ console.log('Stack', stack.primaryStack.stackId);
59
+ console.log('BackEnd', stack.backEnd.node.id);
60
+ console.log('FrontEnd', stack.frontEnd.node.id);
61
+ console.log('Storage', stack.storage.node.id);
62
+
63
+ app.synth();
64
+ }
65
+
66
+ if (require.main === module) {
67
+ main();
68
+ }
@@ -0,0 +1,378 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { readFileSync, unlinkSync, writeFileSync } from 'fs';
3
+ import readline from 'readline';
4
+ import { main } from './init';
5
+
6
+ jest.mock('@aws-sdk/client-acm');
7
+ jest.mock('@aws-sdk/client-ssm');
8
+ jest.mock('@aws-sdk/client-sts');
9
+
10
+ test('Init tool success', async () => {
11
+ const filename = `test-${randomUUID()}.json`;
12
+
13
+ await main(
14
+ mockReadline(
15
+ 'foo',
16
+ filename,
17
+ 'us-east-1',
18
+ 'account-123',
19
+ 'TestStack',
20
+ 'test.example.com',
21
+ 'support@example.com',
22
+ '', // default API domain
23
+ '', // default app domain
24
+ '', // default storage domain
25
+ '', // default storage bucket
26
+ '', // default availability zones
27
+ 'y', // Yes, create a database
28
+ '', // default database instances
29
+ '', // default server instances
30
+ '', // default server memory
31
+ '', // default server cpu
32
+ '', // default server image
33
+ 'y', // Yes, request api certificate
34
+ '', // default DNS validation
35
+ 'y', // Yes, request app certificate
36
+ '', // default DNS validation
37
+ 'y', // Yes, request storage certificate
38
+ '', // default DNS validation
39
+ 'y' // Yes, write to Parameter Store
40
+ )
41
+ );
42
+
43
+ const config = JSON.parse(readFileSync(filename, 'utf8'));
44
+ expect(config).toMatchObject({
45
+ apiPort: 8103,
46
+ name: 'foo',
47
+ region: 'us-east-1',
48
+ accountNumber: 'account-123',
49
+ stackName: 'TestStack',
50
+ domainName: 'test.example.com',
51
+ apiDomainName: 'api.test.example.com',
52
+ appDomainName: 'app.test.example.com',
53
+ storageDomainName: 'storage.test.example.com',
54
+ storageBucketName: 'medplum-foo-storage',
55
+ maxAzs: 2,
56
+ rdsInstances: 1,
57
+ desiredServerCount: 1,
58
+ serverMemory: 512,
59
+ serverCpu: 256,
60
+ serverImage: 'medplum/medplum-server:latest',
61
+ storagePublicKey: expect.stringContaining('-----BEGIN PUBLIC KEY-----'),
62
+ apiSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
63
+ appSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
64
+ storageSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
65
+ });
66
+ unlinkSync(filename);
67
+ });
68
+
69
+ test('Overwrite existing file', async () => {
70
+ const filename = `test-${randomUUID()}.json`;
71
+ writeFileSync(filename, '{}', 'utf8');
72
+
73
+ await main(
74
+ mockReadline(
75
+ 'foo',
76
+ filename,
77
+ 'y', // Yes, overwrite
78
+ 'us-east-1',
79
+ 'account-123',
80
+ 'TestStack',
81
+ 'test.example.com',
82
+ 'support@example.com',
83
+ '', // default API domain
84
+ '', // default app domain
85
+ '', // default storage domain
86
+ '', // default storage bucket
87
+ '', // default availability zones
88
+ 'y', // Yes, create a database
89
+ '', // default database instances
90
+ '', // default server instances
91
+ '', // default server memory
92
+ '', // default server cpu
93
+ '', // default server image
94
+ 'y', // Yes, request api certificate
95
+ '', // default DNS validation
96
+ 'y', // Yes, request app certificate
97
+ '', // default DNS validation
98
+ 'y', // Yes, request storage certificate
99
+ '', // default DNS validation
100
+ 'y' // Yes, write to Parameter Store
101
+ )
102
+ );
103
+
104
+ const config = JSON.parse(readFileSync(filename, 'utf8'));
105
+ expect(config).toMatchObject({
106
+ apiPort: 8103,
107
+ name: 'foo',
108
+ region: 'us-east-1',
109
+ accountNumber: 'account-123',
110
+ stackName: 'TestStack',
111
+ domainName: 'test.example.com',
112
+ apiDomainName: 'api.test.example.com',
113
+ appDomainName: 'app.test.example.com',
114
+ storageDomainName: 'storage.test.example.com',
115
+ storageBucketName: 'medplum-foo-storage',
116
+ maxAzs: 2,
117
+ rdsInstances: 1,
118
+ desiredServerCount: 1,
119
+ serverMemory: 512,
120
+ serverCpu: 256,
121
+ serverImage: 'medplum/medplum-server:latest',
122
+ storagePublicKey: expect.stringContaining('-----BEGIN PUBLIC KEY-----'),
123
+ apiSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
124
+ appSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
125
+ storageSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
126
+ });
127
+ unlinkSync(filename);
128
+ });
129
+
130
+ test('Invalid AWS credentials', async () => {
131
+ const filename = `test-${randomUUID()}.json`;
132
+
133
+ console.log = jest.fn();
134
+
135
+ await main(
136
+ mockReadline(
137
+ 'foo',
138
+ filename,
139
+ 'us-bad-1', // Special fake region for mock clients
140
+ 'account-123',
141
+ 'TestStack',
142
+ 'test.example.com',
143
+ 'support@example.com',
144
+ '', // default API domain
145
+ '', // default app domain
146
+ '', // default storage domain
147
+ '', // default storage bucket
148
+ '', // default availability zones
149
+ 'y', // Yes, create a database
150
+ '', // default database instances
151
+ '', // default server instances
152
+ '', // default server memory
153
+ '', // default server cpu
154
+ '', // default server image
155
+ 'y', // Yes, request api certificate
156
+ '', // default DNS validation
157
+ 'y', // Yes, request app certificate
158
+ '', // default DNS validation
159
+ 'y', // Yes, request storage certificate
160
+ '', // default DNS validation
161
+ 'y' // Yes, write to Parameter Store
162
+ )
163
+ );
164
+
165
+ expect(console.log).toHaveBeenCalledWith('Warning: Unable to get AWS account ID', 'Invalid region');
166
+
167
+ const config = JSON.parse(readFileSync(filename, 'utf8'));
168
+ expect(config).toMatchObject({
169
+ apiPort: 8103,
170
+ name: 'foo',
171
+ region: 'us-bad-1',
172
+ accountNumber: 'account-123',
173
+ stackName: 'TestStack',
174
+ domainName: 'test.example.com',
175
+ apiDomainName: 'api.test.example.com',
176
+ appDomainName: 'app.test.example.com',
177
+ storageDomainName: 'storage.test.example.com',
178
+ storageBucketName: 'medplum-foo-storage',
179
+ maxAzs: 2,
180
+ rdsInstances: 1,
181
+ desiredServerCount: 1,
182
+ serverMemory: 512,
183
+ serverCpu: 256,
184
+ serverImage: 'medplum/medplum-server:latest',
185
+ storagePublicKey: expect.stringContaining('-----BEGIN PUBLIC KEY-----'),
186
+ apiSslCertArn: 'TODO',
187
+ appSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
188
+ storageSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
189
+ });
190
+ unlinkSync(filename);
191
+ });
192
+
193
+ test('Bring your own database', async () => {
194
+ const filename = `test-${randomUUID()}.json`;
195
+
196
+ await main(
197
+ mockReadline(
198
+ 'foo',
199
+ filename,
200
+ 'us-east-1',
201
+ 'account-123',
202
+ 'TestStack',
203
+ 'test.example.com',
204
+ 'support@example.com',
205
+ '', // default API domain
206
+ '', // default app domain
207
+ '', // default storage domain
208
+ '', // default storage bucket
209
+ '', // default availability zones
210
+ 'n', // No, do not create a database
211
+ '', // default server instances
212
+ '', // default server memory
213
+ '', // default server cpu
214
+ '', // default server image
215
+ 'y', // Yes, request api certificate
216
+ '', // default DNS validation
217
+ 'y', // Yes, request app certificate
218
+ '', // default DNS validation
219
+ 'y', // Yes, request storage certificate
220
+ '', // default DNS validation
221
+ 'y' // Yes, write to Parameter Store
222
+ )
223
+ );
224
+
225
+ const config = JSON.parse(readFileSync(filename, 'utf8'));
226
+ expect(config).toMatchObject({
227
+ apiPort: 8103,
228
+ name: 'foo',
229
+ region: 'us-east-1',
230
+ accountNumber: 'account-123',
231
+ stackName: 'TestStack',
232
+ domainName: 'test.example.com',
233
+ apiDomainName: 'api.test.example.com',
234
+ appDomainName: 'app.test.example.com',
235
+ storageDomainName: 'storage.test.example.com',
236
+ storageBucketName: 'medplum-foo-storage',
237
+ maxAzs: 2,
238
+ rdsSecretsArn: 'TODO',
239
+ desiredServerCount: 1,
240
+ serverMemory: 512,
241
+ serverCpu: 256,
242
+ serverImage: 'medplum/medplum-server:latest',
243
+ storagePublicKey: expect.stringContaining('-----BEGIN PUBLIC KEY-----'),
244
+ apiSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
245
+ appSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
246
+ storageSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
247
+ });
248
+ unlinkSync(filename);
249
+ });
250
+
251
+ test('Do not request SSL certs', async () => {
252
+ const filename = `test-${randomUUID()}.json`;
253
+
254
+ await main(
255
+ mockReadline(
256
+ 'foo',
257
+ filename,
258
+ 'us-east-1',
259
+ 'account-123',
260
+ 'TestStack',
261
+ 'test.example.com',
262
+ 'support@example.com',
263
+ '', // default API domain
264
+ '', // default app domain
265
+ '', // default storage domain
266
+ '', // default storage bucket
267
+ '', // default availability zones
268
+ 'y', // Yes, create a database
269
+ '', // default database instances
270
+ '', // default server instances
271
+ '', // default server memory
272
+ '', // default server cpu
273
+ '', // default server image
274
+ 'n', // No api certificate
275
+ 'n', // No app certificate
276
+ 'n', // No storage certificate
277
+ 'y' // Yes, write to Parameter Store
278
+ )
279
+ );
280
+
281
+ const config = JSON.parse(readFileSync(filename, 'utf8'));
282
+ expect(config).toMatchObject({
283
+ apiPort: 8103,
284
+ name: 'foo',
285
+ region: 'us-east-1',
286
+ accountNumber: 'account-123',
287
+ stackName: 'TestStack',
288
+ domainName: 'test.example.com',
289
+ apiDomainName: 'api.test.example.com',
290
+ appDomainName: 'app.test.example.com',
291
+ storageDomainName: 'storage.test.example.com',
292
+ storageBucketName: 'medplum-foo-storage',
293
+ maxAzs: 2,
294
+ rdsInstances: 1,
295
+ desiredServerCount: 1,
296
+ serverMemory: 512,
297
+ serverCpu: 256,
298
+ serverImage: 'medplum/medplum-server:latest',
299
+ storagePublicKey: expect.stringContaining('-----BEGIN PUBLIC KEY-----'),
300
+ apiSslCertArn: 'TODO',
301
+ appSslCertArn: 'TODO',
302
+ storageSslCertArn: 'TODO',
303
+ });
304
+ unlinkSync(filename);
305
+ });
306
+
307
+ test('Existing SSL certificates', async () => {
308
+ const filename = `test-${randomUUID()}.json`;
309
+
310
+ await main(
311
+ mockReadline(
312
+ 'foo',
313
+ filename,
314
+ 'us-east-1',
315
+ 'account-123',
316
+ 'TestStack',
317
+ 'existing.example.com',
318
+ 'support@example.com',
319
+ '', // default API domain
320
+ '', // default app domain
321
+ '', // default storage domain
322
+ '', // default storage bucket
323
+ '', // default availability zones
324
+ 'y', // Yes, create a database
325
+ '', // default database instances
326
+ '', // default server instances
327
+ '', // default server memory
328
+ '', // default server cpu
329
+ '', // default server image
330
+ 'y', // Yes, request api certificate
331
+ '', // default DNS validation
332
+ 'y', // Yes, request app certificate
333
+ '', // default DNS validation
334
+ 'y', // Yes, request storage certificate
335
+ '', // default DNS validation
336
+ 'y' // Yes, write to Parameter Store
337
+ )
338
+ );
339
+
340
+ const config = JSON.parse(readFileSync(filename, 'utf8'));
341
+ expect(config).toMatchObject({
342
+ apiPort: 8103,
343
+ name: 'foo',
344
+ region: 'us-east-1',
345
+ accountNumber: 'account-123',
346
+ stackName: 'TestStack',
347
+ domainName: 'existing.example.com',
348
+ apiDomainName: 'api.existing.example.com',
349
+ appDomainName: 'app.existing.example.com',
350
+ storageDomainName: 'storage.existing.example.com',
351
+ storageBucketName: 'medplum-foo-storage',
352
+ maxAzs: 2,
353
+ rdsInstances: 1,
354
+ desiredServerCount: 1,
355
+ serverMemory: 512,
356
+ serverCpu: 256,
357
+ serverImage: 'medplum/medplum-server:latest',
358
+ storagePublicKey: expect.stringContaining('-----BEGIN PUBLIC KEY-----'),
359
+ apiSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789013',
360
+ appSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
361
+ storageSslCertArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012',
362
+ });
363
+ unlinkSync(filename);
364
+ });
365
+
366
+ function mockReadline(...answers: string[]): readline.Interface {
367
+ const result = { write: jest.fn(), question: jest.fn() };
368
+ const debug = false;
369
+ for (const answer of answers) {
370
+ result.question.mockImplementationOnce((q: string, cb: (answer: string) => void) => {
371
+ if (debug) {
372
+ console.log(q, answer);
373
+ }
374
+ cb(answer);
375
+ });
376
+ }
377
+ return result as unknown as readline.Interface;
378
+ }