@friggframework/devtools 2.0.0--canary.406.78e2685.0 → 2.0.0--canary.398.dd443c7.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/build-command/index.js +4 -2
- package/frigg-cli/deploy-command/index.js +5 -2
- package/frigg-cli/generate-iam-command.js +115 -0
- package/frigg-cli/index.js +11 -1
- package/infrastructure/AWS-DISCOVERY-TROUBLESHOOTING.md +245 -0
- package/infrastructure/AWS-IAM-CREDENTIAL-NEEDS.md +596 -0
- package/infrastructure/DEPLOYMENT-INSTRUCTIONS.md +268 -0
- package/infrastructure/GENERATE-IAM-DOCS.md +253 -0
- package/infrastructure/IAM-POLICY-TEMPLATES.md +176 -0
- package/infrastructure/README-TESTING.md +332 -0
- package/infrastructure/README.md +421 -0
- package/infrastructure/WEBSOCKET-CONFIGURATION.md +105 -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 +568 -0
- package/infrastructure/aws-discovery.test.js +373 -0
- package/infrastructure/build-time-discovery.js +206 -0
- package/infrastructure/build-time-discovery.test.js +375 -0
- package/infrastructure/create-frigg-infrastructure.js +2 -2
- package/infrastructure/frigg-deployment-iam-stack.yaml +379 -0
- package/infrastructure/iam-generator.js +687 -0
- package/infrastructure/iam-generator.test.js +169 -0
- package/infrastructure/iam-policy-basic.json +212 -0
- package/infrastructure/iam-policy-full.json +282 -0
- package/infrastructure/integration.test.js +383 -0
- package/infrastructure/run-discovery.js +110 -0
- package/infrastructure/serverless-template.js +514 -167
- package/infrastructure/serverless-template.test.js +541 -0
- package/management-ui/dist/assets/FriggLogo-B7Xx8ZW1.svg +1 -0
- package/management-ui/dist/assets/index-BA21WgFa.js +1221 -0
- package/management-ui/dist/assets/index-CbM64Oba.js +1221 -0
- package/management-ui/dist/assets/index-CkvseXTC.css +1 -0
- package/management-ui/dist/index.html +14 -0
- package/package.json +9 -5
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
const AWS = require('aws-sdk');
|
|
2
|
+
const { AWSDiscovery } = require('./aws-discovery');
|
|
3
|
+
|
|
4
|
+
// Mock AWS SDK
|
|
5
|
+
jest.mock('@aws-sdk/client-ec2');
|
|
6
|
+
jest.mock('@aws-sdk/client-kms');
|
|
7
|
+
jest.mock('@aws-sdk/client-sts');
|
|
8
|
+
|
|
9
|
+
const { EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, DescribeRouteTablesCommand } = require('@aws-sdk/client-ec2');
|
|
10
|
+
const { KMSClient, ListKeysCommand, DescribeKeyCommand } = require('@aws-sdk/client-kms');
|
|
11
|
+
const { STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts');
|
|
12
|
+
|
|
13
|
+
describe('AWSDiscovery', () => {
|
|
14
|
+
let discovery;
|
|
15
|
+
let mockEC2Send;
|
|
16
|
+
let mockKMSSend;
|
|
17
|
+
let mockSTSSend;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
discovery = new AWSDiscovery('us-east-1');
|
|
21
|
+
|
|
22
|
+
// Create mock send functions
|
|
23
|
+
mockEC2Send = jest.fn();
|
|
24
|
+
mockKMSSend = jest.fn();
|
|
25
|
+
mockSTSSend = jest.fn();
|
|
26
|
+
|
|
27
|
+
// Mock the client constructors and send methods
|
|
28
|
+
EC2Client.mockImplementation(() => ({
|
|
29
|
+
send: mockEC2Send
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
KMSClient.mockImplementation(() => ({
|
|
33
|
+
send: mockKMSSend
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
STSClient.mockImplementation(() => ({
|
|
37
|
+
send: mockSTSSend
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Reset mocks
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('getAccountId', () => {
|
|
45
|
+
it('should return AWS account ID', async () => {
|
|
46
|
+
const mockAccountId = '123456789012';
|
|
47
|
+
mockSTSSend.mockResolvedValue({
|
|
48
|
+
Account: mockAccountId
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const result = await discovery.getAccountId();
|
|
52
|
+
|
|
53
|
+
expect(result).toBe(mockAccountId);
|
|
54
|
+
expect(mockSTSSend).toHaveBeenCalledWith(expect.any(GetCallerIdentityCommand));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should throw error when STS call fails', async () => {
|
|
58
|
+
const error = new Error('STS Error');
|
|
59
|
+
mockSTSSend.mockRejectedValue(error);
|
|
60
|
+
|
|
61
|
+
await expect(discovery.getAccountId()).rejects.toThrow('STS Error');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('findDefaultVpc', () => {
|
|
66
|
+
it('should return default VPC when found', async () => {
|
|
67
|
+
const mockVpc = {
|
|
68
|
+
VpcId: 'vpc-12345678',
|
|
69
|
+
IsDefault: true,
|
|
70
|
+
State: 'available'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
mockEC2Send.mockResolvedValue({
|
|
74
|
+
Vpcs: [mockVpc]
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = await discovery.findDefaultVpc();
|
|
78
|
+
|
|
79
|
+
expect(result).toEqual(mockVpc);
|
|
80
|
+
expect(mockEC2Send).toHaveBeenCalledWith(expect.objectContaining({
|
|
81
|
+
input: {
|
|
82
|
+
Filters: [{
|
|
83
|
+
Name: 'is-default',
|
|
84
|
+
Values: ['true']
|
|
85
|
+
}]
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return first available VPC when no default VPC exists', async () => {
|
|
91
|
+
const mockVpc = {
|
|
92
|
+
VpcId: 'vpc-87654321',
|
|
93
|
+
IsDefault: false,
|
|
94
|
+
State: 'available'
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
mockEC2Send
|
|
98
|
+
.mockResolvedValueOnce({ Vpcs: [] }) // No default VPC
|
|
99
|
+
.mockResolvedValueOnce({ Vpcs: [mockVpc] }); // All VPCs
|
|
100
|
+
|
|
101
|
+
const result = await discovery.findDefaultVpc();
|
|
102
|
+
|
|
103
|
+
expect(result).toEqual(mockVpc);
|
|
104
|
+
expect(mockEC2Send).toHaveBeenCalledTimes(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should throw error when no VPCs found', async () => {
|
|
108
|
+
mockEC2Send
|
|
109
|
+
.mockResolvedValueOnce({ Vpcs: [] }) // No default VPC
|
|
110
|
+
.mockResolvedValueOnce({ Vpcs: [] }); // No VPCs at all
|
|
111
|
+
|
|
112
|
+
await expect(discovery.findDefaultVpc()).rejects.toThrow('No VPC found in the account');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('findPrivateSubnets', () => {
|
|
117
|
+
const mockVpcId = 'vpc-12345678';
|
|
118
|
+
|
|
119
|
+
it('should return private subnets when found', async () => {
|
|
120
|
+
const mockSubnets = [
|
|
121
|
+
{ SubnetId: 'subnet-private-1', VpcId: mockVpcId },
|
|
122
|
+
{ SubnetId: 'subnet-private-2', VpcId: mockVpcId }
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
mockEC2Send
|
|
126
|
+
.mockResolvedValueOnce({ Subnets: mockSubnets }) // DescribeSubnets
|
|
127
|
+
.mockResolvedValue({ RouteTables: [] }); // DescribeRouteTables (private)
|
|
128
|
+
|
|
129
|
+
const result = await discovery.findPrivateSubnets(mockVpcId);
|
|
130
|
+
|
|
131
|
+
expect(result).toHaveLength(2);
|
|
132
|
+
expect(result[0].SubnetId).toBe('subnet-private-1');
|
|
133
|
+
expect(result[1].SubnetId).toBe('subnet-private-2');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should return at least 2 subnets even if mixed private/public', async () => {
|
|
137
|
+
const mockSubnets = [
|
|
138
|
+
{ SubnetId: 'subnet-1', VpcId: mockVpcId },
|
|
139
|
+
{ SubnetId: 'subnet-2', VpcId: mockVpcId },
|
|
140
|
+
{ SubnetId: 'subnet-3', VpcId: mockVpcId }
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
mockEC2Send
|
|
144
|
+
.mockResolvedValueOnce({ Subnets: mockSubnets }) // DescribeSubnets
|
|
145
|
+
.mockResolvedValue({ // Only one private subnet
|
|
146
|
+
RouteTables: [{
|
|
147
|
+
Routes: [] // No IGW route = private
|
|
148
|
+
}]
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const result = await discovery.findPrivateSubnets(mockVpcId);
|
|
152
|
+
|
|
153
|
+
expect(result).toHaveLength(2);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should throw error when no subnets found', async () => {
|
|
157
|
+
mockEC2Send.mockResolvedValue({ Subnets: [] });
|
|
158
|
+
|
|
159
|
+
await expect(discovery.findPrivateSubnets(mockVpcId)).rejects.toThrow(`No subnets found in VPC ${mockVpcId}`);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('isSubnetPrivate', () => {
|
|
164
|
+
const mockSubnetId = 'subnet-12345678';
|
|
165
|
+
|
|
166
|
+
it('should return false for public subnet (has IGW route)', async () => {
|
|
167
|
+
mockEC2Send.mockResolvedValue({
|
|
168
|
+
RouteTables: [{
|
|
169
|
+
Routes: [{
|
|
170
|
+
GatewayId: 'igw-12345678',
|
|
171
|
+
DestinationCidrBlock: '0.0.0.0/0'
|
|
172
|
+
}]
|
|
173
|
+
}]
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = await discovery.isSubnetPrivate(mockSubnetId);
|
|
177
|
+
|
|
178
|
+
expect(result).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should return true for private subnet (no IGW route)', async () => {
|
|
182
|
+
mockEC2Send.mockResolvedValue({
|
|
183
|
+
RouteTables: [{
|
|
184
|
+
Routes: [{
|
|
185
|
+
GatewayId: 'local',
|
|
186
|
+
DestinationCidrBlock: '10.0.0.0/16'
|
|
187
|
+
}]
|
|
188
|
+
}]
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const result = await discovery.isSubnetPrivate(mockSubnetId);
|
|
192
|
+
|
|
193
|
+
expect(result).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should default to private on error', async () => {
|
|
197
|
+
mockEC2Send.mockRejectedValue(new Error('Route table error'));
|
|
198
|
+
|
|
199
|
+
const result = await discovery.isSubnetPrivate(mockSubnetId);
|
|
200
|
+
|
|
201
|
+
expect(result).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('findDefaultSecurityGroup', () => {
|
|
206
|
+
const mockVpcId = 'vpc-12345678';
|
|
207
|
+
|
|
208
|
+
it('should return Frigg security group when found', async () => {
|
|
209
|
+
const mockFriggSg = {
|
|
210
|
+
GroupId: 'sg-frigg-123',
|
|
211
|
+
GroupName: 'frigg-lambda-sg',
|
|
212
|
+
VpcId: mockVpcId
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
mockEC2Send.mockResolvedValue({
|
|
216
|
+
SecurityGroups: [mockFriggSg]
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const result = await discovery.findDefaultSecurityGroup(mockVpcId);
|
|
220
|
+
|
|
221
|
+
expect(result).toEqual(mockFriggSg);
|
|
222
|
+
expect(mockEC2Send).toHaveBeenCalledWith(expect.objectContaining({
|
|
223
|
+
input: {
|
|
224
|
+
Filters: [
|
|
225
|
+
{ Name: 'vpc-id', Values: [mockVpcId] },
|
|
226
|
+
{ Name: 'group-name', Values: ['frigg-lambda-sg'] }
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
}));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should fallback to default security group', async () => {
|
|
233
|
+
const mockDefaultSg = {
|
|
234
|
+
GroupId: 'sg-default-123',
|
|
235
|
+
GroupName: 'default',
|
|
236
|
+
VpcId: mockVpcId
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
mockEC2Send
|
|
240
|
+
.mockResolvedValueOnce({ SecurityGroups: [] }) // No Frigg SG
|
|
241
|
+
.mockResolvedValueOnce({ SecurityGroups: [mockDefaultSg] }); // Default SG
|
|
242
|
+
|
|
243
|
+
const result = await discovery.findDefaultSecurityGroup(mockVpcId);
|
|
244
|
+
|
|
245
|
+
expect(result).toEqual(mockDefaultSg);
|
|
246
|
+
expect(mockEC2Send).toHaveBeenCalledTimes(2);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should throw error when no security groups found', async () => {
|
|
250
|
+
mockEC2Send.mockResolvedValue({ SecurityGroups: [] });
|
|
251
|
+
|
|
252
|
+
await expect(discovery.findDefaultSecurityGroup(mockVpcId)).rejects.toThrow(`No security group found for VPC ${mockVpcId}`);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('findDefaultKmsKey', () => {
|
|
257
|
+
it('should return customer managed key when found', async () => {
|
|
258
|
+
const mockKeyId = 'key-12345678';
|
|
259
|
+
const mockKeyArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012';
|
|
260
|
+
|
|
261
|
+
mockKMSSend
|
|
262
|
+
.mockResolvedValueOnce({ // ListKeys
|
|
263
|
+
Keys: [{ KeyId: mockKeyId }]
|
|
264
|
+
})
|
|
265
|
+
.mockResolvedValueOnce({ // DescribeKey
|
|
266
|
+
KeyMetadata: {
|
|
267
|
+
KeyId: mockKeyId,
|
|
268
|
+
Arn: mockKeyArn,
|
|
269
|
+
KeyManager: 'CUSTOMER',
|
|
270
|
+
KeyState: 'Enabled'
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
mockSTSSend.mockResolvedValue({ Account: '123456789012' });
|
|
275
|
+
|
|
276
|
+
const result = await discovery.findDefaultKmsKey();
|
|
277
|
+
|
|
278
|
+
expect(result).toBe(mockKeyArn);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should return wildcard pattern when no customer keys found', async () => {
|
|
282
|
+
mockKMSSend.mockResolvedValue({ Keys: [] });
|
|
283
|
+
mockSTSSend.mockResolvedValue({ Account: '123456789012' });
|
|
284
|
+
|
|
285
|
+
const result = await discovery.findDefaultKmsKey();
|
|
286
|
+
|
|
287
|
+
expect(result).toBe('arn:aws:kms:us-east-1:123456789012:key/*');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should return fallback on error', async () => {
|
|
291
|
+
mockKMSSend.mockRejectedValue(new Error('KMS Error'));
|
|
292
|
+
|
|
293
|
+
const result = await discovery.findDefaultKmsKey();
|
|
294
|
+
|
|
295
|
+
expect(result).toBe('*');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('discoverResources', () => {
|
|
300
|
+
it('should discover all AWS resources successfully', async () => {
|
|
301
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
302
|
+
const mockSubnets = [
|
|
303
|
+
{ SubnetId: 'subnet-1' },
|
|
304
|
+
{ SubnetId: 'subnet-2' }
|
|
305
|
+
];
|
|
306
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
307
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
308
|
+
const mockKmsArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678';
|
|
309
|
+
|
|
310
|
+
// Mock all the discovery methods
|
|
311
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
312
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
313
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
314
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
315
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(mockKmsArn);
|
|
316
|
+
|
|
317
|
+
const result = await discovery.discoverResources();
|
|
318
|
+
|
|
319
|
+
expect(result).toEqual({
|
|
320
|
+
defaultVpcId: 'vpc-12345678',
|
|
321
|
+
defaultSecurityGroupId: 'sg-12345678',
|
|
322
|
+
privateSubnetId1: 'subnet-1',
|
|
323
|
+
privateSubnetId2: 'subnet-2',
|
|
324
|
+
privateRouteTableId: 'rtb-12345678',
|
|
325
|
+
defaultKmsKeyId: mockKmsArn
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Verify all methods were called
|
|
329
|
+
expect(discovery.findDefaultVpc).toHaveBeenCalled();
|
|
330
|
+
expect(discovery.findPrivateSubnets).toHaveBeenCalledWith('vpc-12345678');
|
|
331
|
+
expect(discovery.findDefaultSecurityGroup).toHaveBeenCalledWith('vpc-12345678');
|
|
332
|
+
expect(discovery.findPrivateRouteTable).toHaveBeenCalledWith('vpc-12345678');
|
|
333
|
+
expect(discovery.findDefaultKmsKey).toHaveBeenCalled();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should handle single subnet scenario', async () => {
|
|
337
|
+
const mockVpc = { VpcId: 'vpc-12345678' };
|
|
338
|
+
const mockSubnets = [{ SubnetId: 'subnet-1' }]; // Only one subnet
|
|
339
|
+
const mockSecurityGroup = { GroupId: 'sg-12345678' };
|
|
340
|
+
const mockRouteTable = { RouteTableId: 'rtb-12345678' };
|
|
341
|
+
const mockKmsArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678';
|
|
342
|
+
|
|
343
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockResolvedValue(mockVpc);
|
|
344
|
+
jest.spyOn(discovery, 'findPrivateSubnets').mockResolvedValue(mockSubnets);
|
|
345
|
+
jest.spyOn(discovery, 'findDefaultSecurityGroup').mockResolvedValue(mockSecurityGroup);
|
|
346
|
+
jest.spyOn(discovery, 'findPrivateRouteTable').mockResolvedValue(mockRouteTable);
|
|
347
|
+
jest.spyOn(discovery, 'findDefaultKmsKey').mockResolvedValue(mockKmsArn);
|
|
348
|
+
|
|
349
|
+
const result = await discovery.discoverResources();
|
|
350
|
+
|
|
351
|
+
expect(result.privateSubnetId1).toBe('subnet-1');
|
|
352
|
+
expect(result.privateSubnetId2).toBe('subnet-1'); // Should duplicate single subnet
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should throw error when discovery fails', async () => {
|
|
356
|
+
jest.spyOn(discovery, 'findDefaultVpc').mockRejectedValue(new Error('VPC discovery failed'));
|
|
357
|
+
|
|
358
|
+
await expect(discovery.discoverResources()).rejects.toThrow('VPC discovery failed');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe('constructor', () => {
|
|
363
|
+
it('should initialize with default region', () => {
|
|
364
|
+
const defaultDiscovery = new AWSDiscovery();
|
|
365
|
+
expect(defaultDiscovery.region).toBe('us-east-1');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should initialize with custom region', () => {
|
|
369
|
+
const customDiscovery = new AWSDiscovery('us-west-2');
|
|
370
|
+
expect(customDiscovery.region).toBe('us-west-2');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
let AWSDiscovery;
|
|
4
|
+
|
|
5
|
+
function loadAWSDiscovery() {
|
|
6
|
+
if (!AWSDiscovery) {
|
|
7
|
+
({ AWSDiscovery } = require('./aws-discovery'));
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build-time AWS resource discovery and configuration injection
|
|
13
|
+
* This utility runs during the build process to discover AWS resources
|
|
14
|
+
* and inject them into the serverless configuration
|
|
15
|
+
*/
|
|
16
|
+
class BuildTimeDiscovery {
|
|
17
|
+
/**
|
|
18
|
+
* Creates an instance of BuildTimeDiscovery
|
|
19
|
+
* @param {string} [region=process.env.AWS_REGION || 'us-east-1'] - AWS region for discovery
|
|
20
|
+
*/
|
|
21
|
+
constructor(region = process.env.AWS_REGION || 'us-east-1') {
|
|
22
|
+
loadAWSDiscovery();
|
|
23
|
+
this.region = region;
|
|
24
|
+
this.discovery = new AWSDiscovery(region);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Discover AWS resources and create a configuration file
|
|
29
|
+
* @param {string} [outputPath='./aws-discovery-config.json'] - Path to write the configuration file
|
|
30
|
+
* @returns {Promise<Object>} Configuration object containing discovered resources
|
|
31
|
+
* @throws {Error} If AWS resource discovery fails
|
|
32
|
+
*/
|
|
33
|
+
async discoverAndCreateConfig(outputPath = './aws-discovery-config.json') {
|
|
34
|
+
try {
|
|
35
|
+
console.log('Starting AWS resource discovery for build...');
|
|
36
|
+
|
|
37
|
+
const resources = await this.discovery.discoverResources();
|
|
38
|
+
|
|
39
|
+
// Create configuration object
|
|
40
|
+
const config = {
|
|
41
|
+
awsDiscovery: resources,
|
|
42
|
+
generatedAt: new Date().toISOString(),
|
|
43
|
+
region: this.region
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Write configuration to file
|
|
47
|
+
fs.writeFileSync(outputPath, JSON.stringify(config, null, 2));
|
|
48
|
+
console.log(`AWS discovery configuration written to: ${outputPath}`);
|
|
49
|
+
|
|
50
|
+
return config;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Error during AWS resource discovery:', error.message);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Replace placeholders in serverless template with discovered values
|
|
59
|
+
* @param {string} templateContent - The template content with placeholders
|
|
60
|
+
* @param {Object} discoveredResources - Object containing discovered AWS resource IDs
|
|
61
|
+
* @returns {string} Updated template content with placeholders replaced
|
|
62
|
+
*/
|
|
63
|
+
replaceTemplateVariables(templateContent, discoveredResources) {
|
|
64
|
+
let updatedContent = templateContent;
|
|
65
|
+
|
|
66
|
+
// Replace AWS discovery placeholders
|
|
67
|
+
const replacements = {
|
|
68
|
+
'${self:custom.awsDiscovery.defaultVpcId}': discoveredResources.defaultVpcId,
|
|
69
|
+
'${self:custom.awsDiscovery.defaultSecurityGroupId}': discoveredResources.defaultSecurityGroupId,
|
|
70
|
+
'${self:custom.awsDiscovery.privateSubnetId1}': discoveredResources.privateSubnetId1,
|
|
71
|
+
'${self:custom.awsDiscovery.privateSubnetId2}': discoveredResources.privateSubnetId2,
|
|
72
|
+
'${self:custom.awsDiscovery.privateRouteTableId}': discoveredResources.privateRouteTableId,
|
|
73
|
+
'${self:custom.awsDiscovery.defaultKmsKeyId}': discoveredResources.defaultKmsKeyId
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
77
|
+
// Use a more targeted replacement to avoid replacing similar strings
|
|
78
|
+
updatedContent = updatedContent.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return updatedContent;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Process serverless configuration and inject discovered resources
|
|
86
|
+
* @param {string} configPath - Path to the serverless configuration file
|
|
87
|
+
* @param {string} [outputPath=null] - Output path for updated config (defaults to overwriting original)
|
|
88
|
+
* @returns {Promise<Object>} Object containing discovered AWS resources
|
|
89
|
+
* @throws {Error} If processing the serverless configuration fails
|
|
90
|
+
*/
|
|
91
|
+
async processServerlessConfig(configPath, outputPath = null) {
|
|
92
|
+
try {
|
|
93
|
+
console.log(`Processing serverless configuration: ${configPath}`);
|
|
94
|
+
|
|
95
|
+
// Read the current serverless configuration
|
|
96
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
97
|
+
|
|
98
|
+
// Discover AWS resources
|
|
99
|
+
const resources = await this.discovery.discoverResources();
|
|
100
|
+
|
|
101
|
+
// Replace placeholders with discovered values
|
|
102
|
+
const updatedContent = this.replaceTemplateVariables(configContent, resources);
|
|
103
|
+
|
|
104
|
+
// Write to output file or overwrite original
|
|
105
|
+
const finalPath = outputPath || configPath;
|
|
106
|
+
fs.writeFileSync(finalPath, updatedContent);
|
|
107
|
+
|
|
108
|
+
console.log(`Updated serverless configuration written to: ${finalPath}`);
|
|
109
|
+
|
|
110
|
+
return resources;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Error processing serverless configuration:', error.message);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate a custom serverless configuration section for discovered resources
|
|
119
|
+
* @param {Object} discoveredResources - Object containing discovered AWS resource IDs
|
|
120
|
+
* @returns {Object} Custom section object for serverless configuration
|
|
121
|
+
*/
|
|
122
|
+
generateCustomSection(discoveredResources) {
|
|
123
|
+
return {
|
|
124
|
+
awsDiscovery: discoveredResources
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Pre-build hook to discover resources and prepare configuration
|
|
130
|
+
* @param {Object} appDefinition - Application definition object
|
|
131
|
+
* @param {string} region - AWS region for discovery
|
|
132
|
+
* @returns {Promise<Object|null>} Discovered resources or null if discovery not needed
|
|
133
|
+
* @throws {Error} If pre-build AWS discovery fails
|
|
134
|
+
*/
|
|
135
|
+
async preBuildHook(appDefinition, region) {
|
|
136
|
+
try {
|
|
137
|
+
console.log('Running pre-build AWS discovery hook...');
|
|
138
|
+
|
|
139
|
+
// Only run discovery if VPC, KMS, or SSM features are enabled
|
|
140
|
+
const needsDiscovery = appDefinition.vpc?.enable ||
|
|
141
|
+
appDefinition.encryption?.useDefaultKMSForFieldLevelEncryption ||
|
|
142
|
+
appDefinition.ssm?.enable;
|
|
143
|
+
|
|
144
|
+
if (!needsDiscovery) {
|
|
145
|
+
console.log('No AWS discovery needed based on app definition');
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Create discovery instance with specified region
|
|
150
|
+
loadAWSDiscovery();
|
|
151
|
+
const discovery = new AWSDiscovery(region);
|
|
152
|
+
const resources = await discovery.discoverResources();
|
|
153
|
+
|
|
154
|
+
// Create environment variables for serverless
|
|
155
|
+
const envVars = {
|
|
156
|
+
AWS_DISCOVERY_VPC_ID: resources.defaultVpcId,
|
|
157
|
+
AWS_DISCOVERY_SECURITY_GROUP_ID: resources.defaultSecurityGroupId,
|
|
158
|
+
AWS_DISCOVERY_SUBNET_ID_1: resources.privateSubnetId1,
|
|
159
|
+
AWS_DISCOVERY_SUBNET_ID_2: resources.privateSubnetId2,
|
|
160
|
+
AWS_DISCOVERY_PUBLIC_SUBNET_ID: resources.publicSubnetId,
|
|
161
|
+
AWS_DISCOVERY_ROUTE_TABLE_ID: resources.privateRouteTableId,
|
|
162
|
+
AWS_DISCOVERY_KMS_KEY_ID: resources.defaultKmsKeyId
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Set environment variables for serverless to use
|
|
166
|
+
Object.assign(process.env, envVars);
|
|
167
|
+
|
|
168
|
+
console.log('AWS discovery completed and environment variables set');
|
|
169
|
+
return resources;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('Error in pre-build AWS discovery hook:', error.message);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* CLI utility function for build-time discovery
|
|
179
|
+
* @param {Object} [options={}] - Options for build-time discovery
|
|
180
|
+
* @param {string} [options.region=process.env.AWS_REGION || 'us-east-1'] - AWS region
|
|
181
|
+
* @param {string} [options.outputPath='./aws-discovery-config.json'] - Output path for config file
|
|
182
|
+
* @param {string} [options.configPath=null] - Path to existing serverless config to process
|
|
183
|
+
* @returns {Promise<Object>} Discovered AWS resources
|
|
184
|
+
*/
|
|
185
|
+
async function runBuildTimeDiscovery(options = {}) {
|
|
186
|
+
const {
|
|
187
|
+
region = process.env.AWS_REGION || 'us-east-1',
|
|
188
|
+
outputPath = './aws-discovery-config.json',
|
|
189
|
+
configPath = null
|
|
190
|
+
} = options;
|
|
191
|
+
|
|
192
|
+
const discovery = new BuildTimeDiscovery(region);
|
|
193
|
+
|
|
194
|
+
if (configPath) {
|
|
195
|
+
// Process existing serverless configuration
|
|
196
|
+
return await discovery.processServerlessConfig(configPath);
|
|
197
|
+
} else {
|
|
198
|
+
// Just discover and create config file
|
|
199
|
+
return await discovery.discoverAndCreateConfig(outputPath);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
BuildTimeDiscovery,
|
|
205
|
+
runBuildTimeDiscovery
|
|
206
|
+
};
|