@medplum/cdk 2.1.7 → 2.1.9
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/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/config.d.ts +17 -0
- package/package.json +1 -1
- package/src/backend.ts +1 -1
- package/src/config.test.ts +487 -0
- package/src/config.ts +202 -0
- package/src/index.ts +13 -5
package/src/config.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
|
|
2
|
+
import {
|
|
3
|
+
ExternalSecret,
|
|
4
|
+
ExternalSecretPrimitive,
|
|
5
|
+
ExternalSecretPrimitiveType,
|
|
6
|
+
MedplumInfraConfig,
|
|
7
|
+
MedplumSourceInfraConfig,
|
|
8
|
+
OperationOutcomeError,
|
|
9
|
+
badRequest,
|
|
10
|
+
validationError,
|
|
11
|
+
} from '@medplum/core';
|
|
12
|
+
|
|
13
|
+
const VALID_PRIMITIVE_TYPES = ['string', 'boolean', 'number'];
|
|
14
|
+
const ssmClients = {} as Record<string, SSMClient>;
|
|
15
|
+
|
|
16
|
+
export class InfraConfigNormalizer {
|
|
17
|
+
private config: MedplumSourceInfraConfig;
|
|
18
|
+
private clients: { ssm: SSMClient };
|
|
19
|
+
constructor(config: MedplumSourceInfraConfig) {
|
|
20
|
+
const { region } = config;
|
|
21
|
+
if (!region) {
|
|
22
|
+
throw new OperationOutcomeError(validationError("'region' must be defined as a string literal in config."));
|
|
23
|
+
}
|
|
24
|
+
if (!ssmClients[region]) {
|
|
25
|
+
ssmClients[region] = new SSMClient({ region });
|
|
26
|
+
}
|
|
27
|
+
this.config = config;
|
|
28
|
+
this.clients = { ssm: ssmClients[region] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async fetchParameterStoreSecret(key: string): Promise<string> {
|
|
32
|
+
const response = await this.clients.ssm.send(
|
|
33
|
+
new GetParameterCommand({
|
|
34
|
+
Name: key,
|
|
35
|
+
WithDecryption: true,
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
const param = response.Parameter;
|
|
39
|
+
if (!param) {
|
|
40
|
+
throw new OperationOutcomeError(
|
|
41
|
+
badRequest(
|
|
42
|
+
`Key '${key}' not found. Make sure your key is correct and that it is defined in your Parameter Store.`
|
|
43
|
+
)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const paramValue = param.Value;
|
|
47
|
+
if (!paramValue) {
|
|
48
|
+
throw new OperationOutcomeError(
|
|
49
|
+
badRequest(
|
|
50
|
+
`Key '${key}' found but has no value. Make sure your key is correct and that it is defined in your Parameter Store.`
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return paramValue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async fetchExternalSecret(externalSecret: ExternalSecret): Promise<ExternalSecretPrimitive> {
|
|
58
|
+
assertValidExternalSecret(externalSecret);
|
|
59
|
+
const { system, key, type } = externalSecret;
|
|
60
|
+
let rawValue: ExternalSecretPrimitive;
|
|
61
|
+
switch (system) {
|
|
62
|
+
case 'aws_ssm_parameter_store': {
|
|
63
|
+
rawValue = await this.fetchParameterStoreSecret(key);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
throw new OperationOutcomeError(
|
|
68
|
+
validationError(`Unknown system '${system}' for ExternalSecret. Unable to fetch the secret for key '${key}'.`)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return normalizeFetchedValue(key, rawValue, type);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async normalizeInfraConfigArray(currentVal: any[]): Promise<ExternalSecretPrimitive[] | Record<string, any>[]> {
|
|
75
|
+
// ------ case 3a: primitives or `ExternalSecret`
|
|
76
|
+
const firstEle = currentVal[0];
|
|
77
|
+
let newArray: ExternalSecretPrimitive[] | Record<string, any>[];
|
|
78
|
+
if ((typeof firstEle !== 'object' && firstEle !== null) || isExternalSecretLike(firstEle)) {
|
|
79
|
+
newArray = new Array(currentVal.length) as ExternalSecretPrimitive[];
|
|
80
|
+
for (let i = 0; i < currentVal.length; i++) {
|
|
81
|
+
const currIdxVal = currentVal[i] as unknown as ExternalSecretPrimitive | ExternalSecret;
|
|
82
|
+
if (typeof currIdxVal !== 'object') {
|
|
83
|
+
newArray[i] = currIdxVal;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const fetchedVal = await this.fetchExternalSecret(currIdxVal);
|
|
87
|
+
newArray[i] = fetchedVal;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ------ case 3b: other objects (recurse)
|
|
91
|
+
else {
|
|
92
|
+
newArray = new Array(currentVal.length) as Record<string, any>[];
|
|
93
|
+
for (let i = 0; i < currentVal.length; i++) {
|
|
94
|
+
newArray[i] = await this.normalizeObjectInInfraConfig(currentVal[i]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return newArray;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async normalizeValueForKey(obj: Record<string, any>, key: string): Promise<void> {
|
|
101
|
+
const currentVal = obj[key];
|
|
102
|
+
// cases:
|
|
103
|
+
// --- case 1: primitive
|
|
104
|
+
if (typeof currentVal !== 'object') {
|
|
105
|
+
obj[key] = currentVal;
|
|
106
|
+
}
|
|
107
|
+
// --- case 2: object conforming to `ExternalSecret` schema
|
|
108
|
+
else if (isExternalSecretLike(currentVal)) {
|
|
109
|
+
obj[key] = await this.fetchExternalSecret(currentVal);
|
|
110
|
+
}
|
|
111
|
+
// --- case 3: an array of:
|
|
112
|
+
else if (Array.isArray(currentVal) && currentVal.length) {
|
|
113
|
+
obj[key] = await this.normalizeInfraConfigArray(currentVal);
|
|
114
|
+
}
|
|
115
|
+
// --- case 4: other object (recurse)
|
|
116
|
+
else if (typeof currentVal === 'object') {
|
|
117
|
+
obj[key] = await this.normalizeObjectInInfraConfig(currentVal);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async normalizeObjectInInfraConfig(obj: Record<string, any>): Promise<Record<string, any>> {
|
|
122
|
+
const normalizedObj = { ...obj };
|
|
123
|
+
// walk config object
|
|
124
|
+
for (const key of Object.keys(normalizedObj)) {
|
|
125
|
+
await this.normalizeValueForKey(normalizedObj, key);
|
|
126
|
+
}
|
|
127
|
+
return normalizedObj;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async normalizeConfig(): Promise<MedplumInfraConfig> {
|
|
131
|
+
return this.normalizeObjectInInfraConfig(this.config) as Promise<MedplumInfraConfig>;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function normalizeFetchedValue(
|
|
136
|
+
key: string,
|
|
137
|
+
rawValue: ExternalSecretPrimitive,
|
|
138
|
+
expectedType: ExternalSecretPrimitiveType
|
|
139
|
+
): ExternalSecretPrimitive {
|
|
140
|
+
const typeOfVal = typeof rawValue;
|
|
141
|
+
// Return raw type if type is string and value is of type string, or if type isn't string and typeof val isn't string
|
|
142
|
+
if (!VALID_PRIMITIVE_TYPES.includes(typeOfVal)) {
|
|
143
|
+
throw new OperationOutcomeError(
|
|
144
|
+
validationError(
|
|
145
|
+
`Invalid value found for type; expected either ${VALID_PRIMITIVE_TYPES.join(', or')} but got ${typeOfVal}`
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (typeOfVal === expectedType) {
|
|
150
|
+
return rawValue;
|
|
151
|
+
} else if (typeOfVal === 'string' && expectedType === 'boolean') {
|
|
152
|
+
const normalized = (rawValue as string).toLowerCase() as 'true' | 'false';
|
|
153
|
+
if (normalized !== 'true' && normalized !== 'false') {
|
|
154
|
+
throw new OperationOutcomeError(
|
|
155
|
+
validationError(`Invalid value found for key '${key}'; expected boolean value but got '${rawValue}'`)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return normalized === 'true';
|
|
159
|
+
} else if (typeOfVal === 'string' && expectedType === 'number') {
|
|
160
|
+
const parsed = parseInt(rawValue as string, 10);
|
|
161
|
+
if (Number.isNaN(parsed)) {
|
|
162
|
+
throw new OperationOutcomeError(
|
|
163
|
+
validationError(`Invalid value found for key '${key}'; expected integer value but got '${rawValue}'`)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return parsed;
|
|
167
|
+
} else {
|
|
168
|
+
throw new OperationOutcomeError(
|
|
169
|
+
validationError(`Invalid value found for type; expected ${expectedType} value but got value of type ${typeOfVal}`)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function isExternalSecretLike(obj: Record<string, any>): obj is ExternalSecret {
|
|
175
|
+
return (
|
|
176
|
+
typeof obj === 'object' &&
|
|
177
|
+
typeof obj.system === 'string' &&
|
|
178
|
+
typeof obj.key === 'string' &&
|
|
179
|
+
typeof obj.type === 'string'
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function isExternalSecret(obj: Record<string, any>): obj is ExternalSecret {
|
|
184
|
+
return (
|
|
185
|
+
typeof obj === 'object' &&
|
|
186
|
+
typeof obj.system === 'string' &&
|
|
187
|
+
typeof obj.key === 'string' &&
|
|
188
|
+
VALID_PRIMITIVE_TYPES.includes(obj.type)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function assertValidExternalSecret(obj: Record<string, any>): asserts obj is ExternalSecret {
|
|
193
|
+
if (!isExternalSecret(obj)) {
|
|
194
|
+
throw new OperationOutcomeError(
|
|
195
|
+
validationError('obj is not a valid `ExternalSecret`, must contain a valid `system`, `key`, and `type` prop.')
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function normalizeInfraConfig(config: MedplumSourceInfraConfig): Promise<MedplumInfraConfig> {
|
|
201
|
+
return new InfraConfigNormalizer(config).normalizeConfig();
|
|
202
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MedplumSourceInfraConfig } from '@medplum/core';
|
|
2
2
|
import { App } from 'aws-cdk-lib';
|
|
3
3
|
import { readFileSync } from 'fs';
|
|
4
4
|
import { resolve } from 'path';
|
|
5
|
+
import { normalizeInfraConfig } from './config';
|
|
5
6
|
import { MedplumStack } from './stack';
|
|
6
7
|
|
|
7
8
|
export * from './backend';
|
|
@@ -21,12 +22,19 @@ export function main(context?: Record<string, string>): void {
|
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const config = JSON.parse(readFileSync(resolve(configFileName), 'utf-8')) as
|
|
25
|
+
const config = JSON.parse(readFileSync(resolve(configFileName), 'utf-8')) as MedplumSourceInfraConfig;
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
normalizeInfraConfig(config)
|
|
28
|
+
.then((normalizedConfig) => {
|
|
29
|
+
const stack = new MedplumStack(app, normalizedConfig);
|
|
30
|
+
console.log('Stack', stack.primaryStack.stackId);
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
app.synth();
|
|
33
|
+
})
|
|
34
|
+
.catch((err) => {
|
|
35
|
+
console.error(err);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
if (require.main === module) {
|