@friggframework/devtools 2.0.0--canary.461.8cf93ae.0 → 2.0.0--canary.474.aa465e4.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/__tests__/unit/commands/deploy.test.js +0 -39
- package/frigg-cli/deploy-command/index.js +0 -5
- package/frigg-cli/index.js +0 -1
- 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,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudFormation-based Resource Discovery v2
|
|
3
|
+
*
|
|
4
|
+
* Refactored to return structured DiscoveryResult instead of flat object.
|
|
5
|
+
* Part of the clean resource ownership architecture.
|
|
6
|
+
*
|
|
7
|
+
* Domain Service - Hexagonal Architecture
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { createEmptyDiscoveryResult } = require('./types');
|
|
11
|
+
|
|
12
|
+
class CloudFormationDiscoveryV2 {
|
|
13
|
+
constructor(provider, config = {}) {
|
|
14
|
+
this.provider = provider;
|
|
15
|
+
this.serviceName = config.serviceName;
|
|
16
|
+
this.stage = config.stage;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Discover resources from an existing CloudFormation stack
|
|
21
|
+
*
|
|
22
|
+
* @param {string} stackName - Name of the CloudFormation stack
|
|
23
|
+
* @returns {Promise<Object>} DiscoveryResult or empty result if stack doesn't exist
|
|
24
|
+
*/
|
|
25
|
+
async discoverFromStack(stackName) {
|
|
26
|
+
try {
|
|
27
|
+
// Try to get the stack
|
|
28
|
+
const stack = await this.provider.describeStack(stackName);
|
|
29
|
+
|
|
30
|
+
// Get stack resources
|
|
31
|
+
const resources = await this.provider.listStackResources(stackName);
|
|
32
|
+
|
|
33
|
+
// Create structured discovery result
|
|
34
|
+
const discovery = createEmptyDiscoveryResult();
|
|
35
|
+
discovery.fromCloudFormation = true;
|
|
36
|
+
discovery.stackName = stackName;
|
|
37
|
+
discovery.region = this.provider.region;
|
|
38
|
+
|
|
39
|
+
// Extract stack-managed resources
|
|
40
|
+
await this._extractStackManagedResources(resources || [], discovery);
|
|
41
|
+
|
|
42
|
+
// Also keep flat structure for backwards compatibility (temporarily)
|
|
43
|
+
const flatDiscovered = {
|
|
44
|
+
fromCloudFormationStack: true,
|
|
45
|
+
stackName: stackName,
|
|
46
|
+
existingLogicalIds: discovery.stackManaged.map(r => r.logicalId)
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Extract from outputs (legacy)
|
|
50
|
+
if (stack.Outputs && stack.Outputs.length > 0) {
|
|
51
|
+
this._extractFromOutputs(stack.Outputs, flatDiscovered);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Extract flat properties from stackManaged resources
|
|
55
|
+
this._createFlatPropertiesFromStackManaged(discovery, flatDiscovered);
|
|
56
|
+
|
|
57
|
+
// Return both structures (flat for backwards compat, structured for new code)
|
|
58
|
+
return {
|
|
59
|
+
...flatDiscovered,
|
|
60
|
+
_structured: discovery // New structured format
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// Stack doesn't exist - return empty discovery
|
|
64
|
+
if (error.message && error.message.includes('does not exist')) {
|
|
65
|
+
const empty = createEmptyDiscoveryResult();
|
|
66
|
+
return {
|
|
67
|
+
fromCloudFormationStack: false,
|
|
68
|
+
_structured: empty
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Other errors - log and return empty
|
|
73
|
+
console.warn(`⚠️ CloudFormation discovery failed: ${error.message}`);
|
|
74
|
+
const empty = createEmptyDiscoveryResult();
|
|
75
|
+
return {
|
|
76
|
+
fromCloudFormationStack: false,
|
|
77
|
+
_structured: empty
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract stack-managed resources into structured format
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
async _extractStackManagedResources(resources, discovery) {
|
|
87
|
+
console.log(` DEBUG: Processing ${resources.length} CloudFormation resources...`);
|
|
88
|
+
|
|
89
|
+
for (const resource of resources) {
|
|
90
|
+
const { LogicalResourceId, PhysicalResourceId, ResourceType } = resource;
|
|
91
|
+
|
|
92
|
+
// Only track Frigg-managed resources
|
|
93
|
+
if (!LogicalResourceId.startsWith('Frigg') && !LogicalResourceId.includes('Migration')) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Add to stack-managed list
|
|
98
|
+
const stackResource = {
|
|
99
|
+
logicalId: LogicalResourceId,
|
|
100
|
+
physicalId: PhysicalResourceId,
|
|
101
|
+
resourceType: ResourceType,
|
|
102
|
+
properties: {} // Will be populated as needed
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
discovery.stackManaged.push(stackResource);
|
|
106
|
+
|
|
107
|
+
// Query AWS for detailed properties for certain resources
|
|
108
|
+
await this._enrichResourceProperties(stackResource, discovery);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(` ✓ Discovered ${discovery.stackManaged.length} stack-managed resources`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Enrich resource properties by querying AWS APIs
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
async _enrichResourceProperties(stackResource, discovery) {
|
|
119
|
+
const { logicalId, physicalId, resourceType } = stackResource;
|
|
120
|
+
|
|
121
|
+
// Security Group - query to get VPC ID
|
|
122
|
+
if (logicalId === 'FriggLambdaSecurityGroup' && resourceType === 'AWS::EC2::SecurityGroup') {
|
|
123
|
+
console.log(` ✓ Found security group in stack: ${physicalId}`);
|
|
124
|
+
|
|
125
|
+
if (this.provider && this.provider.getEC2Client) {
|
|
126
|
+
try {
|
|
127
|
+
console.log(` Querying EC2 to get VPC ID from security group...`);
|
|
128
|
+
const { DescribeSecurityGroupsCommand } = require('@aws-sdk/client-ec2');
|
|
129
|
+
const ec2Client = this.provider.getEC2Client();
|
|
130
|
+
const sgDetails = await ec2Client.send(
|
|
131
|
+
new DescribeSecurityGroupsCommand({
|
|
132
|
+
GroupIds: [physicalId]
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (sgDetails.SecurityGroups && sgDetails.SecurityGroups.length > 0) {
|
|
137
|
+
const sg = sgDetails.SecurityGroups[0];
|
|
138
|
+
stackResource.properties.VpcId = sg.VpcId;
|
|
139
|
+
console.log(` ✓ Extracted VPC ID from security group: ${sg.VpcId}`);
|
|
140
|
+
|
|
141
|
+
// Also add VPC to stack-managed if not already there
|
|
142
|
+
const vpcExists = discovery.stackManaged.some(r => r.logicalId === 'FriggVPC');
|
|
143
|
+
if (!vpcExists && sg.VpcId) {
|
|
144
|
+
// Note: VPC was created by stack, just not listed in resources yet
|
|
145
|
+
// We'll add it when we encounter it, or infer it here
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.warn(` ⚠️ Could not get VPC from security group: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Aurora Cluster - query to get endpoint
|
|
155
|
+
if (logicalId === 'FriggAuroraCluster' && resourceType === 'AWS::RDS::DBCluster') {
|
|
156
|
+
console.log(` ✓ Found Aurora cluster in stack: ${physicalId}`);
|
|
157
|
+
|
|
158
|
+
if (this.provider) {
|
|
159
|
+
try {
|
|
160
|
+
console.log(` Querying RDS to get Aurora endpoint...`);
|
|
161
|
+
const { DescribeDBClustersCommand, RDSClient } = require('@aws-sdk/client-rds');
|
|
162
|
+
|
|
163
|
+
const rdsClient = new RDSClient({ region: this.provider.region });
|
|
164
|
+
const clusterDetails = await rdsClient.send(
|
|
165
|
+
new DescribeDBClustersCommand({
|
|
166
|
+
DBClusterIdentifier: physicalId
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (clusterDetails.DBClusters && clusterDetails.DBClusters.length > 0) {
|
|
171
|
+
const cluster = clusterDetails.DBClusters[0];
|
|
172
|
+
stackResource.properties.Endpoint = cluster.Endpoint;
|
|
173
|
+
stackResource.properties.Port = cluster.Port;
|
|
174
|
+
stackResource.properties.DBClusterIdentifier = cluster.DBClusterIdentifier;
|
|
175
|
+
console.log(` ✓ Extracted Aurora endpoint: ${cluster.Endpoint}:${cluster.Port}`);
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.warn(` ⚠️ Could not get endpoint from Aurora cluster: ${error.message}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// KMS Key Alias - query to get ARN
|
|
184
|
+
if (logicalId === 'FriggKMSKeyAlias' && resourceType === 'AWS::KMS::Alias') {
|
|
185
|
+
console.log(` ✓ Found KMS key alias in stack: ${physicalId}`);
|
|
186
|
+
|
|
187
|
+
if (this.provider && this.provider.describeKmsKey) {
|
|
188
|
+
try {
|
|
189
|
+
console.log(` Querying KMS for alias: ${physicalId}...`);
|
|
190
|
+
const keyMetadata = await this.provider.describeKmsKey(physicalId);
|
|
191
|
+
|
|
192
|
+
if (keyMetadata) {
|
|
193
|
+
stackResource.properties.KeyArn = keyMetadata.Arn;
|
|
194
|
+
console.log(` ✓ Found KMS key via alias query: ${keyMetadata.Arn}`);
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.warn(` ⚠️ Could not get key ARN from alias: ${error.message}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// VPC - just log
|
|
203
|
+
if (logicalId === 'FriggVPC' && resourceType === 'AWS::EC2::VPC') {
|
|
204
|
+
console.log(` ✓ Found VPC in stack: ${physicalId}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create flat properties from stack-managed resources (backwards compatibility)
|
|
210
|
+
* @private
|
|
211
|
+
*/
|
|
212
|
+
_createFlatPropertiesFromStackManaged(discovery, flatDiscovered) {
|
|
213
|
+
for (const resource of discovery.stackManaged) {
|
|
214
|
+
const { logicalId, physicalId, properties } = resource;
|
|
215
|
+
|
|
216
|
+
// Map to flat property names
|
|
217
|
+
switch (logicalId) {
|
|
218
|
+
case 'FriggVPC':
|
|
219
|
+
flatDiscovered.defaultVpcId = physicalId;
|
|
220
|
+
break;
|
|
221
|
+
case 'FriggLambdaSecurityGroup':
|
|
222
|
+
flatDiscovered.securityGroupId = physicalId;
|
|
223
|
+
if (properties.VpcId) {
|
|
224
|
+
flatDiscovered.defaultVpcId = properties.VpcId;
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case 'FriggPrivateSubnet1':
|
|
228
|
+
flatDiscovered.privateSubnetId1 = physicalId;
|
|
229
|
+
break;
|
|
230
|
+
case 'FriggPrivateSubnet2':
|
|
231
|
+
flatDiscovered.privateSubnetId2 = physicalId;
|
|
232
|
+
break;
|
|
233
|
+
case 'FriggPublicSubnet':
|
|
234
|
+
flatDiscovered.publicSubnetId1 = physicalId;
|
|
235
|
+
break;
|
|
236
|
+
case 'FriggPublicSubnet2':
|
|
237
|
+
flatDiscovered.publicSubnetId2 = physicalId;
|
|
238
|
+
break;
|
|
239
|
+
case 'FriggNatGateway':
|
|
240
|
+
flatDiscovered.natGatewayId = physicalId;
|
|
241
|
+
break;
|
|
242
|
+
case 'FriggLambdaRouteTable':
|
|
243
|
+
flatDiscovered.routeTableId = physicalId;
|
|
244
|
+
break;
|
|
245
|
+
case 'FriggVPCEndpointSecurityGroup':
|
|
246
|
+
flatDiscovered.vpcEndpointSecurityGroupId = physicalId;
|
|
247
|
+
break;
|
|
248
|
+
case 'FriggS3VPCEndpoint':
|
|
249
|
+
flatDiscovered.s3VpcEndpointId = physicalId;
|
|
250
|
+
break;
|
|
251
|
+
case 'FriggDynamoDBVPCEndpoint':
|
|
252
|
+
flatDiscovered.dynamoDbVpcEndpointId = physicalId;
|
|
253
|
+
break;
|
|
254
|
+
case 'FriggKMSVPCEndpoint':
|
|
255
|
+
flatDiscovered.kmsVpcEndpointId = physicalId;
|
|
256
|
+
break;
|
|
257
|
+
case 'FriggSecretsManagerVPCEndpoint':
|
|
258
|
+
flatDiscovered.secretsManagerVpcEndpointId = physicalId;
|
|
259
|
+
break;
|
|
260
|
+
case 'FriggSQSVPCEndpoint':
|
|
261
|
+
flatDiscovered.sqsVpcEndpointId = physicalId;
|
|
262
|
+
break;
|
|
263
|
+
case 'FriggAuroraCluster':
|
|
264
|
+
flatDiscovered.auroraClusterId = physicalId;
|
|
265
|
+
if (properties.Endpoint) {
|
|
266
|
+
flatDiscovered.auroraClusterEndpoint = properties.Endpoint;
|
|
267
|
+
}
|
|
268
|
+
if (properties.Port) {
|
|
269
|
+
flatDiscovered.auroraClusterPort = properties.Port;
|
|
270
|
+
flatDiscovered.auroraPort = properties.Port;
|
|
271
|
+
}
|
|
272
|
+
if (properties.DBClusterIdentifier) {
|
|
273
|
+
flatDiscovered.auroraClusterIdentifier = properties.DBClusterIdentifier;
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
case 'FriggKMSKey':
|
|
277
|
+
flatDiscovered.defaultKmsKeyId = physicalId;
|
|
278
|
+
break;
|
|
279
|
+
case 'FriggKMSKeyAlias':
|
|
280
|
+
flatDiscovered.kmsKeyAlias = physicalId;
|
|
281
|
+
if (properties.KeyArn) {
|
|
282
|
+
flatDiscovered.defaultKmsKeyId = properties.KeyArn;
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
case 'FriggMigrationStatusBucket':
|
|
286
|
+
flatDiscovered.migrationStatusBucket = physicalId;
|
|
287
|
+
break;
|
|
288
|
+
case 'DbMigrationQueue':
|
|
289
|
+
flatDiscovered.migrationQueueUrl = physicalId;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Extract discovered resources from CloudFormation stack outputs (legacy)
|
|
297
|
+
* @private
|
|
298
|
+
*/
|
|
299
|
+
_extractFromOutputs(outputs, discovered) {
|
|
300
|
+
const outputMap = outputs.reduce((acc, output) => {
|
|
301
|
+
acc[output.OutputKey] = output.OutputValue;
|
|
302
|
+
return acc;
|
|
303
|
+
}, {});
|
|
304
|
+
|
|
305
|
+
// VPC outputs
|
|
306
|
+
if (outputMap.VpcId) {
|
|
307
|
+
discovered.defaultVpcId = outputMap.VpcId;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (outputMap.PrivateSubnetIds) {
|
|
311
|
+
discovered.privateSubnetIds = outputMap.PrivateSubnetIds.split(',').map(id => id.trim());
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (outputMap.PublicSubnetId) {
|
|
315
|
+
discovered.publicSubnetId = outputMap.PublicSubnetId;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (outputMap.SecurityGroupId) {
|
|
319
|
+
discovered.securityGroupId = outputMap.SecurityGroupId;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// KMS outputs
|
|
323
|
+
if (outputMap.KMS_KEY_ARN) {
|
|
324
|
+
discovered.defaultKmsKeyId = outputMap.KMS_KEY_ARN;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Database outputs
|
|
328
|
+
if (outputMap.DatabaseEndpoint) {
|
|
329
|
+
discovered.databaseEndpoint = outputMap.DatabaseEndpoint;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = CloudFormationDiscoveryV2;
|
|
@@ -48,6 +48,8 @@ describe('CloudFormationDiscovery', () => {
|
|
|
48
48
|
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
49
49
|
|
|
50
50
|
expect(result).toEqual({
|
|
51
|
+
fromCloudFormationStack: true,
|
|
52
|
+
stackName: 'test-stack',
|
|
51
53
|
defaultVpcId: 'vpc-123', // VpcBuilder expects 'defaultVpcId', not 'vpcId'
|
|
52
54
|
privateSubnetIds: ['subnet-1', 'subnet-2'],
|
|
53
55
|
publicSubnetId: 'subnet-3',
|
|
@@ -69,6 +71,8 @@ describe('CloudFormationDiscovery', () => {
|
|
|
69
71
|
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
70
72
|
|
|
71
73
|
expect(result).toEqual({
|
|
74
|
+
fromCloudFormationStack: true,
|
|
75
|
+
stackName: 'test-stack',
|
|
72
76
|
defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789:key/abc',
|
|
73
77
|
});
|
|
74
78
|
});
|
|
@@ -137,7 +141,10 @@ describe('CloudFormationDiscovery', () => {
|
|
|
137
141
|
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
138
142
|
|
|
139
143
|
expect(result).toEqual({
|
|
144
|
+
fromCloudFormationStack: true,
|
|
145
|
+
stackName: 'test-stack',
|
|
140
146
|
auroraClusterId: 'test-cluster',
|
|
147
|
+
existingLogicalIds: ['FriggAuroraCluster'],
|
|
141
148
|
});
|
|
142
149
|
});
|
|
143
150
|
|
|
@@ -201,7 +208,10 @@ describe('CloudFormationDiscovery', () => {
|
|
|
201
208
|
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
202
209
|
|
|
203
210
|
expect(result).toEqual({
|
|
211
|
+
fromCloudFormationStack: true,
|
|
212
|
+
stackName: 'test-stack',
|
|
204
213
|
migrationStatusBucket: 'test-migration-bucket',
|
|
214
|
+
existingLogicalIds: ['FriggMigrationStatusBucket'],
|
|
205
215
|
});
|
|
206
216
|
});
|
|
207
217
|
|
|
@@ -225,7 +235,10 @@ describe('CloudFormationDiscovery', () => {
|
|
|
225
235
|
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
226
236
|
|
|
227
237
|
expect(result).toEqual({
|
|
238
|
+
fromCloudFormationStack: true,
|
|
239
|
+
stackName: 'test-stack',
|
|
228
240
|
migrationQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789/test-queue',
|
|
241
|
+
existingLogicalIds: ['DbMigrationQueue'],
|
|
229
242
|
});
|
|
230
243
|
});
|
|
231
244
|
|
|
@@ -249,7 +262,10 @@ describe('CloudFormationDiscovery', () => {
|
|
|
249
262
|
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
250
263
|
|
|
251
264
|
expect(result).toEqual({
|
|
265
|
+
fromCloudFormationStack: true,
|
|
266
|
+
stackName: 'test-stack',
|
|
252
267
|
natGatewayId: 'nat-0123456789',
|
|
268
|
+
existingLogicalIds: ['FriggNatGateway'],
|
|
253
269
|
});
|
|
254
270
|
});
|
|
255
271
|
|
|
@@ -273,7 +289,10 @@ describe('CloudFormationDiscovery', () => {
|
|
|
273
289
|
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
274
290
|
|
|
275
291
|
expect(result).toEqual({
|
|
292
|
+
fromCloudFormationStack: true,
|
|
293
|
+
stackName: 'test-stack',
|
|
276
294
|
defaultVpcId: 'vpc-037ec55fe87aec1e7',
|
|
295
|
+
existingLogicalIds: ['FriggVPC'],
|
|
277
296
|
});
|
|
278
297
|
});
|
|
279
298
|
|
|
@@ -305,10 +324,13 @@ describe('CloudFormationDiscovery', () => {
|
|
|
305
324
|
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
306
325
|
|
|
307
326
|
expect(result).toEqual({
|
|
327
|
+
fromCloudFormationStack: true,
|
|
328
|
+
stackName: 'test-stack',
|
|
308
329
|
defaultVpcId: 'vpc-123', // VpcBuilder expects 'defaultVpcId'
|
|
309
330
|
defaultKmsKeyId: 'arn:aws:kms:us-east-1:123456789:key/abc',
|
|
310
331
|
auroraClusterId: 'test-cluster',
|
|
311
332
|
natGatewayId: 'nat-123',
|
|
333
|
+
existingLogicalIds: ['FriggAuroraCluster', 'FriggNatGateway'],
|
|
312
334
|
});
|
|
313
335
|
});
|
|
314
336
|
|
|
@@ -329,7 +351,10 @@ describe('CloudFormationDiscovery', () => {
|
|
|
329
351
|
|
|
330
352
|
const result = await cfDiscovery.discoverFromStack('test-stack');
|
|
331
353
|
|
|
332
|
-
expect(result).toEqual({
|
|
354
|
+
expect(result).toEqual({
|
|
355
|
+
fromCloudFormationStack: true,
|
|
356
|
+
stackName: 'test-stack',
|
|
357
|
+
});
|
|
333
358
|
});
|
|
334
359
|
|
|
335
360
|
it('should query EC2 for subnets when VPC found but no subnet resources in stack', async () => {
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview App definition types
|
|
3
|
+
*
|
|
4
|
+
* Defines the structure of the application definition with new ownership-based schema.
|
|
5
|
+
* This replaces the old 'management' mode system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* VPC configuration
|
|
10
|
+
* @typedef {Object} VpcDefinition
|
|
11
|
+
* @property {boolean} enable - Whether VPC is enabled
|
|
12
|
+
*
|
|
13
|
+
* @property {Object} [ownership] - Resource ownership configuration
|
|
14
|
+
* @property {'stack'|'external'|'auto'} [ownership.vpc] - VPC ownership
|
|
15
|
+
* @property {'stack'|'external'|'auto'} [ownership.securityGroup] - Security group ownership
|
|
16
|
+
* @property {'stack'|'external'|'auto'} [ownership.subnets] - Subnets ownership
|
|
17
|
+
* @property {'stack'|'external'|'auto'} [ownership.natGateway] - NAT gateway ownership
|
|
18
|
+
* @property {'stack'|'external'|'auto'} [ownership.vpcEndpoints] - VPC endpoints ownership
|
|
19
|
+
*
|
|
20
|
+
* @property {Object} [external] - External resource references (required if ownership='external')
|
|
21
|
+
* @property {string} [external.vpcId] - External VPC ID
|
|
22
|
+
* @property {string[]} [external.securityGroupIds] - External security group IDs
|
|
23
|
+
* @property {string[]} [external.subnetIds] - External subnet IDs
|
|
24
|
+
* @property {string} [external.natGatewayId] - External NAT gateway ID
|
|
25
|
+
* @property {Object} [external.vpcEndpointIds] - External VPC endpoint IDs
|
|
26
|
+
* @property {string} [external.vpcEndpointIds.s3] - S3 endpoint ID
|
|
27
|
+
* @property {string} [external.vpcEndpointIds.dynamodb] - DynamoDB endpoint ID
|
|
28
|
+
* @property {string} [external.vpcEndpointIds.kms] - KMS endpoint ID
|
|
29
|
+
* @property {string} [external.vpcEndpointIds.secretsManager] - Secrets Manager endpoint ID
|
|
30
|
+
* @property {string} [external.vpcEndpointIds.sqs] - SQS endpoint ID
|
|
31
|
+
*
|
|
32
|
+
* @property {Object} [config] - Configuration preferences
|
|
33
|
+
* @property {boolean} [config.selfHeal] - Auto-configure NAT/routes/etc (default: false)
|
|
34
|
+
* @property {string} [config.cidrBlock] - CIDR block for stack-owned VPC (default: '10.0.0.0/16')
|
|
35
|
+
* @property {boolean} [config.enableVpcEndpoints] - Enable VPC endpoints (default: true)
|
|
36
|
+
* @property {Object} [config.natGateway] - NAT Gateway configuration
|
|
37
|
+
* @property {boolean} [config.natGateway.enable] - Enable NAT Gateway (default: true)
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Aurora PostgreSQL configuration
|
|
42
|
+
* @typedef {Object} AuroraDefinition
|
|
43
|
+
* @property {boolean} enable - Whether Aurora is enabled
|
|
44
|
+
*
|
|
45
|
+
* @property {Object} [ownership] - Resource ownership configuration
|
|
46
|
+
* @property {'stack'|'external'|'auto'} [ownership.cluster] - Aurora cluster ownership
|
|
47
|
+
* @property {'stack'|'external'|'auto'} [ownership.subnetGroup] - DB subnet group ownership
|
|
48
|
+
* @property {'stack'|'external'|'auto'} [ownership.secret] - Secrets Manager secret ownership
|
|
49
|
+
*
|
|
50
|
+
* @property {Object} [external] - External resource references
|
|
51
|
+
* @property {string} [external.clusterId] - External cluster identifier
|
|
52
|
+
* @property {string} [external.clusterEndpoint] - External cluster endpoint
|
|
53
|
+
* @property {number} [external.port] - External cluster port
|
|
54
|
+
* @property {string} [external.secretArn] - External Secrets Manager ARN
|
|
55
|
+
*
|
|
56
|
+
* @property {Object} [config] - Configuration preferences
|
|
57
|
+
* @property {'aurora-postgresql'|'aurora-mysql'} [config.engine] - Database engine
|
|
58
|
+
* @property {number} [config.minCapacity] - Min serverless capacity (default: 0.5)
|
|
59
|
+
* @property {number} [config.maxCapacity] - Max serverless capacity (default: 1)
|
|
60
|
+
* @property {string} [config.database] - Database name (default: 'frigg')
|
|
61
|
+
* @property {boolean} [config.publiclyAccessible] - Public access (default: false)
|
|
62
|
+
* @property {boolean} [config.autoCreateCredentials] - Auto-create credentials in Secrets Manager
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* KMS encryption configuration
|
|
67
|
+
* @typedef {Object} KmsDefinition
|
|
68
|
+
* @property {boolean} enable - Whether KMS is enabled
|
|
69
|
+
*
|
|
70
|
+
* @property {Object} [ownership] - Resource ownership configuration
|
|
71
|
+
* @property {'stack'|'external'|'auto'} [ownership.key] - KMS key ownership
|
|
72
|
+
*
|
|
73
|
+
* @property {Object} [external] - External resource references
|
|
74
|
+
* @property {string} [external.keyId] - External KMS key ID or ARN
|
|
75
|
+
* @property {string} [external.keyAlias] - External KMS key alias
|
|
76
|
+
*
|
|
77
|
+
* @property {Object} [config] - Configuration preferences
|
|
78
|
+
* @property {boolean} [config.enableKeyRotation] - Enable automatic key rotation
|
|
79
|
+
* @property {string} [config.description] - Key description
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* SSM Parameter Store configuration
|
|
84
|
+
* @typedef {Object} SsmDefinition
|
|
85
|
+
* @property {boolean} enable - Whether SSM is enabled
|
|
86
|
+
* @property {string[]} [parameterPaths] - Parameter paths to grant access to
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Database migration configuration
|
|
91
|
+
* @typedef {Object} MigrationDefinition
|
|
92
|
+
* @property {boolean} enable - Whether migrations are enabled
|
|
93
|
+
* @property {string} [migrationPath] - Path to migration files
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* WebSocket configuration
|
|
98
|
+
* @typedef {Object} WebsocketDefinition
|
|
99
|
+
* @property {boolean} enable - Whether WebSocket API is enabled
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Integration configuration
|
|
104
|
+
* @typedef {Object} IntegrationDefinition
|
|
105
|
+
* @property {Object} Definition - Integration definition object
|
|
106
|
+
* @property {string} Definition.name - Integration name
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Complete application definition
|
|
111
|
+
* @typedef {Object} AppDefinition
|
|
112
|
+
* @property {string} name - Application name
|
|
113
|
+
* @property {string} stage - Deployment stage (e.g., 'dev', 'production')
|
|
114
|
+
* @property {string} provider - Cloud provider (e.g., 'aws')
|
|
115
|
+
* @property {string} region - AWS region
|
|
116
|
+
*
|
|
117
|
+
* @property {VpcDefinition} [vpc] - VPC configuration
|
|
118
|
+
* @property {Object} [database] - Database configuration
|
|
119
|
+
* @property {AuroraDefinition} [database.postgres] - PostgreSQL configuration
|
|
120
|
+
* @property {KmsDefinition} [encryption] - KMS encryption configuration
|
|
121
|
+
* @property {SsmDefinition} [ssm] - SSM Parameter Store configuration
|
|
122
|
+
* @property {MigrationDefinition} [migrations] - Database migration configuration
|
|
123
|
+
* @property {WebsocketDefinition} [websockets] - WebSocket API configuration
|
|
124
|
+
* @property {IntegrationDefinition[]} [integrations] - Integration definitions
|
|
125
|
+
*
|
|
126
|
+
* @property {Object} [environment] - Environment variables
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Validate app definition has required fields
|
|
131
|
+
* @param {AppDefinition} appDefinition - App definition to validate
|
|
132
|
+
* @throws {Error} If required fields are missing
|
|
133
|
+
*/
|
|
134
|
+
function validateAppDefinition(appDefinition) {
|
|
135
|
+
if (!appDefinition) {
|
|
136
|
+
throw new Error('App definition is required');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!appDefinition.name) {
|
|
140
|
+
throw new Error('App definition must have a name');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!appDefinition.provider) {
|
|
144
|
+
throw new Error('App definition must have a provider');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!appDefinition.region) {
|
|
148
|
+
throw new Error('App definition must have a region');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get stack name from app definition
|
|
154
|
+
* @param {AppDefinition} appDefinition - App definition
|
|
155
|
+
* @returns {string} Stack name
|
|
156
|
+
*/
|
|
157
|
+
function getStackName(appDefinition) {
|
|
158
|
+
const stage = appDefinition.stage || 'dev';
|
|
159
|
+
return `${appDefinition.name}-${stage}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if VPC is enabled
|
|
164
|
+
* @param {AppDefinition} appDefinition - App definition
|
|
165
|
+
* @returns {boolean}
|
|
166
|
+
*/
|
|
167
|
+
function isVpcEnabled(appDefinition) {
|
|
168
|
+
return appDefinition.vpc?.enable === true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check if Aurora is enabled
|
|
173
|
+
* @param {AppDefinition} appDefinition - App definition
|
|
174
|
+
* @returns {boolean}
|
|
175
|
+
*/
|
|
176
|
+
function isAuroraEnabled(appDefinition) {
|
|
177
|
+
return appDefinition.database?.postgres?.enable === true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if KMS is enabled
|
|
182
|
+
* @param {AppDefinition} appDefinition - App definition
|
|
183
|
+
* @returns {boolean}
|
|
184
|
+
*/
|
|
185
|
+
function isKmsEnabled(appDefinition) {
|
|
186
|
+
return appDefinition.encryption?.enable === true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if SSM is enabled
|
|
191
|
+
* @param {AppDefinition} appDefinition - App definition
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
function isSsmEnabled(appDefinition) {
|
|
195
|
+
return appDefinition.ssm?.enable === true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
validateAppDefinition,
|
|
200
|
+
getStackName,
|
|
201
|
+
isVpcEnabled,
|
|
202
|
+
isAuroraEnabled,
|
|
203
|
+
isKmsEnabled,
|
|
204
|
+
isSsmEnabled
|
|
205
|
+
};
|