@friggframework/devtools 2.0.0--canary.397.155fecd.0 → 2.0.0--canary.398.e2147f7.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/AWS-IAM-CREDENTIAL-NEEDS.md +541 -0
- package/infrastructure/README-TESTING.md +332 -0
- package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
- package/infrastructure/__tests__/helpers/test-utils.js +277 -0
- package/infrastructure/aws-discovery.js +395 -0
- package/infrastructure/aws-discovery.test.js +373 -0
- package/infrastructure/build-time-discovery.js +197 -0
- package/infrastructure/build-time-discovery.test.js +375 -0
- package/infrastructure/create-frigg-infrastructure.js +9 -1
- package/infrastructure/integration.test.js +383 -0
- package/infrastructure/serverless-template.js +510 -6
- package/infrastructure/serverless-template.test.js +498 -0
- package/package.json +5 -5
- package/test/mock-integration.js +14 -4
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test utilities for VPC/KMS/SSM testing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { mockEnvironmentVariables, mockFallbackEnvironmentVariables } = require('../fixtures/mock-aws-resources');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Set up environment variables for testing
|
|
9
|
+
* @param {Object} envVars - Environment variables to set
|
|
10
|
+
*/
|
|
11
|
+
function setTestEnvironmentVariables(envVars = mockEnvironmentVariables) {
|
|
12
|
+
Object.keys(envVars).forEach(key => {
|
|
13
|
+
process.env[key] = envVars[key];
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Clean up environment variables after testing
|
|
19
|
+
* @param {Object} envVars - Environment variables to clean up
|
|
20
|
+
*/
|
|
21
|
+
function cleanupTestEnvironmentVariables(envVars = mockEnvironmentVariables) {
|
|
22
|
+
Object.keys(envVars).forEach(key => {
|
|
23
|
+
delete process.env[key];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set up fallback environment variables for error testing
|
|
29
|
+
*/
|
|
30
|
+
function setFallbackEnvironmentVariables() {
|
|
31
|
+
setTestEnvironmentVariables(mockFallbackEnvironmentVariables);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a mock AWS SDK client send function
|
|
36
|
+
* @param {Array} responses - Array of responses to return in order
|
|
37
|
+
* @returns {Function} Mock send function
|
|
38
|
+
*/
|
|
39
|
+
function createMockSendFunction(responses) {
|
|
40
|
+
let callCount = 0;
|
|
41
|
+
return jest.fn().mockImplementation(() => {
|
|
42
|
+
const response = responses[callCount] || responses[responses.length - 1];
|
|
43
|
+
callCount++;
|
|
44
|
+
return Promise.resolve(response);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a mock serverless object for plugin testing
|
|
50
|
+
* @param {Object} serviceConfig - Serverless service configuration
|
|
51
|
+
* @param {Array} commands - Commands in processedInput
|
|
52
|
+
* @returns {Object} Mock serverless object
|
|
53
|
+
*/
|
|
54
|
+
function createMockServerless(serviceConfig = {}, commands = []) {
|
|
55
|
+
return {
|
|
56
|
+
cli: {
|
|
57
|
+
log: jest.fn()
|
|
58
|
+
},
|
|
59
|
+
service: {
|
|
60
|
+
provider: {
|
|
61
|
+
name: 'aws',
|
|
62
|
+
region: 'us-east-1',
|
|
63
|
+
...serviceConfig.provider
|
|
64
|
+
},
|
|
65
|
+
plugins: serviceConfig.plugins || [],
|
|
66
|
+
custom: serviceConfig.custom || {},
|
|
67
|
+
functions: serviceConfig.functions || {},
|
|
68
|
+
...serviceConfig
|
|
69
|
+
},
|
|
70
|
+
processedInput: {
|
|
71
|
+
commands: commands
|
|
72
|
+
},
|
|
73
|
+
getProvider: jest.fn(() => ({})),
|
|
74
|
+
extendConfiguration: jest.fn()
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Verify that environment variables are set correctly
|
|
80
|
+
* @param {Object} expectedVars - Expected environment variables
|
|
81
|
+
*/
|
|
82
|
+
function verifyEnvironmentVariables(expectedVars) {
|
|
83
|
+
Object.keys(expectedVars).forEach(key => {
|
|
84
|
+
expect(process.env[key]).toBe(expectedVars[key]);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a mock integration definition
|
|
90
|
+
* @param {string} name - Integration name
|
|
91
|
+
* @returns {Object} Mock integration
|
|
92
|
+
*/
|
|
93
|
+
function createMockIntegration(name = 'testIntegration') {
|
|
94
|
+
return {
|
|
95
|
+
Definition: {
|
|
96
|
+
name: name
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a mock app definition with specified features
|
|
103
|
+
* @param {Object} features - Features to enable (vpc, kms, ssm)
|
|
104
|
+
* @param {Array} integrations - Integration definitions
|
|
105
|
+
* @returns {Object} Mock app definition
|
|
106
|
+
*/
|
|
107
|
+
function createMockAppDefinition(features = {}, integrations = []) {
|
|
108
|
+
const appDefinition = {
|
|
109
|
+
name: 'test-app',
|
|
110
|
+
integrations: integrations
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (features.vpc) {
|
|
114
|
+
appDefinition.vpc = { enable: true };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (features.kms) {
|
|
118
|
+
appDefinition.encryption = { useDefaultKMSForFieldLevelEncryption: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (features.ssm) {
|
|
122
|
+
appDefinition.ssm = { enable: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return appDefinition;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Verify serverless configuration contains expected VPC settings
|
|
130
|
+
* @param {Object} config - Serverless configuration
|
|
131
|
+
*/
|
|
132
|
+
function verifyVpcConfiguration(config) {
|
|
133
|
+
expect(config.provider.vpc).toBe('${self:custom.vpc.${self:provider.stage}}');
|
|
134
|
+
expect(config.custom.vpc).toEqual({
|
|
135
|
+
'${self:provider.stage}': {
|
|
136
|
+
securityGroupIds: ['${env:AWS_DISCOVERY_SECURITY_GROUP_ID}'],
|
|
137
|
+
subnetIds: [
|
|
138
|
+
'${env:AWS_DISCOVERY_SUBNET_ID_1}',
|
|
139
|
+
'${env:AWS_DISCOVERY_SUBNET_ID_2}'
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
expect(config.resources.Resources.VPCEndpointS3).toBeDefined();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Verify serverless configuration contains expected KMS settings
|
|
148
|
+
* @param {Object} config - Serverless configuration
|
|
149
|
+
*/
|
|
150
|
+
function verifyKmsConfiguration(config) {
|
|
151
|
+
expect(config.plugins).toContain('serverless-kms-grants');
|
|
152
|
+
expect(config.provider.environment.KMS_KEY_ARN).toBe('${self:custom.kmsGrants.kmsKeyId}');
|
|
153
|
+
expect(config.custom.kmsGrants).toEqual({
|
|
154
|
+
kmsKeyId: '${env:AWS_DISCOVERY_KMS_KEY_ID}'
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Verify KMS IAM permissions
|
|
158
|
+
const kmsPermission = config.provider.iamRoleStatements.find(
|
|
159
|
+
statement => statement.Action.includes('kms:GenerateDataKey')
|
|
160
|
+
);
|
|
161
|
+
expect(kmsPermission).toBeDefined();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Verify serverless configuration contains expected SSM settings
|
|
166
|
+
* @param {Object} config - Serverless configuration
|
|
167
|
+
*/
|
|
168
|
+
function verifySsmConfiguration(config) {
|
|
169
|
+
expect(config.provider.layers).toEqual([
|
|
170
|
+
'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11'
|
|
171
|
+
]);
|
|
172
|
+
expect(config.provider.environment.SSM_PARAMETER_PREFIX).toBe('/${self:service}/${self:provider.stage}');
|
|
173
|
+
|
|
174
|
+
// Verify SSM IAM permissions
|
|
175
|
+
const ssmPermission = config.provider.iamRoleStatements.find(
|
|
176
|
+
statement => statement.Action.includes('ssm:GetParameter')
|
|
177
|
+
);
|
|
178
|
+
expect(ssmPermission).toBeDefined();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Verify integration-specific resources are created
|
|
183
|
+
* @param {Object} config - Serverless configuration
|
|
184
|
+
* @param {string} integrationName - Name of the integration
|
|
185
|
+
*/
|
|
186
|
+
function verifyIntegrationConfiguration(config, integrationName) {
|
|
187
|
+
const capitalizedName = integrationName.charAt(0).toUpperCase() + integrationName.slice(1);
|
|
188
|
+
|
|
189
|
+
// Verify integration function
|
|
190
|
+
expect(config.functions[integrationName]).toBeDefined();
|
|
191
|
+
|
|
192
|
+
// Verify queue worker function
|
|
193
|
+
expect(config.functions[`${integrationName}QueueWorker`]).toBeDefined();
|
|
194
|
+
|
|
195
|
+
// Verify SQS queue resource
|
|
196
|
+
expect(config.resources.Resources[`${capitalizedName}Queue`]).toBeDefined();
|
|
197
|
+
|
|
198
|
+
// Verify environment variable
|
|
199
|
+
expect(config.provider.environment[`${integrationName.toUpperCase()}_QUEUE_URL`]).toBeDefined();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Wait for async operations to complete
|
|
204
|
+
* @param {number} ms - Milliseconds to wait
|
|
205
|
+
*/
|
|
206
|
+
function wait(ms = 0) {
|
|
207
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Capture console output for testing
|
|
212
|
+
*/
|
|
213
|
+
function captureConsoleOutput() {
|
|
214
|
+
const originalLog = console.log;
|
|
215
|
+
const originalError = console.error;
|
|
216
|
+
const originalWarn = console.warn;
|
|
217
|
+
|
|
218
|
+
const logs = [];
|
|
219
|
+
const errors = [];
|
|
220
|
+
const warnings = [];
|
|
221
|
+
|
|
222
|
+
console.log = (...args) => {
|
|
223
|
+
logs.push(args.join(' '));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
console.error = (...args) => {
|
|
227
|
+
errors.push(args.join(' '));
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
console.warn = (...args) => {
|
|
231
|
+
warnings.push(args.join(' '));
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
logs,
|
|
236
|
+
errors,
|
|
237
|
+
warnings,
|
|
238
|
+
restore: () => {
|
|
239
|
+
console.log = originalLog;
|
|
240
|
+
console.error = originalError;
|
|
241
|
+
console.warn = originalWarn;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Mock process.argv for testing
|
|
248
|
+
* @param {Array} argv - Arguments to set
|
|
249
|
+
*/
|
|
250
|
+
function mockProcessArgv(argv = ['node', 'test']) {
|
|
251
|
+
const originalArgv = process.argv;
|
|
252
|
+
jest.spyOn(process, 'argv', 'get').mockReturnValue(argv);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
restore: () => {
|
|
256
|
+
process.argv = originalArgv;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
setTestEnvironmentVariables,
|
|
263
|
+
cleanupTestEnvironmentVariables,
|
|
264
|
+
setFallbackEnvironmentVariables,
|
|
265
|
+
createMockSendFunction,
|
|
266
|
+
createMockServerless,
|
|
267
|
+
verifyEnvironmentVariables,
|
|
268
|
+
createMockIntegration,
|
|
269
|
+
createMockAppDefinition,
|
|
270
|
+
verifyVpcConfiguration,
|
|
271
|
+
verifyKmsConfiguration,
|
|
272
|
+
verifySsmConfiguration,
|
|
273
|
+
verifyIntegrationConfiguration,
|
|
274
|
+
wait,
|
|
275
|
+
captureConsoleOutput,
|
|
276
|
+
mockProcessArgv
|
|
277
|
+
};
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
const { EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, DescribeRouteTablesCommand } = require('@aws-sdk/client-ec2');
|
|
2
|
+
const { KMSClient, ListKeysCommand, DescribeKeyCommand } = require('@aws-sdk/client-kms');
|
|
3
|
+
const { STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AWS Resource Discovery utilities for Frigg applications
|
|
7
|
+
* These functions use AWS credentials to discover default resources during build time
|
|
8
|
+
*/
|
|
9
|
+
class AWSDiscovery {
|
|
10
|
+
/**
|
|
11
|
+
* Creates an instance of AWSDiscovery
|
|
12
|
+
* @param {string} [region='us-east-1'] - AWS region to use for discovery
|
|
13
|
+
*/
|
|
14
|
+
constructor(region = 'us-east-1') {
|
|
15
|
+
this.region = region;
|
|
16
|
+
this.ec2Client = new EC2Client({ region });
|
|
17
|
+
this.kmsClient = new KMSClient({ region });
|
|
18
|
+
this.stsClient = new STSClient({ region });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get AWS account ID
|
|
23
|
+
* @returns {Promise<string>} The AWS account ID
|
|
24
|
+
* @throws {Error} If unable to retrieve account ID
|
|
25
|
+
*/
|
|
26
|
+
async getAccountId() {
|
|
27
|
+
try {
|
|
28
|
+
const command = new GetCallerIdentityCommand({});
|
|
29
|
+
const response = await this.stsClient.send(command);
|
|
30
|
+
return response.Account;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Error getting AWS account ID:', error);
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find the default VPC for the account
|
|
39
|
+
* @returns {Promise<Object>} VPC object containing VpcId and other properties
|
|
40
|
+
* @throws {Error} If no VPC is found in the account
|
|
41
|
+
*/
|
|
42
|
+
async findDefaultVpc() {
|
|
43
|
+
try {
|
|
44
|
+
const command = new DescribeVpcsCommand({
|
|
45
|
+
Filters: [
|
|
46
|
+
{
|
|
47
|
+
Name: 'is-default',
|
|
48
|
+
Values: ['true']
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const response = await this.ec2Client.send(command);
|
|
54
|
+
|
|
55
|
+
if (response.Vpcs && response.Vpcs.length > 0) {
|
|
56
|
+
return response.Vpcs[0];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If no default VPC, get the first available VPC
|
|
60
|
+
const allVpcsCommand = new DescribeVpcsCommand({});
|
|
61
|
+
const allVpcsResponse = await this.ec2Client.send(allVpcsCommand);
|
|
62
|
+
|
|
63
|
+
if (allVpcsResponse.Vpcs && allVpcsResponse.Vpcs.length > 0) {
|
|
64
|
+
console.log('No default VPC found, using first available VPC');
|
|
65
|
+
return allVpcsResponse.Vpcs[0];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw new Error('No VPC found in the account');
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Error finding default VPC:', error);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Find private subnets for the given VPC
|
|
77
|
+
* @param {string} vpcId - The VPC ID to search within
|
|
78
|
+
* @returns {Promise<Array>} Array of subnet objects (at least 2 for high availability)
|
|
79
|
+
* @throws {Error} If no subnets are found in the VPC
|
|
80
|
+
*/
|
|
81
|
+
async findPrivateSubnets(vpcId) {
|
|
82
|
+
try {
|
|
83
|
+
const command = new DescribeSubnetsCommand({
|
|
84
|
+
Filters: [
|
|
85
|
+
{
|
|
86
|
+
Name: 'vpc-id',
|
|
87
|
+
Values: [vpcId]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const response = await this.ec2Client.send(command);
|
|
93
|
+
|
|
94
|
+
if (!response.Subnets || response.Subnets.length === 0) {
|
|
95
|
+
throw new Error(`No subnets found in VPC ${vpcId}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Prefer private subnets (no direct route to IGW)
|
|
99
|
+
const privateSubnets = [];
|
|
100
|
+
const publicSubnets = [];
|
|
101
|
+
|
|
102
|
+
for (const subnet of response.Subnets) {
|
|
103
|
+
// Check route tables to determine if subnet is private
|
|
104
|
+
const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
|
|
105
|
+
if (isPrivate) {
|
|
106
|
+
privateSubnets.push(subnet);
|
|
107
|
+
} else {
|
|
108
|
+
publicSubnets.push(subnet);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Return at least 2 subnets for high availability
|
|
113
|
+
const selectedSubnets = privateSubnets.length >= 2 ?
|
|
114
|
+
privateSubnets.slice(0, 2) :
|
|
115
|
+
response.Subnets.slice(0, 2);
|
|
116
|
+
|
|
117
|
+
return selectedSubnets;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('Error finding private subnets:', error);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if a subnet is private (no direct route to Internet Gateway)
|
|
126
|
+
* @param {string} subnetId - The subnet ID to check
|
|
127
|
+
* @returns {Promise<boolean>} True if subnet is private, false if public
|
|
128
|
+
*/
|
|
129
|
+
async isSubnetPrivate(subnetId) {
|
|
130
|
+
try {
|
|
131
|
+
const command = new DescribeRouteTablesCommand({
|
|
132
|
+
Filters: [
|
|
133
|
+
{
|
|
134
|
+
Name: 'association.subnet-id',
|
|
135
|
+
Values: [subnetId]
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const response = await this.ec2Client.send(command);
|
|
141
|
+
|
|
142
|
+
for (const routeTable of response.RouteTables || []) {
|
|
143
|
+
for (const route of routeTable.Routes || []) {
|
|
144
|
+
// If there's a route to an Internet Gateway, it's a public subnet
|
|
145
|
+
if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return true; // No IGW route found, assume private
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.warn(`Could not determine if subnet ${subnetId} is private:`, error);
|
|
154
|
+
return true; // Default to private for safety
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Find or create a default security group for Lambda functions
|
|
160
|
+
* @param {string} vpcId - The VPC ID to search within
|
|
161
|
+
* @returns {Promise<Object>} Security group object containing GroupId and other properties
|
|
162
|
+
* @throws {Error} If no security group is found for the VPC
|
|
163
|
+
*/
|
|
164
|
+
async findDefaultSecurityGroup(vpcId) {
|
|
165
|
+
try {
|
|
166
|
+
// First try to find existing Frigg security group
|
|
167
|
+
const friggSgCommand = new DescribeSecurityGroupsCommand({
|
|
168
|
+
Filters: [
|
|
169
|
+
{
|
|
170
|
+
Name: 'vpc-id',
|
|
171
|
+
Values: [vpcId]
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
Name: 'group-name',
|
|
175
|
+
Values: ['frigg-lambda-sg']
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const friggResponse = await this.ec2Client.send(friggSgCommand);
|
|
181
|
+
if (friggResponse.SecurityGroups && friggResponse.SecurityGroups.length > 0) {
|
|
182
|
+
return friggResponse.SecurityGroups[0];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Fall back to default security group
|
|
186
|
+
const defaultSgCommand = new DescribeSecurityGroupsCommand({
|
|
187
|
+
Filters: [
|
|
188
|
+
{
|
|
189
|
+
Name: 'vpc-id',
|
|
190
|
+
Values: [vpcId]
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
Name: 'group-name',
|
|
194
|
+
Values: ['default']
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const defaultResponse = await this.ec2Client.send(defaultSgCommand);
|
|
200
|
+
if (defaultResponse.SecurityGroups && defaultResponse.SecurityGroups.length > 0) {
|
|
201
|
+
return defaultResponse.SecurityGroups[0];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
throw new Error(`No security group found for VPC ${vpcId}`);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('Error finding default security group:', error);
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Find public subnets for NAT Gateway placement
|
|
213
|
+
* @param {string} vpcId - The VPC ID to search within
|
|
214
|
+
* @returns {Promise<Object>} First public subnet object for NAT Gateway placement
|
|
215
|
+
* @throws {Error} If no public subnets are found in the VPC
|
|
216
|
+
*/
|
|
217
|
+
async findPublicSubnets(vpcId) {
|
|
218
|
+
try {
|
|
219
|
+
const command = new DescribeSubnetsCommand({
|
|
220
|
+
Filters: [
|
|
221
|
+
{
|
|
222
|
+
Name: 'vpc-id',
|
|
223
|
+
Values: [vpcId]
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const response = await this.ec2Client.send(command);
|
|
229
|
+
|
|
230
|
+
if (!response.Subnets || response.Subnets.length === 0) {
|
|
231
|
+
throw new Error(`No subnets found in VPC ${vpcId}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Find public subnets (have direct route to IGW)
|
|
235
|
+
const publicSubnets = [];
|
|
236
|
+
|
|
237
|
+
for (const subnet of response.Subnets) {
|
|
238
|
+
// Check route tables to determine if subnet is public
|
|
239
|
+
const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
|
|
240
|
+
if (!isPrivate) {
|
|
241
|
+
publicSubnets.push(subnet);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (publicSubnets.length === 0) {
|
|
246
|
+
throw new Error(`No public subnets found in VPC ${vpcId} for NAT Gateway placement`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Return first public subnet for NAT Gateway
|
|
250
|
+
return publicSubnets[0];
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error('Error finding public subnets:', error);
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Find private route table for VPC endpoints
|
|
259
|
+
* @param {string} vpcId - The VPC ID to search within
|
|
260
|
+
* @returns {Promise<Object>} Route table object containing RouteTableId and other properties
|
|
261
|
+
* @throws {Error} If no route tables are found for the VPC
|
|
262
|
+
*/
|
|
263
|
+
async findPrivateRouteTable(vpcId) {
|
|
264
|
+
try {
|
|
265
|
+
const command = new DescribeRouteTablesCommand({
|
|
266
|
+
Filters: [
|
|
267
|
+
{
|
|
268
|
+
Name: 'vpc-id',
|
|
269
|
+
Values: [vpcId]
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const response = await this.ec2Client.send(command);
|
|
275
|
+
|
|
276
|
+
if (!response.RouteTables || response.RouteTables.length === 0) {
|
|
277
|
+
throw new Error(`No route tables found for VPC ${vpcId}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Find a route table that doesn't have direct IGW route (private)
|
|
281
|
+
for (const routeTable of response.RouteTables) {
|
|
282
|
+
let hasIgwRoute = false;
|
|
283
|
+
for (const route of routeTable.Routes || []) {
|
|
284
|
+
if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
|
|
285
|
+
hasIgwRoute = true;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!hasIgwRoute) {
|
|
290
|
+
return routeTable;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// If no private route table found, return the first one
|
|
295
|
+
return response.RouteTables[0];
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error('Error finding private route table:', error);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Find the default KMS key for the account
|
|
304
|
+
* @returns {Promise<string>} KMS key ARN or wildcard pattern as fallback
|
|
305
|
+
*/
|
|
306
|
+
async findDefaultKmsKey() {
|
|
307
|
+
try {
|
|
308
|
+
// First try to find a key with alias/aws/lambda
|
|
309
|
+
const command = new ListKeysCommand({});
|
|
310
|
+
const response = await this.kmsClient.send(command);
|
|
311
|
+
|
|
312
|
+
if (!response.Keys || response.Keys.length === 0) {
|
|
313
|
+
// Return AWS managed key ARN pattern as fallback
|
|
314
|
+
const accountId = await this.getAccountId();
|
|
315
|
+
return `arn:aws:kms:${this.region}:${accountId}:key/*`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Look for customer managed keys first
|
|
319
|
+
for (const key of response.Keys) {
|
|
320
|
+
try {
|
|
321
|
+
const describeCommand = new DescribeKeyCommand({ KeyId: key.KeyId });
|
|
322
|
+
const keyDetails = await this.kmsClient.send(describeCommand);
|
|
323
|
+
|
|
324
|
+
if (keyDetails.KeyMetadata &&
|
|
325
|
+
keyDetails.KeyMetadata.KeyManager === 'CUSTOMER' &&
|
|
326
|
+
keyDetails.KeyMetadata.KeyState === 'Enabled') {
|
|
327
|
+
return keyDetails.KeyMetadata.Arn;
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
// Continue to next key if we can't describe this one
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Fallback to wildcard pattern for AWS managed keys
|
|
336
|
+
const accountId = await this.getAccountId();
|
|
337
|
+
return `arn:aws:kms:${this.region}:${accountId}:key/*`;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error('Error finding default KMS key:', error);
|
|
340
|
+
// Return wildcard pattern as ultimate fallback
|
|
341
|
+
return '*';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Discover all AWS resources needed for Frigg deployment
|
|
347
|
+
* @returns {Promise<Object>} Object containing discovered resource IDs:
|
|
348
|
+
* @returns {string} return.defaultVpcId - The default VPC ID
|
|
349
|
+
* @returns {string} return.defaultSecurityGroupId - The default security group ID
|
|
350
|
+
* @returns {string} return.privateSubnetId1 - First private subnet ID
|
|
351
|
+
* @returns {string} return.privateSubnetId2 - Second private subnet ID
|
|
352
|
+
* @returns {string} return.publicSubnetId - Public subnet ID for NAT Gateway
|
|
353
|
+
* @returns {string} return.privateRouteTableId - Private route table ID
|
|
354
|
+
* @returns {string} return.defaultKmsKeyId - Default KMS key ARN
|
|
355
|
+
* @throws {Error} If resource discovery fails
|
|
356
|
+
*/
|
|
357
|
+
async discoverResources() {
|
|
358
|
+
try {
|
|
359
|
+
console.log('Discovering AWS resources for Frigg deployment...');
|
|
360
|
+
|
|
361
|
+
const vpc = await this.findDefaultVpc();
|
|
362
|
+
console.log(`Found VPC: ${vpc.VpcId}`);
|
|
363
|
+
|
|
364
|
+
const privateSubnets = await this.findPrivateSubnets(vpc.VpcId);
|
|
365
|
+
console.log(`Found ${privateSubnets.length} private subnets: ${privateSubnets.map(s => s.SubnetId).join(', ')}`);
|
|
366
|
+
|
|
367
|
+
const publicSubnet = await this.findPublicSubnets(vpc.VpcId);
|
|
368
|
+
console.log(`Found public subnet for NAT Gateway: ${publicSubnet.SubnetId}`);
|
|
369
|
+
|
|
370
|
+
const securityGroup = await this.findDefaultSecurityGroup(vpc.VpcId);
|
|
371
|
+
console.log(`Found security group: ${securityGroup.GroupId}`);
|
|
372
|
+
|
|
373
|
+
const routeTable = await this.findPrivateRouteTable(vpc.VpcId);
|
|
374
|
+
console.log(`Found route table: ${routeTable.RouteTableId}`);
|
|
375
|
+
|
|
376
|
+
const kmsKeyArn = await this.findDefaultKmsKey();
|
|
377
|
+
console.log(`Found KMS key: ${kmsKeyArn}`);
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
defaultVpcId: vpc.VpcId,
|
|
381
|
+
defaultSecurityGroupId: securityGroup.GroupId,
|
|
382
|
+
privateSubnetId1: privateSubnets[0]?.SubnetId,
|
|
383
|
+
privateSubnetId2: privateSubnets[1]?.SubnetId || privateSubnets[0]?.SubnetId,
|
|
384
|
+
publicSubnetId: publicSubnet.SubnetId,
|
|
385
|
+
privateRouteTableId: routeTable.RouteTableId,
|
|
386
|
+
defaultKmsKeyId: kmsKeyArn
|
|
387
|
+
};
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.error('Error discovering AWS resources:', error);
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = { AWSDiscovery };
|