@friggframework/devtools 2.0.0--canary.666.81647c4.0 → 2.0.0--canary.668.8e53aa9.0
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/frigg-cli/README.md +1 -38
- package/frigg-cli/__tests__/unit/commands/build.test.js +0 -3
- package/frigg-cli/__tests__/unit/commands/deploy.test.js +0 -3
- package/frigg-cli/build-command/index.js +0 -6
- package/frigg-cli/deploy-command/index.js +0 -6
- package/frigg-cli/index.js +1 -13
- package/frigg-cli/init-command/backend-first-handler.js +46 -25
- package/frigg-cli/start-command/index.js +0 -4
- package/frigg-cli/start-command/start-command.test.js +0 -3
- package/infrastructure/create-frigg-infrastructure.js +1 -4
- package/infrastructure/docs/POSTGRES-CONFIGURATION.md +98 -60
- package/package.json +7 -7
- package/frigg-cli/__tests__/unit/commands/validate-wiring.test.js +0 -92
- package/frigg-cli/validate-command/__tests__/init-validation.test.js +0 -63
- package/frigg-cli/validate-command/__tests__/integrations-checks.test.js +0 -133
- package/frigg-cli/validate-command/__tests__/validate-app-definition.test.js +0 -326
- package/frigg-cli/validate-command/__tests__/validate-command.test.js +0 -241
- package/frigg-cli/validate-command/checks/config.js +0 -468
- package/frigg-cli/validate-command/checks/integrations.js +0 -274
- package/frigg-cli/validate-command/format-report.js +0 -58
- package/frigg-cli/validate-command/index.js +0 -107
- package/frigg-cli/validate-command/load-app-definition.js +0 -93
- package/frigg-cli/validate-command/schema-issues.js +0 -154
- package/frigg-cli/validate-command/validate-app-definition.js +0 -60
- package/infrastructure/app-definition-schema-contract.test.js +0 -607
- package/infrastructure/create-frigg-infrastructure.test.js +0 -51
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validate an app definition: the JSON Schema (keys, types, enums) plus the
|
|
3
|
-
* semantic checks the schema cannot express (ADR-051). Pure: no I/O.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const {
|
|
7
|
-
validateAppDefinition,
|
|
8
|
-
applyAppDefinitionDefaults,
|
|
9
|
-
getSchema,
|
|
10
|
-
} = require('@friggframework/schemas');
|
|
11
|
-
const { schemaErrorsToIssues } = require('./schema-issues');
|
|
12
|
-
const { checkIntegrations } = require('./checks/integrations');
|
|
13
|
-
const { checkConfig } = require('./checks/config');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {object} params
|
|
17
|
-
* @param {object} params.definition the `Definition` exported by index.js
|
|
18
|
-
* @param {string} [params.stage] the stage being built or deployed
|
|
19
|
-
* @param {object} [params.env] environment variables (default process.env)
|
|
20
|
-
* @returns {{ errors: object[], warnings: object[] }}
|
|
21
|
-
*/
|
|
22
|
-
function validateDefinition({ definition, stage, env = process.env }) {
|
|
23
|
-
const schemaResult = validateAppDefinition(definition);
|
|
24
|
-
// Integration entries are classes: checkIntegrations reports them more
|
|
25
|
-
// precisely than the schema's view of them can.
|
|
26
|
-
const issues = schemaErrorsToIssues(
|
|
27
|
-
schemaResult.errors,
|
|
28
|
-
getSchema('app-definition')
|
|
29
|
-
).filter((issue) => !issue.pointer.startsWith('/integrations/'));
|
|
30
|
-
|
|
31
|
-
let semantic = [];
|
|
32
|
-
if (definition && typeof definition === 'object') {
|
|
33
|
-
semantic = [
|
|
34
|
-
...checkIntegrations(definition),
|
|
35
|
-
...checkConfig({
|
|
36
|
-
definition: applyAppDefinitionDefaults(definition),
|
|
37
|
-
raw: definition,
|
|
38
|
-
stage,
|
|
39
|
-
env,
|
|
40
|
-
}),
|
|
41
|
-
];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// One issue per pointer and code; the schema's wins.
|
|
45
|
-
const seen = new Set(issues.map((i) => `${i.code}:${i.pointer}`));
|
|
46
|
-
for (const issue of semantic) {
|
|
47
|
-
const key = `${issue.code}:${issue.pointer}`;
|
|
48
|
-
if (!seen.has(key)) {
|
|
49
|
-
seen.add(key);
|
|
50
|
-
issues.push(issue);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
errors: issues.filter((i) => i.severity === 'error'),
|
|
56
|
-
warnings: issues.filter((i) => i.severity !== 'error'),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
module.exports = { validateDefinition };
|
|
@@ -1,607 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contract between the app-definition schema and the infrastructure
|
|
3
|
-
* builders (ADR-051). It composes the infrastructure for a matrix of app
|
|
4
|
-
* definitions, records every app-definition key the builders read, and
|
|
5
|
-
* asserts that:
|
|
6
|
-
*
|
|
7
|
-
* 1. every key a builder reads is described by the schema;
|
|
8
|
-
* 2. every key the schema describes is read by a builder, or is listed in
|
|
9
|
-
* READ_OUTSIDE_THE_BUILDERS with the code that reads it, or is marked
|
|
10
|
-
* `deprecated` in the schema (no reader);
|
|
11
|
-
* 3. no builder reads a key the schema marks `deprecated`;
|
|
12
|
-
* 4. applying the schema defaults changes nothing in the composed
|
|
13
|
-
* infrastructure: each schema default is the builders' own default;
|
|
14
|
-
* 5. the schema accepts every definition in the matrix (keys and values).
|
|
15
|
-
*
|
|
16
|
-
* When this test fails, change the schema and the runtime together.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const createDiscoveryResponse = (overrides = {}) => ({
|
|
20
|
-
defaultVpcId: 'vpc-123456',
|
|
21
|
-
vpcCidr: '172.31.0.0/16',
|
|
22
|
-
defaultSecurityGroupId: 'sg-123456',
|
|
23
|
-
privateSubnetId1: 'subnet-123456',
|
|
24
|
-
privateSubnetId2: 'subnet-789012',
|
|
25
|
-
publicSubnetId: 'subnet-public',
|
|
26
|
-
publicSubnetId1: 'subnet-public-1',
|
|
27
|
-
publicSubnetId2: 'subnet-public-2',
|
|
28
|
-
defaultRouteTableId: 'rtb-123456',
|
|
29
|
-
defaultKmsKeyId:
|
|
30
|
-
'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012',
|
|
31
|
-
existingNatGatewayId: 'nat-default123',
|
|
32
|
-
auroraClusterEndpoint:
|
|
33
|
-
'test-cluster.cluster-abc123.us-east-1.rds.amazonaws.com',
|
|
34
|
-
auroraPort: 5432,
|
|
35
|
-
auroraEngine: 'aurora-postgresql',
|
|
36
|
-
...overrides,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
jest.mock('./domains/shared/resource-discovery', () => {
|
|
40
|
-
const actual = jest.requireActual('./domains/shared/resource-discovery');
|
|
41
|
-
return { ...actual, gatherDiscoveredResources: jest.fn() };
|
|
42
|
-
});
|
|
43
|
-
jest.mock('./domains/shared/utilities/prisma-layer-manager', () => ({
|
|
44
|
-
ensurePrismaLayerExists: jest.fn().mockResolvedValue(undefined),
|
|
45
|
-
}));
|
|
46
|
-
|
|
47
|
-
const {
|
|
48
|
-
gatherDiscoveredResources,
|
|
49
|
-
} = require('./domains/shared/resource-discovery');
|
|
50
|
-
const { composeServerlessDefinition } = require('./infrastructure-composer');
|
|
51
|
-
const {
|
|
52
|
-
getSchema,
|
|
53
|
-
applyAppDefinitionDefaults,
|
|
54
|
-
validateAppDefinition,
|
|
55
|
-
} = require('@friggframework/schemas');
|
|
56
|
-
|
|
57
|
-
const schema = getSchema('app-definition');
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Schema keys no builder reads, with the code that does read them. Keep
|
|
61
|
-
* this list short: a key here is checked only by its own tests.
|
|
62
|
-
*/
|
|
63
|
-
const READ_OUTSIDE_THE_BUILDERS = {
|
|
64
|
-
user: 'core: handlers/app-definition-loader.js (userConfig), user/use-cases/*',
|
|
65
|
-
'user.*': 'core: user/use-cases/* (login, authenticate, x-frigg headers)',
|
|
66
|
-
'telemetry.*':
|
|
67
|
-
'core: telemetry/telemetry-config.js (only exporter.type is read by the environment builder)',
|
|
68
|
-
'encryption.schema':
|
|
69
|
-
'core: database/encryption/encryption-schema-registry.js',
|
|
70
|
-
'encryption.schema.*':
|
|
71
|
-
'core: database/encryption/encryption-schema-registry.js',
|
|
72
|
-
'encryption.disable':
|
|
73
|
-
'core: database/encryption/encryption-schema-registry.js',
|
|
74
|
-
'encryption.disable.*':
|
|
75
|
-
'core: database/encryption/encryption-schema-registry.js',
|
|
76
|
-
'encryption.keyAlias':
|
|
77
|
-
'devtools: shared/resource-discovery.js (discovery filter; discovery is mocked here)',
|
|
78
|
-
'database.postgres.clusterId':
|
|
79
|
-
'devtools: shared/resource-discovery.js (discovery filter)',
|
|
80
|
-
'database.postgres.instanceId':
|
|
81
|
-
'devtools: shared/resource-discovery.js (discovery filter)',
|
|
82
|
-
'database.mongoDB':
|
|
83
|
-
'core: database/config.js; devtools: scripts/build-prisma-layer.js (mocked here)',
|
|
84
|
-
'database.mongoDB.enable':
|
|
85
|
-
'core: database/config.js; devtools: scripts/build-prisma-layer.js (mocked here)',
|
|
86
|
-
'database.documentDB.enable':
|
|
87
|
-
'core: database/config.js; devtools: scripts/build-prisma-layer.js (mocked here)',
|
|
88
|
-
aws: 'devtools: shared/resource-discovery.js shouldRunDiscovery (discovery is mocked here)',
|
|
89
|
-
'aws.*': 'devtools: shared/resource-discovery.js',
|
|
90
|
-
'vpc.selfHeal':
|
|
91
|
-
'devtools: shared/resource-discovery.js (route-table repair) and vpc-builder translation',
|
|
92
|
-
deployment: 'frigg-cli: deploy-command/index.js',
|
|
93
|
-
'deployment.*': 'frigg-cli: deploy-command/index.js',
|
|
94
|
-
'ssm.parameters.*': 'frigg-cli: ssm-command/index.js (type, tier)',
|
|
95
|
-
'admin.*':
|
|
96
|
-
'admin-scripts: src/infrastructure/bootstrap.js; devtools: admin-script-builder.js',
|
|
97
|
-
stage: 'devtools: security/kms-builder.js alias inference (only without discovery)',
|
|
98
|
-
'logging.format': 'informational: records are always JSON',
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// Schema navigation
|
|
103
|
-
|
|
104
|
-
function isObjectSchema(node) {
|
|
105
|
-
return (
|
|
106
|
-
node &&
|
|
107
|
-
(node.properties ||
|
|
108
|
-
node.patternProperties ||
|
|
109
|
-
typeof node.additionalProperties === 'object')
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** The schema node describing `segments`, or null when the schema has none. */
|
|
114
|
-
function schemaNodeFor(segments) {
|
|
115
|
-
let node = schema;
|
|
116
|
-
for (const segment of segments) {
|
|
117
|
-
if (!node) return null;
|
|
118
|
-
if (node.type === 'array' || node.items) {
|
|
119
|
-
node = node.items;
|
|
120
|
-
if (segment === '[]') continue;
|
|
121
|
-
}
|
|
122
|
-
if (node.properties && node.properties[segment]) {
|
|
123
|
-
node = node.properties[segment];
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
const pattern = Object.entries(node.patternProperties || {}).find(
|
|
127
|
-
([re]) => new RegExp(re).test(segment)
|
|
128
|
-
);
|
|
129
|
-
if (pattern) {
|
|
130
|
-
node = pattern[1];
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
if (
|
|
134
|
-
node.additionalProperties &&
|
|
135
|
-
typeof node.additionalProperties === 'object'
|
|
136
|
-
) {
|
|
137
|
-
node = node.additionalProperties;
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
if (node.additionalProperties === true) return { open: true };
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
return node;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function isDeprecated(segments) {
|
|
147
|
-
let node = schema;
|
|
148
|
-
for (const segment of segments) {
|
|
149
|
-
node = node?.properties?.[segment];
|
|
150
|
-
if (!node) return false;
|
|
151
|
-
if (node.deprecated) return true;
|
|
152
|
-
}
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Every key path the schema declares (dot paths, `*` for pattern keys). */
|
|
157
|
-
function schemaPaths(node = schema, prefix = []) {
|
|
158
|
-
const paths = [];
|
|
159
|
-
for (const [key, child] of Object.entries(node.properties || {})) {
|
|
160
|
-
const path = [...prefix, key];
|
|
161
|
-
paths.push(path);
|
|
162
|
-
if (isObjectSchema(child)) paths.push(...schemaPaths(child, path));
|
|
163
|
-
}
|
|
164
|
-
if (node.patternProperties) {
|
|
165
|
-
for (const child of Object.values(node.patternProperties)) {
|
|
166
|
-
paths.push([...prefix, '*']);
|
|
167
|
-
if (isObjectSchema(child))
|
|
168
|
-
paths.push(...schemaPaths(child, [...prefix, '*']));
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return paths;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
// Read recording
|
|
176
|
-
|
|
177
|
-
const IGNORED_PROPS = new Set([
|
|
178
|
-
'toJSON',
|
|
179
|
-
'constructor',
|
|
180
|
-
'then',
|
|
181
|
-
'valueOf',
|
|
182
|
-
'toString',
|
|
183
|
-
'inspect',
|
|
184
|
-
'asymmetricMatch',
|
|
185
|
-
'$$typeof',
|
|
186
|
-
'nodeType',
|
|
187
|
-
'@@__IMMUTABLE_ITERABLE__@@',
|
|
188
|
-
'@@__IMMUTABLE_RECORD__@@',
|
|
189
|
-
]);
|
|
190
|
-
|
|
191
|
-
function isPlainObject(value) {
|
|
192
|
-
if (value === null || typeof value !== 'object') return false;
|
|
193
|
-
const proto = Object.getPrototypeOf(value);
|
|
194
|
-
return proto === Object.prototype || proto === null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Wrap a definition so every property read is recorded as a dot path.
|
|
199
|
-
* Arrays and classes are returned as they are (integrations are classes).
|
|
200
|
-
*/
|
|
201
|
-
function recordReads(definition, reads) {
|
|
202
|
-
const wrap = (target, path) =>
|
|
203
|
-
new Proxy(target, {
|
|
204
|
-
get(obj, prop, receiver) {
|
|
205
|
-
const value = Reflect.get(obj, prop, receiver);
|
|
206
|
-
if (typeof prop === 'symbol' || IGNORED_PROPS.has(prop))
|
|
207
|
-
return value;
|
|
208
|
-
const next = [...path, prop];
|
|
209
|
-
reads.add(next.join('.'));
|
|
210
|
-
return isPlainObject(value) ? wrap(value, next) : value;
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
return wrap(definition, []);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
// The matrix
|
|
218
|
-
|
|
219
|
-
class WebhookIntegration {
|
|
220
|
-
static Definition = {
|
|
221
|
-
name: 'acme',
|
|
222
|
-
version: '1.0.0',
|
|
223
|
-
modules: {},
|
|
224
|
-
webhooks: true,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
class PlainIntegration {
|
|
228
|
-
static Definition = { name: 'plain', version: '1.0.0', modules: {} };
|
|
229
|
-
}
|
|
230
|
-
class Script {
|
|
231
|
-
static Definition = {
|
|
232
|
-
name: 'cleanup',
|
|
233
|
-
version: '1.0.0',
|
|
234
|
-
schedule: { enabled: false },
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
class Report {
|
|
238
|
-
static Definition = {
|
|
239
|
-
name: 'usage',
|
|
240
|
-
version: '1.0.0',
|
|
241
|
-
output: { format: 'csv' },
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const base = () => ({
|
|
246
|
-
name: 'contract-app',
|
|
247
|
-
provider: 'aws',
|
|
248
|
-
integrations: [PlainIntegration],
|
|
249
|
-
environment: { DATABASE_URL: true, API_KEY: 'ssm' },
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
const MATRIX = {
|
|
253
|
-
minimal: () => ({ ...base() }),
|
|
254
|
-
'scaffold (external db, kms, no vpc)': () => ({
|
|
255
|
-
...base(),
|
|
256
|
-
user: { usePassword: true },
|
|
257
|
-
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
258
|
-
managementMode: 'managed',
|
|
259
|
-
vpcIsolation: 'isolated',
|
|
260
|
-
vpc: { enable: false },
|
|
261
|
-
database: {
|
|
262
|
-
postgres: {
|
|
263
|
-
enable: true,
|
|
264
|
-
management: 'use-existing',
|
|
265
|
-
endpoint: 'db.example.com',
|
|
266
|
-
port: 5432,
|
|
267
|
-
database: 'frigg',
|
|
268
|
-
username: 'frigg',
|
|
269
|
-
},
|
|
270
|
-
},
|
|
271
|
-
logging: { level: 'debug', retentionInDays: 14 },
|
|
272
|
-
ssm: {
|
|
273
|
-
enable: true,
|
|
274
|
-
parameterPrefix: '/frigg/contract',
|
|
275
|
-
kmsKeyArn: 'arn:aws:kms:us-east-1:123456789012:key/abc',
|
|
276
|
-
restrictIamToPrefix: true,
|
|
277
|
-
parameters: { API_KEY: { type: 'SecureString', tier: 'standard' } },
|
|
278
|
-
},
|
|
279
|
-
lambda: { scopedEnvironment: true, keepNestedNodeModules: false },
|
|
280
|
-
usePrismaLambdaLayer: true,
|
|
281
|
-
}),
|
|
282
|
-
'vpc discover + aurora discover + kms create': () => ({
|
|
283
|
-
...base(),
|
|
284
|
-
integrations: [WebhookIntegration, PlainIntegration],
|
|
285
|
-
vpc: {
|
|
286
|
-
enable: true,
|
|
287
|
-
management: 'discover',
|
|
288
|
-
enableVPCEndpoints: true,
|
|
289
|
-
selfHeal: true,
|
|
290
|
-
cidrBlock: '10.0.0.0/16',
|
|
291
|
-
},
|
|
292
|
-
database: {
|
|
293
|
-
postgres: {
|
|
294
|
-
enable: true,
|
|
295
|
-
management: 'discover',
|
|
296
|
-
minCapacity: 0.5,
|
|
297
|
-
maxCapacity: 2,
|
|
298
|
-
publiclyAccessible: false,
|
|
299
|
-
autoCreateCredentials: false,
|
|
300
|
-
engineVersion: '15.13',
|
|
301
|
-
},
|
|
302
|
-
dynamodb: { enable: true },
|
|
303
|
-
},
|
|
304
|
-
encryption: {
|
|
305
|
-
fieldLevelEncryptionMethod: 'kms',
|
|
306
|
-
createResourceIfNoneFound: true,
|
|
307
|
-
kmsKeyAlias: true,
|
|
308
|
-
},
|
|
309
|
-
websockets: { enable: true },
|
|
310
|
-
scheduler: { enable: true },
|
|
311
|
-
telemetry: {
|
|
312
|
-
exporter: { type: 'otlp', endpoint: 'https://otel.example.com' },
|
|
313
|
-
},
|
|
314
|
-
}),
|
|
315
|
-
'vpc create-new + aurora managed': () => ({
|
|
316
|
-
...base(),
|
|
317
|
-
vpc: {
|
|
318
|
-
enable: true,
|
|
319
|
-
management: 'create-new',
|
|
320
|
-
subnets: { management: 'create' },
|
|
321
|
-
natGateway: { management: 'createAndManage' },
|
|
322
|
-
shareAcrossStages: false,
|
|
323
|
-
},
|
|
324
|
-
database: { postgres: { enable: true, management: 'managed' } },
|
|
325
|
-
encryption: { fieldLevelEncryptionMethod: 'aes' },
|
|
326
|
-
}),
|
|
327
|
-
'vpc use-existing': () => ({
|
|
328
|
-
...base(),
|
|
329
|
-
vpc: {
|
|
330
|
-
enable: true,
|
|
331
|
-
management: 'use-existing',
|
|
332
|
-
vpcId: 'vpc-abc',
|
|
333
|
-
securityGroupIds: ['sg-abc'],
|
|
334
|
-
subnets: {
|
|
335
|
-
management: 'use-existing',
|
|
336
|
-
ids: ['subnet-a', 'subnet-b'],
|
|
337
|
-
},
|
|
338
|
-
natGateway: { id: 'nat-abc' },
|
|
339
|
-
},
|
|
340
|
-
}),
|
|
341
|
-
'managed + isolated': () => ({
|
|
342
|
-
...base(),
|
|
343
|
-
managementMode: 'managed',
|
|
344
|
-
vpcIsolation: 'isolated',
|
|
345
|
-
vpc: { enable: true },
|
|
346
|
-
database: { postgres: { enable: true } },
|
|
347
|
-
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
348
|
-
}),
|
|
349
|
-
'managed + shared': () => ({
|
|
350
|
-
...base(),
|
|
351
|
-
managementMode: 'managed',
|
|
352
|
-
vpcIsolation: 'shared',
|
|
353
|
-
vpc: { enable: true },
|
|
354
|
-
database: { postgres: { enable: true } },
|
|
355
|
-
encryption: { fieldLevelEncryptionMethod: 'kms' },
|
|
356
|
-
}),
|
|
357
|
-
'existing mode': () => ({
|
|
358
|
-
...base(),
|
|
359
|
-
managementMode: 'existing',
|
|
360
|
-
vpc: {
|
|
361
|
-
enable: true,
|
|
362
|
-
external: {
|
|
363
|
-
vpcId: 'vpc-x',
|
|
364
|
-
subnetIds: ['subnet-a', 'subnet-b'],
|
|
365
|
-
securityGroupIds: ['sg-x'],
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
database: {
|
|
369
|
-
postgres: {
|
|
370
|
-
enable: true,
|
|
371
|
-
external: {
|
|
372
|
-
clusterIdentifier: 'c',
|
|
373
|
-
instanceIdentifier: 'i',
|
|
374
|
-
subnetGroupName: 'g',
|
|
375
|
-
secretArn: 'arn:aws:secretsmanager:us-east-1:1:secret:x',
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
},
|
|
379
|
-
}),
|
|
380
|
-
'ownership-based': () => ({
|
|
381
|
-
...base(),
|
|
382
|
-
vpc: {
|
|
383
|
-
enable: true,
|
|
384
|
-
ownership: {
|
|
385
|
-
vpc: 'stack',
|
|
386
|
-
securityGroup: 'stack',
|
|
387
|
-
subnets: 'stack',
|
|
388
|
-
natGateway: 'stack',
|
|
389
|
-
vpcEndpoints: 'stack',
|
|
390
|
-
},
|
|
391
|
-
config: {
|
|
392
|
-
cidrBlock: '10.2.0.0/16',
|
|
393
|
-
selfHeal: true,
|
|
394
|
-
natGateway: { enable: true },
|
|
395
|
-
enableVpcEndpoints: true,
|
|
396
|
-
},
|
|
397
|
-
},
|
|
398
|
-
database: {
|
|
399
|
-
postgres: {
|
|
400
|
-
enable: true,
|
|
401
|
-
ownership: {
|
|
402
|
-
cluster: 'stack',
|
|
403
|
-
instance: 'stack',
|
|
404
|
-
subnetGroup: 'stack',
|
|
405
|
-
secret: 'stack',
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
},
|
|
409
|
-
encryption: {
|
|
410
|
-
fieldLevelEncryptionMethod: 'kms',
|
|
411
|
-
ownership: { key: 'stack' },
|
|
412
|
-
},
|
|
413
|
-
migration: { ownership: { bucket: 'stack', queue: 'stack' } },
|
|
414
|
-
}),
|
|
415
|
-
'ownership-based, external': () => ({
|
|
416
|
-
...base(),
|
|
417
|
-
vpc: {
|
|
418
|
-
enable: true,
|
|
419
|
-
ownership: {
|
|
420
|
-
vpc: 'external',
|
|
421
|
-
securityGroup: 'external',
|
|
422
|
-
subnets: 'external',
|
|
423
|
-
natGateway: 'external',
|
|
424
|
-
vpcEndpoints: 'external',
|
|
425
|
-
},
|
|
426
|
-
external: {
|
|
427
|
-
vpcId: 'vpc-ext',
|
|
428
|
-
securityGroupIds: ['sg-ext'],
|
|
429
|
-
subnetIds: ['subnet-e1', 'subnet-e2'],
|
|
430
|
-
natGatewayId: 'nat-ext',
|
|
431
|
-
vpcEndpointIds: { s3: 'vpce-s3', dynamodb: 'vpce-ddb' },
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
}),
|
|
435
|
-
'admin scripts and reports': () => ({
|
|
436
|
-
...base(),
|
|
437
|
-
adminScripts: [Script],
|
|
438
|
-
reports: [Report],
|
|
439
|
-
admin: { includeBuiltinReports: true, enableScheduling: true },
|
|
440
|
-
database: { postgres: { enable: false }, mongoDB: { enable: true } },
|
|
441
|
-
}),
|
|
442
|
-
documentdb: () => ({
|
|
443
|
-
...base(),
|
|
444
|
-
database: {
|
|
445
|
-
documentDB: {
|
|
446
|
-
enable: true,
|
|
447
|
-
tlsCAFile: './security/global-bundle.pem',
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
usePrismaLambdaLayer: false,
|
|
451
|
-
}),
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
// ---------------------------------------------------------------------------
|
|
455
|
-
|
|
456
|
-
async function compose(definition) {
|
|
457
|
-
return composeServerlessDefinition(definition);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
describe('app-definition schema ⇄ infrastructure builders contract (ADR-051)', () => {
|
|
461
|
-
const reads = new Set();
|
|
462
|
-
const composed = {};
|
|
463
|
-
let logSpy;
|
|
464
|
-
let warnSpy;
|
|
465
|
-
let errorSpy;
|
|
466
|
-
const savedArgv = process.argv;
|
|
467
|
-
const savedEnv = { ...process.env };
|
|
468
|
-
|
|
469
|
-
beforeAll(async () => {
|
|
470
|
-
logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
471
|
-
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
472
|
-
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
473
|
-
process.argv = ['node', 'test'];
|
|
474
|
-
delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
|
|
475
|
-
process.env.AWS_REGION = 'us-east-1';
|
|
476
|
-
gatherDiscoveredResources.mockImplementation(async () =>
|
|
477
|
-
createDiscoveryResponse()
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
for (const [name, make] of Object.entries(MATRIX)) {
|
|
481
|
-
composed[name] = await compose(recordReads(make(), reads));
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
afterAll(() => {
|
|
486
|
-
logSpy.mockRestore();
|
|
487
|
-
warnSpy.mockRestore();
|
|
488
|
-
errorSpy.mockRestore();
|
|
489
|
-
process.argv = savedArgv;
|
|
490
|
-
process.env = savedEnv;
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it('composes every definition in the matrix', () => {
|
|
494
|
-
for (const name of Object.keys(MATRIX)) {
|
|
495
|
-
expect(composed[name]).toEqual(
|
|
496
|
-
expect.objectContaining({ service: expect.any(String) })
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
it('the schema accepts every definition the builders compose', () => {
|
|
502
|
-
const invalid = Object.entries(MATRIX)
|
|
503
|
-
.map(([name, make]) => [name, validateAppDefinition(make()).errors])
|
|
504
|
-
.filter(([, errors]) => errors);
|
|
505
|
-
|
|
506
|
-
expect(invalid).toEqual([]);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it('records the reads (sanity check of the recorder)', () => {
|
|
510
|
-
for (const path of [
|
|
511
|
-
'vpc.management',
|
|
512
|
-
'database.postgres.management',
|
|
513
|
-
'encryption.fieldLevelEncryptionMethod',
|
|
514
|
-
'managementMode',
|
|
515
|
-
'environment.DATABASE_URL',
|
|
516
|
-
]) {
|
|
517
|
-
expect([path, reads.has(path)]).toEqual([path, true]);
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
it('every key a builder reads is in the schema', () => {
|
|
522
|
-
const missing = [...reads]
|
|
523
|
-
.map((path) => path.split('.'))
|
|
524
|
-
// integrations[i] are classes: their Definition is checked by `frigg validate`.
|
|
525
|
-
.filter(
|
|
526
|
-
(segments) =>
|
|
527
|
-
!['integrations', 'adminScripts', 'reports'].includes(
|
|
528
|
-
segments[0]
|
|
529
|
-
) || segments.length === 1
|
|
530
|
-
)
|
|
531
|
-
// Keys a builder writes into its own translated copy (e.g. vpc._x) are internal.
|
|
532
|
-
.filter((segments) => !segments.some((s) => s.startsWith('_')))
|
|
533
|
-
.filter((segments) => schemaNodeFor(segments) === null)
|
|
534
|
-
.map((segments) => segments.join('.'));
|
|
535
|
-
|
|
536
|
-
expect(missing).toEqual([]);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
it('no builder reads a key the schema marks deprecated', () => {
|
|
540
|
-
const readDeprecated = [...reads].filter((path) =>
|
|
541
|
-
isDeprecated(path.split('.'))
|
|
542
|
-
);
|
|
543
|
-
|
|
544
|
-
expect(readDeprecated).toEqual([]);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it('every schema key is read by a builder, read elsewhere (listed), or deprecated', () => {
|
|
548
|
-
const readPaths = new Set(reads);
|
|
549
|
-
const isRead = (segments) => {
|
|
550
|
-
const dotted = segments.join('.');
|
|
551
|
-
if (readPaths.has(dotted)) return true;
|
|
552
|
-
// `*` (patternProperties) matches any concrete key read
|
|
553
|
-
if (segments.includes('*')) {
|
|
554
|
-
const re = new RegExp(
|
|
555
|
-
`^${segments
|
|
556
|
-
.map((s) =>
|
|
557
|
-
s === '*' ? '[^.]+' : s.replace(/[.$]/g, '\\$&')
|
|
558
|
-
)
|
|
559
|
-
.join('\\.')}$`
|
|
560
|
-
);
|
|
561
|
-
return [...readPaths].some((p) => re.test(p));
|
|
562
|
-
}
|
|
563
|
-
return false;
|
|
564
|
-
};
|
|
565
|
-
const listedElsewhere = (segments) => {
|
|
566
|
-
for (let i = segments.length; i > 0; i--) {
|
|
567
|
-
const prefix = segments.slice(0, i).join('.');
|
|
568
|
-
if (READ_OUTSIDE_THE_BUILDERS[prefix] && i === segments.length)
|
|
569
|
-
return true;
|
|
570
|
-
if (
|
|
571
|
-
READ_OUTSIDE_THE_BUILDERS[`${prefix}.*`] &&
|
|
572
|
-
i < segments.length
|
|
573
|
-
)
|
|
574
|
-
return true;
|
|
575
|
-
}
|
|
576
|
-
return false;
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
const unread = schemaPaths()
|
|
580
|
-
.filter(
|
|
581
|
-
(segments) => !isDeprecated(segments.filter((s) => s !== '*'))
|
|
582
|
-
)
|
|
583
|
-
.filter(
|
|
584
|
-
(segments) => !isRead(segments) && !listedElsewhere(segments)
|
|
585
|
-
)
|
|
586
|
-
.map((segments) => segments.join('.'));
|
|
587
|
-
|
|
588
|
-
expect(unread).toEqual([]);
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it('applying the schema defaults does not change the composed infrastructure', async () => {
|
|
592
|
-
const differences = [];
|
|
593
|
-
for (const [name, make] of Object.entries(MATRIX)) {
|
|
594
|
-
const withoutDefaults = await compose(make());
|
|
595
|
-
const withDefaults = await compose(
|
|
596
|
-
applyAppDefinitionDefaults(make())
|
|
597
|
-
);
|
|
598
|
-
try {
|
|
599
|
-
expect(withDefaults).toEqual(withoutDefaults);
|
|
600
|
-
} catch (error) {
|
|
601
|
-
differences.push(`${name}:\n${error.message}`);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
expect(differences).toEqual([]);
|
|
606
|
-
});
|
|
607
|
-
});
|