@friggframework/devtools 2.0.0--canary.461.8cf93ae.0 → 2.0.0--canary.474.213c7d9.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/infrastructure/ARCHITECTURE.md +487 -0
- package/infrastructure/domains/database/aurora-builder.js +234 -57
- package/infrastructure/domains/database/aurora-builder.test.js +7 -2
- package/infrastructure/domains/database/aurora-resolver.js +210 -0
- package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
- package/infrastructure/domains/database/migration-builder.js +256 -215
- package/infrastructure/domains/database/migration-builder.test.js +5 -111
- package/infrastructure/domains/database/migration-resolver.js +163 -0
- package/infrastructure/domains/database/migration-resolver.test.js +337 -0
- package/infrastructure/domains/integration/integration-builder.js +258 -84
- package/infrastructure/domains/integration/integration-resolver.js +170 -0
- package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
- package/infrastructure/domains/networking/vpc-builder.js +856 -135
- package/infrastructure/domains/networking/vpc-builder.test.js +10 -6
- package/infrastructure/domains/networking/vpc-resolver.js +324 -0
- package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
- package/infrastructure/domains/security/kms-builder.js +179 -22
- package/infrastructure/domains/security/kms-resolver.js +96 -0
- package/infrastructure/domains/security/kms-resolver.test.js +216 -0
- package/infrastructure/domains/shared/base-resolver.js +186 -0
- package/infrastructure/domains/shared/base-resolver.test.js +305 -0
- package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
- package/infrastructure/domains/shared/cloudformation-discovery.test.js +26 -1
- package/infrastructure/domains/shared/types/app-definition.js +205 -0
- package/infrastructure/domains/shared/types/discovery-result.js +106 -0
- package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
- package/infrastructure/domains/shared/types/index.js +46 -0
- package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
- package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
- package/package.json +6 -6
- package/infrastructure/REFACTOR.md +0 -532
- package/infrastructure/TRANSFORMATION-VISUAL.md +0 -239
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
const VpcResourceResolver = require('./vpc-resolver');
|
|
2
|
+
const { ResourceOwnership } = require('../shared/types');
|
|
3
|
+
|
|
4
|
+
describe('VpcResourceResolver', () => {
|
|
5
|
+
let resolver;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
resolver = new VpcResourceResolver();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('resolveVpc', () => {
|
|
12
|
+
it('should resolve to EXTERNAL when user specifies external', () => {
|
|
13
|
+
const appDefinition = {
|
|
14
|
+
vpc: {
|
|
15
|
+
ownership: { vpc: 'external' },
|
|
16
|
+
external: { vpcId: 'vpc-external-123' }
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
20
|
+
|
|
21
|
+
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
22
|
+
|
|
23
|
+
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
24
|
+
expect(decision.physicalId).toBe('vpc-external-123');
|
|
25
|
+
expect(decision.reason).toContain('User specified ownership=external');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should throw when external specified but vpcId missing', () => {
|
|
29
|
+
const appDefinition = {
|
|
30
|
+
vpc: {
|
|
31
|
+
ownership: { vpc: 'external' },
|
|
32
|
+
external: {}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
36
|
+
|
|
37
|
+
expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
|
|
38
|
+
"ownership='external' for vpcId requires external.vpcId"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should resolve to STACK when user specifies stack', () => {
|
|
43
|
+
const appDefinition = {
|
|
44
|
+
vpc: {
|
|
45
|
+
ownership: { vpc: 'stack' }
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const discovery = {
|
|
49
|
+
stackManaged: [
|
|
50
|
+
{ logicalId: 'FriggVPC', physicalId: 'vpc-stack-123', resourceType: 'AWS::EC2::VPC' }
|
|
51
|
+
],
|
|
52
|
+
external: [],
|
|
53
|
+
fromCloudFormation: true
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
57
|
+
|
|
58
|
+
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
59
|
+
expect(decision.physicalId).toBe('vpc-stack-123');
|
|
60
|
+
expect(decision.reason).toContain('User specified ownership=stack');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should auto-resolve to STACK when VPC in stack (CRITICAL)', () => {
|
|
64
|
+
const appDefinition = {
|
|
65
|
+
vpc: { ownership: { vpc: 'auto' } }
|
|
66
|
+
};
|
|
67
|
+
const discovery = {
|
|
68
|
+
stackManaged: [
|
|
69
|
+
{ logicalId: 'FriggVPC', physicalId: 'vpc-in-stack', resourceType: 'AWS::EC2::VPC' }
|
|
70
|
+
],
|
|
71
|
+
external: [],
|
|
72
|
+
fromCloudFormation: true
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
76
|
+
|
|
77
|
+
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
78
|
+
expect(decision.physicalId).toBe('vpc-in-stack');
|
|
79
|
+
expect(decision.reason).toContain('Found in CloudFormation stack');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should auto-resolve to EXTERNAL when found externally', () => {
|
|
83
|
+
const appDefinition = {
|
|
84
|
+
vpc: { ownership: { vpc: 'auto' } }
|
|
85
|
+
};
|
|
86
|
+
const discovery = {
|
|
87
|
+
stackManaged: [],
|
|
88
|
+
external: [
|
|
89
|
+
{ physicalId: 'vpc-external', resourceType: 'AWS::EC2::VPC', source: 'tag-search' }
|
|
90
|
+
],
|
|
91
|
+
fromCloudFormation: false
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
95
|
+
|
|
96
|
+
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
97
|
+
expect(decision.physicalId).toBe('vpc-external');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should auto-resolve to STACK when not found (create new)', () => {
|
|
101
|
+
const appDefinition = {
|
|
102
|
+
vpc: { ownership: { vpc: 'auto' } }
|
|
103
|
+
};
|
|
104
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
105
|
+
|
|
106
|
+
const decision = resolver.resolveVpc(appDefinition, discovery);
|
|
107
|
+
|
|
108
|
+
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
109
|
+
expect(decision.physicalId).toBeUndefined();
|
|
110
|
+
expect(decision.reason).toContain('No existing resource found');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('resolveSecurityGroup', () => {
|
|
115
|
+
it('should resolve to EXTERNAL with user-provided IDs', () => {
|
|
116
|
+
const appDefinition = {
|
|
117
|
+
vpc: {
|
|
118
|
+
ownership: { securityGroup: 'external' },
|
|
119
|
+
external: { securityGroupIds: ['sg-1', 'sg-2'] }
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
123
|
+
|
|
124
|
+
const decision = resolver.resolveSecurityGroup(appDefinition, discovery);
|
|
125
|
+
|
|
126
|
+
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
127
|
+
expect(decision.physicalIds).toEqual(['sg-1', 'sg-2']);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should auto-resolve to STACK when FriggLambdaSecurityGroup in stack', () => {
|
|
131
|
+
const appDefinition = { vpc: { ownership: { securityGroup: 'auto' } } };
|
|
132
|
+
const discovery = {
|
|
133
|
+
stackManaged: [
|
|
134
|
+
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-069629001ade41c9a', resourceType: 'AWS::EC2::SecurityGroup' }
|
|
135
|
+
],
|
|
136
|
+
external: [],
|
|
137
|
+
fromCloudFormation: true
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const decision = resolver.resolveSecurityGroup(appDefinition, discovery);
|
|
141
|
+
|
|
142
|
+
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
143
|
+
expect(decision.physicalId).toBe('sg-069629001ade41c9a');
|
|
144
|
+
expect(decision.reason).toContain('Found FriggLambdaSecurityGroup in CloudFormation stack');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('resolveSubnets', () => {
|
|
149
|
+
it('should resolve to EXTERNAL with user-provided subnet IDs', () => {
|
|
150
|
+
const appDefinition = {
|
|
151
|
+
vpc: {
|
|
152
|
+
ownership: { subnets: 'external' },
|
|
153
|
+
external: { subnetIds: ['subnet-1', 'subnet-2', 'subnet-3'] }
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
157
|
+
|
|
158
|
+
const decision = resolver.resolveSubnets(appDefinition, discovery);
|
|
159
|
+
|
|
160
|
+
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
161
|
+
expect(decision.physicalIds).toEqual(['subnet-1', 'subnet-2', 'subnet-3']);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should resolve to STACK when subnets found in stack', () => {
|
|
165
|
+
const appDefinition = { vpc: { ownership: { subnets: 'auto' } } };
|
|
166
|
+
const discovery = {
|
|
167
|
+
stackManaged: [
|
|
168
|
+
{ logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-a', resourceType: 'AWS::EC2::Subnet' },
|
|
169
|
+
{ logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-b', resourceType: 'AWS::EC2::Subnet' }
|
|
170
|
+
],
|
|
171
|
+
external: [],
|
|
172
|
+
fromCloudFormation: true
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const decision = resolver.resolveSubnets(appDefinition, discovery);
|
|
176
|
+
|
|
177
|
+
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
178
|
+
expect(decision.physicalIds).toEqual(['subnet-a', 'subnet-b']);
|
|
179
|
+
expect(decision.metadata.subnet1).toBe('subnet-a');
|
|
180
|
+
expect(decision.metadata.subnet2).toBe('subnet-b');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should resolve to EXTERNAL when found externally', () => {
|
|
184
|
+
const appDefinition = { vpc: { ownership: { subnets: 'auto' } } };
|
|
185
|
+
const discovery = {
|
|
186
|
+
stackManaged: [],
|
|
187
|
+
external: [
|
|
188
|
+
{ physicalId: 'subnet-ext-1', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' },
|
|
189
|
+
{ physicalId: 'subnet-ext-2', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' },
|
|
190
|
+
{ physicalId: 'subnet-ext-3', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' }
|
|
191
|
+
],
|
|
192
|
+
fromCloudFormation: false
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const decision = resolver.resolveSubnets(appDefinition, discovery);
|
|
196
|
+
|
|
197
|
+
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
198
|
+
expect(decision.physicalIds).toHaveLength(2); // Takes first 2
|
|
199
|
+
expect(decision.physicalIds).toEqual(['subnet-ext-1', 'subnet-ext-2']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should resolve to STACK when no subnets found (create new)', () => {
|
|
203
|
+
const appDefinition = { vpc: { ownership: { subnets: 'auto' } } };
|
|
204
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
205
|
+
|
|
206
|
+
const decision = resolver.resolveSubnets(appDefinition, discovery);
|
|
207
|
+
|
|
208
|
+
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
209
|
+
expect(decision.physicalId).toBeNull();
|
|
210
|
+
expect(decision.reason).toContain('No existing subnets found');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('resolveNatGateway', () => {
|
|
215
|
+
it('should return null decision when NAT disabled', () => {
|
|
216
|
+
const appDefinition = {
|
|
217
|
+
vpc: {
|
|
218
|
+
ownership: { natGateway: 'auto' },
|
|
219
|
+
config: { natGateway: { enable: false } }
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
223
|
+
|
|
224
|
+
const decision = resolver.resolveNatGateway(appDefinition, discovery);
|
|
225
|
+
|
|
226
|
+
expect(decision.ownership).toBeNull();
|
|
227
|
+
expect(decision.reason).toContain('NAT Gateway disabled');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should resolve to EXTERNAL with user-provided ID', () => {
|
|
231
|
+
const appDefinition = {
|
|
232
|
+
vpc: {
|
|
233
|
+
ownership: { natGateway: 'external' },
|
|
234
|
+
external: { natGatewayId: 'nat-external-123' }
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
238
|
+
|
|
239
|
+
const decision = resolver.resolveNatGateway(appDefinition, discovery);
|
|
240
|
+
|
|
241
|
+
expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
242
|
+
expect(decision.physicalId).toBe('nat-external-123');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should auto-resolve to STACK when found in stack', () => {
|
|
246
|
+
const appDefinition = { vpc: { ownership: { natGateway: 'auto' } } };
|
|
247
|
+
const discovery = {
|
|
248
|
+
stackManaged: [
|
|
249
|
+
{ logicalId: 'FriggNatGateway', physicalId: 'nat-stack-123', resourceType: 'AWS::EC2::NatGateway' }
|
|
250
|
+
],
|
|
251
|
+
external: [],
|
|
252
|
+
fromCloudFormation: true
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const decision = resolver.resolveNatGateway(appDefinition, discovery);
|
|
256
|
+
|
|
257
|
+
expect(decision.ownership).toBe(ResourceOwnership.STACK);
|
|
258
|
+
expect(decision.physicalId).toBe('nat-stack-123');
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('resolveVpcEndpoints', () => {
|
|
263
|
+
it('should return null decisions when endpoints disabled', () => {
|
|
264
|
+
const appDefinition = {
|
|
265
|
+
vpc: {
|
|
266
|
+
ownership: { vpcEndpoints: 'auto' },
|
|
267
|
+
config: { enableVpcEndpoints: false }
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
271
|
+
|
|
272
|
+
const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
|
|
273
|
+
|
|
274
|
+
expect(decisions.s3.ownership).toBeNull();
|
|
275
|
+
expect(decisions.dynamodb.ownership).toBeNull();
|
|
276
|
+
expect(decisions.kms.ownership).toBeNull();
|
|
277
|
+
expect(decisions.secretsManager.ownership).toBeNull();
|
|
278
|
+
expect(decisions.sqs.ownership).toBeNull();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should resolve to EXTERNAL with user-provided endpoint IDs', () => {
|
|
282
|
+
const appDefinition = {
|
|
283
|
+
vpc: {
|
|
284
|
+
ownership: { vpcEndpoints: 'external' },
|
|
285
|
+
external: {
|
|
286
|
+
vpcEndpointIds: {
|
|
287
|
+
s3: 'vpce-s3-123',
|
|
288
|
+
dynamodb: 'vpce-ddb-456'
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
294
|
+
|
|
295
|
+
const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
|
|
296
|
+
|
|
297
|
+
expect(decisions.s3.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
298
|
+
expect(decisions.s3.physicalId).toBe('vpce-s3-123');
|
|
299
|
+
expect(decisions.dynamodb.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
300
|
+
expect(decisions.dynamodb.physicalId).toBe('vpce-ddb-456');
|
|
301
|
+
expect(decisions.kms.ownership).toBeNull(); // Not provided
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should auto-resolve to STACK when endpoints found in stack', () => {
|
|
305
|
+
const appDefinition = { vpc: { ownership: { vpcEndpoints: 'auto' } } };
|
|
306
|
+
const discovery = {
|
|
307
|
+
stackManaged: [
|
|
308
|
+
{ logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack', resourceType: 'AWS::EC2::VPCEndpoint' },
|
|
309
|
+
{ logicalId: 'FriggDynamoDBVPCEndpoint', physicalId: 'vpce-ddb-stack', resourceType: 'AWS::EC2::VPCEndpoint' }
|
|
310
|
+
],
|
|
311
|
+
external: [],
|
|
312
|
+
fromCloudFormation: true
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
|
|
316
|
+
|
|
317
|
+
expect(decisions.s3.ownership).toBe(ResourceOwnership.STACK);
|
|
318
|
+
expect(decisions.s3.physicalId).toBe('vpce-s3-stack');
|
|
319
|
+
expect(decisions.dynamodb.ownership).toBe(ResourceOwnership.STACK);
|
|
320
|
+
expect(decisions.dynamodb.physicalId).toBe('vpce-ddb-stack');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should auto-resolve mixed: some in stack, some new', () => {
|
|
324
|
+
const appDefinition = {
|
|
325
|
+
vpc: { ownership: { vpcEndpoints: 'auto' } },
|
|
326
|
+
encryption: { fieldLevelEncryptionMethod: 'kms' } // Enable KMS endpoint
|
|
327
|
+
};
|
|
328
|
+
const discovery = {
|
|
329
|
+
stackManaged: [
|
|
330
|
+
{ logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack', resourceType: 'AWS::EC2::VPCEndpoint' }
|
|
331
|
+
],
|
|
332
|
+
external: [],
|
|
333
|
+
fromCloudFormation: true
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
|
|
337
|
+
|
|
338
|
+
expect(decisions.s3.ownership).toBe(ResourceOwnership.STACK);
|
|
339
|
+
expect(decisions.s3.physicalId).toBe('vpce-s3-stack');
|
|
340
|
+
|
|
341
|
+
// Others not in stack - should create new
|
|
342
|
+
expect(decisions.dynamodb.ownership).toBe(ResourceOwnership.STACK);
|
|
343
|
+
expect(decisions.dynamodb.physicalId).toBeUndefined();
|
|
344
|
+
expect(decisions.kms.ownership).toBe(ResourceOwnership.STACK);
|
|
345
|
+
expect(decisions.secretsManager.ownership).toBe(ResourceOwnership.STACK);
|
|
346
|
+
expect(decisions.sqs.ownership).toBe(ResourceOwnership.STACK);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('resolveAll', () => {
|
|
351
|
+
it('should resolve all VPC resources at once', () => {
|
|
352
|
+
const appDefinition = {
|
|
353
|
+
vpc: {
|
|
354
|
+
ownership: {
|
|
355
|
+
vpc: 'auto',
|
|
356
|
+
securityGroup: 'auto',
|
|
357
|
+
subnets: 'auto',
|
|
358
|
+
natGateway: 'auto',
|
|
359
|
+
vpcEndpoints: 'auto'
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const discovery = {
|
|
364
|
+
stackManaged: [
|
|
365
|
+
{ logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
|
|
366
|
+
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-456', resourceType: 'AWS::EC2::SecurityGroup' },
|
|
367
|
+
{ logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
|
|
368
|
+
{ logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' },
|
|
369
|
+
{ logicalId: 'FriggNatGateway', physicalId: 'nat-789', resourceType: 'AWS::EC2::NatGateway' },
|
|
370
|
+
{ logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3', resourceType: 'AWS::EC2::VPCEndpoint' }
|
|
371
|
+
],
|
|
372
|
+
external: [],
|
|
373
|
+
fromCloudFormation: true
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const decisions = resolver.resolveAll(appDefinition, discovery);
|
|
377
|
+
|
|
378
|
+
expect(decisions.vpc.ownership).toBe(ResourceOwnership.STACK);
|
|
379
|
+
expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
|
|
380
|
+
expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
|
|
381
|
+
expect(decisions.natGateway.ownership).toBe(ResourceOwnership.STACK);
|
|
382
|
+
expect(decisions.vpcEndpoints.s3.ownership).toBe(ResourceOwnership.STACK);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should handle mixed ownership scenarios', () => {
|
|
386
|
+
const appDefinition = {
|
|
387
|
+
vpc: {
|
|
388
|
+
ownership: {
|
|
389
|
+
vpc: 'external',
|
|
390
|
+
securityGroup: 'stack',
|
|
391
|
+
subnets: 'stack',
|
|
392
|
+
natGateway: 'auto',
|
|
393
|
+
vpcEndpoints: 'auto'
|
|
394
|
+
},
|
|
395
|
+
external: {
|
|
396
|
+
vpcId: 'vpc-shared-production'
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
const discovery = {
|
|
401
|
+
stackManaged: [
|
|
402
|
+
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-stack', resourceType: 'AWS::EC2::SecurityGroup' },
|
|
403
|
+
{ logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
|
|
404
|
+
{ logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' }
|
|
405
|
+
],
|
|
406
|
+
external: [],
|
|
407
|
+
fromCloudFormation: true
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const decisions = resolver.resolveAll(appDefinition, discovery);
|
|
411
|
+
|
|
412
|
+
expect(decisions.vpc.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
413
|
+
expect(decisions.vpc.physicalId).toBe('vpc-shared-production');
|
|
414
|
+
expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
|
|
415
|
+
expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
|
|
416
|
+
expect(decisions.natGateway.ownership).toBe(ResourceOwnership.STACK); // Not found, create new
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe('real-world scenarios', () => {
|
|
421
|
+
it('scenario: fresh deploy, no resources exist', () => {
|
|
422
|
+
const appDefinition = {
|
|
423
|
+
vpc: { enable: true, ownership: {} }
|
|
424
|
+
};
|
|
425
|
+
const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
|
|
426
|
+
|
|
427
|
+
const decisions = resolver.resolveAll(appDefinition, discovery);
|
|
428
|
+
|
|
429
|
+
// All should be STACK (create new)
|
|
430
|
+
expect(decisions.vpc.ownership).toBe(ResourceOwnership.STACK);
|
|
431
|
+
expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
|
|
432
|
+
expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
|
|
433
|
+
expect(decisions.natGateway.ownership).toBe(ResourceOwnership.STACK);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('scenario: redeploy existing stack (the original bug case)', () => {
|
|
437
|
+
const appDefinition = {
|
|
438
|
+
vpc: { enable: true, ownership: {} }
|
|
439
|
+
};
|
|
440
|
+
const discovery = {
|
|
441
|
+
stackManaged: [
|
|
442
|
+
{ logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
|
|
443
|
+
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-069629001ade41c9a', resourceType: 'AWS::EC2::SecurityGroup' },
|
|
444
|
+
{ logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
|
|
445
|
+
{ logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' }
|
|
446
|
+
],
|
|
447
|
+
external: [],
|
|
448
|
+
fromCloudFormation: true,
|
|
449
|
+
stackName: 'create-frigg-app-production'
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const decisions = resolver.resolveAll(appDefinition, discovery);
|
|
453
|
+
|
|
454
|
+
// CRITICAL: All resources in stack must get STACK ownership
|
|
455
|
+
expect(decisions.vpc.ownership).toBe(ResourceOwnership.STACK);
|
|
456
|
+
expect(decisions.vpc.physicalId).toBe('vpc-123');
|
|
457
|
+
|
|
458
|
+
expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
|
|
459
|
+
expect(decisions.securityGroup.physicalId).toBe('sg-069629001ade41c9a');
|
|
460
|
+
expect(decisions.securityGroup.reason).toContain('Found FriggLambdaSecurityGroup in CloudFormation stack');
|
|
461
|
+
|
|
462
|
+
expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
|
|
463
|
+
expect(decisions.subnets.physicalIds).toEqual(['subnet-1', 'subnet-2']);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('scenario: use shared VPC with stack-managed resources', () => {
|
|
467
|
+
const appDefinition = {
|
|
468
|
+
vpc: {
|
|
469
|
+
enable: true,
|
|
470
|
+
ownership: {
|
|
471
|
+
vpc: 'external',
|
|
472
|
+
securityGroup: 'auto',
|
|
473
|
+
subnets: 'auto'
|
|
474
|
+
},
|
|
475
|
+
external: {
|
|
476
|
+
vpcId: 'vpc-shared-across-stages'
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const discovery = {
|
|
481
|
+
stackManaged: [
|
|
482
|
+
{ logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-stage-specific', resourceType: 'AWS::EC2::SecurityGroup' },
|
|
483
|
+
{ logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
|
|
484
|
+
{ logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' }
|
|
485
|
+
],
|
|
486
|
+
external: [],
|
|
487
|
+
fromCloudFormation: true
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const decisions = resolver.resolveAll(appDefinition, discovery);
|
|
491
|
+
|
|
492
|
+
// VPC is external
|
|
493
|
+
expect(decisions.vpc.ownership).toBe(ResourceOwnership.EXTERNAL);
|
|
494
|
+
expect(decisions.vpc.physicalId).toBe('vpc-shared-across-stages');
|
|
495
|
+
|
|
496
|
+
// But security group and subnets are stack-managed
|
|
497
|
+
expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
|
|
498
|
+
expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
});
|