@friggframework/devtools 2.0.0--canary.463.62579dd.0 → 2.0.0--canary.461.ec909cf.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/db-setup.test.js +1 -1
- package/frigg-cli/db-setup-command/index.js +1 -1
- package/infrastructure/POSTGRES-CONFIGURATION.md +630 -0
- package/infrastructure/README.md +51 -0
- package/infrastructure/__tests__/postgres-config.test.js +914 -0
- package/infrastructure/aws-discovery.js +549 -21
- package/infrastructure/aws-discovery.test.js +447 -1
- package/infrastructure/domains/database/aurora-builder.js +307 -0
- package/infrastructure/domains/database/aurora-builder.test.js +482 -0
- package/infrastructure/domains/networking/vpc-builder.js +718 -0
- package/infrastructure/domains/networking/vpc-builder.test.js +772 -0
- package/infrastructure/domains/networking/vpc-discovery.js +159 -0
- package/infrastructure/domains/shared/providers/aws-provider-adapter.js +445 -0
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +385 -0
- package/infrastructure/domains/shared/utilities/handler-path-resolver.js +129 -0
- package/infrastructure/infrastructure-composer.test.js +1895 -0
- package/infrastructure/scripts/build-prisma-layer.js +534 -0
- package/infrastructure/serverless-template.js +790 -84
- package/infrastructure/serverless-template.test.js +94 -1
- package/package.json +8 -6
- package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
- package/frigg-cli/utils/prisma-runner.js +0 -280
|
@@ -8,6 +8,8 @@ let EC2Client,
|
|
|
8
8
|
DescribeInternetGatewaysCommand;
|
|
9
9
|
let KMSClient, ListKeysCommand, DescribeKeyCommand;
|
|
10
10
|
let STSClient, GetCallerIdentityCommand;
|
|
11
|
+
let RDSClient, DescribeDBClustersCommand, DescribeDBSubnetGroupsCommand;
|
|
12
|
+
let SecretsManagerClient, ListSecretsCommand, DescribeSecretCommand;
|
|
11
13
|
|
|
12
14
|
function loadEC2() {
|
|
13
15
|
if (!EC2Client) {
|
|
@@ -30,6 +32,7 @@ function loadKMS() {
|
|
|
30
32
|
KMSClient,
|
|
31
33
|
ListKeysCommand,
|
|
32
34
|
DescribeKeyCommand,
|
|
35
|
+
ListAliasesCommand,
|
|
33
36
|
} = require('@aws-sdk/client-kms'));
|
|
34
37
|
}
|
|
35
38
|
}
|
|
@@ -43,15 +46,121 @@ function loadSTS() {
|
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
function loadRDS() {
|
|
50
|
+
if (!RDSClient) {
|
|
51
|
+
({
|
|
52
|
+
RDSClient,
|
|
53
|
+
DescribeDBClustersCommand,
|
|
54
|
+
DescribeDBSubnetGroupsCommand,
|
|
55
|
+
} = require('@aws-sdk/client-rds'));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadSecretsManager() {
|
|
60
|
+
if (!SecretsManagerClient) {
|
|
61
|
+
({
|
|
62
|
+
SecretsManagerClient,
|
|
63
|
+
ListSecretsCommand,
|
|
64
|
+
DescribeSecretCommand,
|
|
65
|
+
} = require('@aws-sdk/client-secrets-manager'));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
class AWSDiscovery {
|
|
47
70
|
constructor(region = 'us-east-1') {
|
|
71
|
+
console.log('[AWSDiscovery] Initializing AWSDiscovery...');
|
|
72
|
+
console.log('[AWSDiscovery] Region:', region);
|
|
73
|
+
console.log('[AWSDiscovery] System time:', new Date().toISOString());
|
|
74
|
+
console.log('[AWSDiscovery] AWS_PROFILE:', process.env.AWS_PROFILE);
|
|
75
|
+
console.log('[AWSDiscovery] AWS_ACCESS_KEY_ID:', process.env.AWS_ACCESS_KEY_ID ? 'SET (hidden)' : 'NOT SET');
|
|
76
|
+
console.log('[AWSDiscovery] AWS_SECRET_ACCESS_KEY:', process.env.AWS_SECRET_ACCESS_KEY ? 'SET (hidden)' : 'NOT SET');
|
|
77
|
+
console.log('[AWSDiscovery] NODE_TLS_REJECT_UNAUTHORIZED:', process.env.NODE_TLS_REJECT_UNAUTHORIZED);
|
|
78
|
+
|
|
48
79
|
this.region = region;
|
|
49
80
|
loadEC2();
|
|
50
81
|
loadKMS();
|
|
51
82
|
loadSTS();
|
|
83
|
+
loadRDS();
|
|
84
|
+
loadSecretsManager();
|
|
52
85
|
this.ec2Client = new EC2Client({ region });
|
|
53
86
|
this.kmsClient = new KMSClient({ region });
|
|
54
87
|
this.stsClient = new STSClient({ region });
|
|
88
|
+
this.rdsClient = new RDSClient({ region });
|
|
89
|
+
this.secretsManagerClient = new SecretsManagerClient({ region });
|
|
90
|
+
|
|
91
|
+
console.log('[AWSDiscovery] AWS clients initialized successfully');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async validateCredentials() {
|
|
95
|
+
console.log('[AWSDiscovery] Validating AWS credentials...');
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const command = new GetCallerIdentityCommand({});
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
const response = await this.stsClient.send(command);
|
|
101
|
+
const duration = Date.now() - startTime;
|
|
102
|
+
|
|
103
|
+
console.log('[AWSDiscovery] ✅ Credentials are VALID');
|
|
104
|
+
console.log('[AWSDiscovery] Account ID:', response.Account);
|
|
105
|
+
console.log('[AWSDiscovery] User ARN:', response.Arn);
|
|
106
|
+
console.log('[AWSDiscovery] User ID:', response.UserId);
|
|
107
|
+
console.log('[AWSDiscovery] Validation took', duration, 'ms');
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
valid: true,
|
|
111
|
+
accountId: response.Account,
|
|
112
|
+
arn: response.Arn,
|
|
113
|
+
userId: response.UserId
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('[AWSDiscovery] ❌ CREDENTIAL VALIDATION FAILED');
|
|
117
|
+
console.error('[AWSDiscovery] Error:', error.message);
|
|
118
|
+
console.error('[AWSDiscovery] Error Code:', error.Code || error.code);
|
|
119
|
+
|
|
120
|
+
// Provide specific guidance based on error type
|
|
121
|
+
if (error.Code === 'RequestExpired' || error.message.includes('expired')) {
|
|
122
|
+
console.error('\n[AWSDiscovery] 🔍 DIAGNOSIS: Expired Credentials');
|
|
123
|
+
console.error('[AWSDiscovery] Your AWS credentials have expired.');
|
|
124
|
+
console.error('[AWSDiscovery] This commonly happens with:');
|
|
125
|
+
console.error('[AWSDiscovery] - Temporary STS credentials (AWS_ACCESS_KEY_ID starting with "ASIA")');
|
|
126
|
+
console.error('[AWSDiscovery] - AWS SSO sessions that have timed out');
|
|
127
|
+
console.error('[AWSDiscovery] - Hardcoded credentials in .env files');
|
|
128
|
+
console.error('\n[AWSDiscovery] 💡 SOLUTIONS:');
|
|
129
|
+
if (process.env.AWS_ACCESS_KEY_ID?.startsWith('ASIA')) {
|
|
130
|
+
console.error('[AWSDiscovery] 1. Comment out AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in your .env file');
|
|
131
|
+
console.error('[AWSDiscovery] 2. Use AWS_PROFILE instead: AWS_PROFILE=your-profile npm run deploy');
|
|
132
|
+
} else if (process.env.AWS_PROFILE) {
|
|
133
|
+
console.error('[AWSDiscovery] 1. Refresh your AWS SSO login: aws sso login --profile', process.env.AWS_PROFILE);
|
|
134
|
+
console.error('[AWSDiscovery] 2. Or regenerate credentials if using IAM user');
|
|
135
|
+
} else {
|
|
136
|
+
console.error('[AWSDiscovery] 1. Set up AWS profile: aws configure --profile your-profile');
|
|
137
|
+
console.error('[AWSDiscovery] 2. Or use AWS SSO: aws sso login');
|
|
138
|
+
}
|
|
139
|
+
} else if (error.Code === 'InvalidClientTokenId' || error.Code === 'SignatureDoesNotMatch') {
|
|
140
|
+
console.error('\n[AWSDiscovery] 🔍 DIAGNOSIS: Invalid Credentials');
|
|
141
|
+
console.error('[AWSDiscovery] Your AWS credentials are not recognized or incorrect.');
|
|
142
|
+
console.error('\n[AWSDiscovery] 💡 SOLUTIONS:');
|
|
143
|
+
console.error('[AWSDiscovery] 1. Check AWS credentials file: cat ~/.aws/credentials');
|
|
144
|
+
console.error('[AWSDiscovery] 2. Verify profile exists: aws configure list-profiles');
|
|
145
|
+
console.error('[AWSDiscovery] 3. Test credentials: aws sts get-caller-identity --profile', process.env.AWS_PROFILE || 'default');
|
|
146
|
+
} else if (error.message.includes('Could not load credentials')) {
|
|
147
|
+
console.error('\n[AWSDiscovery] 🔍 DIAGNOSIS: No Credentials Found');
|
|
148
|
+
console.error('[AWSDiscovery] AWS SDK cannot find any credentials.');
|
|
149
|
+
console.error('\n[AWSDiscovery] 💡 SOLUTIONS:');
|
|
150
|
+
console.error('[AWSDiscovery] 1. Set AWS_PROFILE: export AWS_PROFILE=your-profile');
|
|
151
|
+
console.error('[AWSDiscovery] 2. Or configure default profile: aws configure');
|
|
152
|
+
console.error('[AWSDiscovery] 3. Or use AWS SSO: aws sso login');
|
|
153
|
+
} else {
|
|
154
|
+
console.error('\n[AWSDiscovery] 💡 GENERAL TROUBLESHOOTING:');
|
|
155
|
+
console.error('[AWSDiscovery] 1. Test credentials manually: aws sts get-caller-identity');
|
|
156
|
+
console.error('[AWSDiscovery] 2. Check ~/.aws/credentials file exists');
|
|
157
|
+
console.error('[AWSDiscovery] 3. Verify network connectivity to AWS');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.error('\n[AWSDiscovery] ⛔ Cannot proceed with AWS discovery until credentials are valid.\n');
|
|
161
|
+
|
|
162
|
+
throw new Error(`AWS credential validation failed: ${error.message}. See detailed guidance above.`);
|
|
163
|
+
}
|
|
55
164
|
}
|
|
56
165
|
|
|
57
166
|
async getAccountId() {
|
|
@@ -67,6 +176,10 @@ class AWSDiscovery {
|
|
|
67
176
|
|
|
68
177
|
async findDefaultVpc() {
|
|
69
178
|
try {
|
|
179
|
+
console.log('[AWSDiscovery.findDefaultVpc] Starting VPC discovery...');
|
|
180
|
+
console.log('[AWSDiscovery.findDefaultVpc] Request timestamp:', new Date().toISOString());
|
|
181
|
+
console.log('[AWSDiscovery.findDefaultVpc] Region:', this.region);
|
|
182
|
+
|
|
70
183
|
const command = new DescribeVpcsCommand({
|
|
71
184
|
Filters: [
|
|
72
185
|
{
|
|
@@ -76,23 +189,54 @@ class AWSDiscovery {
|
|
|
76
189
|
],
|
|
77
190
|
});
|
|
78
191
|
|
|
192
|
+
console.log('[AWSDiscovery.findDefaultVpc] Sending DescribeVpcsCommand (default VPC)...');
|
|
193
|
+
console.log('[AWSDiscovery.findDefaultVpc] Request time before send:', new Date().toISOString());
|
|
194
|
+
|
|
195
|
+
const requestStart = Date.now();
|
|
79
196
|
const response = await this.ec2Client.send(command);
|
|
197
|
+
const requestDuration = Date.now() - requestStart;
|
|
198
|
+
|
|
199
|
+
console.log('[AWSDiscovery.findDefaultVpc] Request completed in', requestDuration, 'ms');
|
|
200
|
+
console.log('[AWSDiscovery.findDefaultVpc] Response time:', new Date().toISOString());
|
|
201
|
+
console.log('[AWSDiscovery.findDefaultVpc] Found', response.Vpcs?.length || 0, 'default VPC(s)');
|
|
80
202
|
|
|
81
203
|
if (response.Vpcs && response.Vpcs.length > 0) {
|
|
204
|
+
console.log('[AWSDiscovery.findDefaultVpc] Using default VPC:', response.Vpcs[0].VpcId);
|
|
82
205
|
return response.Vpcs[0];
|
|
83
206
|
}
|
|
84
207
|
|
|
208
|
+
console.log('[AWSDiscovery.findDefaultVpc] No default VPC found, fetching all VPCs...');
|
|
85
209
|
const allVpcsCommand = new DescribeVpcsCommand({});
|
|
210
|
+
|
|
211
|
+
console.log('[AWSDiscovery.findDefaultVpc] Sending DescribeVpcsCommand (all VPCs)...');
|
|
212
|
+
const allVpcsRequestStart = Date.now();
|
|
86
213
|
const allVpcsResponse = await this.ec2Client.send(allVpcsCommand);
|
|
214
|
+
const allVpcsRequestDuration = Date.now() - allVpcsRequestStart;
|
|
215
|
+
|
|
216
|
+
console.log('[AWSDiscovery.findDefaultVpc] All VPCs request completed in', allVpcsRequestDuration, 'ms');
|
|
217
|
+
console.log('[AWSDiscovery.findDefaultVpc] Found', allVpcsResponse.Vpcs?.length || 0, 'VPC(s)');
|
|
87
218
|
|
|
88
219
|
if (allVpcsResponse.Vpcs && allVpcsResponse.Vpcs.length > 0) {
|
|
89
220
|
console.log('No default VPC found, using first available VPC');
|
|
221
|
+
console.log('[AWSDiscovery.findDefaultVpc] Using VPC:', allVpcsResponse.Vpcs[0].VpcId);
|
|
90
222
|
return allVpcsResponse.Vpcs[0];
|
|
91
223
|
}
|
|
92
224
|
|
|
93
225
|
throw new Error('No VPC found in the account');
|
|
94
226
|
} catch (error) {
|
|
95
|
-
console.error('
|
|
227
|
+
console.error('[AWSDiscovery.findDefaultVpc] ERROR occurred at:', new Date().toISOString());
|
|
228
|
+
console.error('[AWSDiscovery.findDefaultVpc] Error type:', error.constructor.name);
|
|
229
|
+
console.error('[AWSDiscovery.findDefaultVpc] Error code:', error.Code || error.code);
|
|
230
|
+
console.error('[AWSDiscovery.findDefaultVpc] Error message:', error.message);
|
|
231
|
+
console.error('[AWSDiscovery.findDefaultVpc] Error $fault:', error.$fault);
|
|
232
|
+
console.error('[AWSDiscovery.findDefaultVpc] Error $metadata:', JSON.stringify(error.$metadata, null, 2));
|
|
233
|
+
|
|
234
|
+
if (error.$response) {
|
|
235
|
+
console.error('[AWSDiscovery.findDefaultVpc] Response status:', error.$response.statusCode);
|
|
236
|
+
console.error('[AWSDiscovery.findDefaultVpc] Response headers:', JSON.stringify(error.$response.headers, null, 2));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.error('[AWSDiscovery.findDefaultVpc] Full error object:', JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
|
|
96
240
|
throw error;
|
|
97
241
|
}
|
|
98
242
|
}
|
|
@@ -212,13 +356,31 @@ class AWSDiscovery {
|
|
|
212
356
|
console.warn(
|
|
213
357
|
'Please create a public subnet or use VPC endpoints instead'
|
|
214
358
|
);
|
|
215
|
-
return null;
|
|
359
|
+
return { primary: null, secondary: null, all: [] };
|
|
216
360
|
}
|
|
217
361
|
|
|
362
|
+
// Sort by AZ to get subnets in different zones
|
|
363
|
+
const sortedByAz = publicSubnets.sort((a, b) =>
|
|
364
|
+
a.AvailabilityZone.localeCompare(b.AvailabilityZone)
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Get subnets from different AZs if possible
|
|
368
|
+
const primary = sortedByAz[0];
|
|
369
|
+
const secondary = sortedByAz.find(s => s.AvailabilityZone !== primary.AvailabilityZone)
|
|
370
|
+
|| sortedByAz[1]
|
|
371
|
+
|| sortedByAz[0]; // Fallback to same subnet if only one exists
|
|
372
|
+
|
|
218
373
|
console.log(
|
|
219
|
-
`Found ${publicSubnets.length} public
|
|
374
|
+
`Found ${publicSubnets.length} public subnet(s)`
|
|
220
375
|
);
|
|
221
|
-
|
|
376
|
+
console.log(` Primary (NAT Gateway): ${primary.SubnetId} (${primary.AvailabilityZone})`);
|
|
377
|
+
if (secondary.SubnetId !== primary.SubnetId) {
|
|
378
|
+
console.log(` Secondary (Aurora): ${secondary.SubnetId} (${secondary.AvailabilityZone})`);
|
|
379
|
+
} else {
|
|
380
|
+
console.warn(` ⚠️ Only one public subnet found - Aurora public deployments will require creating a second subnet`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { primary, secondary, all: publicSubnets };
|
|
222
384
|
} catch (error) {
|
|
223
385
|
console.error('Error finding public subnets:', error);
|
|
224
386
|
throw error;
|
|
@@ -510,6 +672,341 @@ class AWSDiscovery {
|
|
|
510
672
|
}
|
|
511
673
|
}
|
|
512
674
|
|
|
675
|
+
async findKmsAlias(aliasName) {
|
|
676
|
+
try {
|
|
677
|
+
console.log(`[KMS Alias Discovery] Checking for alias: ${aliasName}`);
|
|
678
|
+
const command = new ListAliasesCommand({});
|
|
679
|
+
const response = await this.kmsClient.send(command);
|
|
680
|
+
|
|
681
|
+
if (!response.Aliases || response.Aliases.length === 0) {
|
|
682
|
+
console.log('[KMS Alias Discovery] No aliases found in account');
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const targetAlias = response.Aliases.find(
|
|
687
|
+
alias => alias.AliasName === aliasName
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
if (targetAlias) {
|
|
691
|
+
console.log(`[KMS Alias Discovery] ✅ Found existing alias: ${aliasName}`);
|
|
692
|
+
console.log(`[KMS Alias Discovery] Target Key: ${targetAlias.TargetKeyId}`);
|
|
693
|
+
return targetAlias;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
console.log(`[KMS Alias Discovery] Alias ${aliasName} does not exist`);
|
|
697
|
+
return null;
|
|
698
|
+
} catch (error) {
|
|
699
|
+
console.warn(
|
|
700
|
+
`[KMS Alias Discovery] Error checking for alias ${aliasName}:`,
|
|
701
|
+
error.message
|
|
702
|
+
);
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async findAuroraCluster(clusterIdentifier = null, serviceName = null, stage = null) {
|
|
708
|
+
try {
|
|
709
|
+
console.log('[AWSDiscovery.findAuroraCluster] Starting Aurora cluster discovery...');
|
|
710
|
+
|
|
711
|
+
const command = new DescribeDBClustersCommand({});
|
|
712
|
+
const response = await this.rdsClient.send(command);
|
|
713
|
+
|
|
714
|
+
if (!response.DBClusters || response.DBClusters.length === 0) {
|
|
715
|
+
console.log('[AWSDiscovery.findAuroraCluster] No Aurora clusters found');
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
console.log(`[AWSDiscovery.findAuroraCluster] Found ${response.DBClusters.length} Aurora cluster(s)`);
|
|
720
|
+
|
|
721
|
+
// Filter for Aurora PostgreSQL clusters
|
|
722
|
+
const postgresClusters = response.DBClusters.filter(
|
|
723
|
+
cluster => cluster.Engine === 'aurora-postgresql' && cluster.Status === 'available'
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
if (postgresClusters.length === 0) {
|
|
727
|
+
console.log('[AWSDiscovery.findAuroraCluster] No available Aurora PostgreSQL clusters found');
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Priority 1: User-specified cluster identifier
|
|
732
|
+
if (clusterIdentifier) {
|
|
733
|
+
const targetCluster = postgresClusters.find(
|
|
734
|
+
cluster => cluster.DBClusterIdentifier === clusterIdentifier
|
|
735
|
+
);
|
|
736
|
+
if (targetCluster) {
|
|
737
|
+
console.log(`[AWSDiscovery.findAuroraCluster] Found specified cluster: ${clusterIdentifier}`);
|
|
738
|
+
return this._formatAuroraCluster(targetCluster);
|
|
739
|
+
}
|
|
740
|
+
console.warn(`[AWSDiscovery.findAuroraCluster] Specified cluster ${clusterIdentifier} not found`);
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Priority 2: Frigg-managed cluster with matching service and stage tags
|
|
745
|
+
if (serviceName && stage) {
|
|
746
|
+
const friggCluster = postgresClusters.find(cluster => {
|
|
747
|
+
const tags = cluster.TagList || [];
|
|
748
|
+
const isFrigg = this._isFriggManaged(tags);
|
|
749
|
+
const matchesService = tags.some(tag => tag.Key === 'Service' && tag.Value === serviceName);
|
|
750
|
+
const matchesStage = tags.some(tag => tag.Key === 'Stage' && tag.Value === stage);
|
|
751
|
+
return isFrigg && matchesService && matchesStage;
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
if (friggCluster) {
|
|
755
|
+
console.log(`[AWSDiscovery.findAuroraCluster] Found Frigg-managed cluster: ${friggCluster.DBClusterIdentifier}`);
|
|
756
|
+
return this._formatAuroraCluster(friggCluster);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Priority 3: Any Frigg-managed cluster
|
|
761
|
+
const anyFriggCluster = postgresClusters.find(cluster =>
|
|
762
|
+
this._isFriggManaged(cluster.TagList || [])
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
if (anyFriggCluster) {
|
|
766
|
+
console.log(`[AWSDiscovery.findAuroraCluster] Found Frigg-managed cluster: ${anyFriggCluster.DBClusterIdentifier}`);
|
|
767
|
+
return this._formatAuroraCluster(anyFriggCluster);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Priority 4: First available cluster
|
|
771
|
+
console.log(`[AWSDiscovery.findAuroraCluster] Using first available cluster: ${postgresClusters[0].DBClusterIdentifier}`);
|
|
772
|
+
return this._formatAuroraCluster(postgresClusters[0]);
|
|
773
|
+
|
|
774
|
+
} catch (error) {
|
|
775
|
+
console.error('[AWSDiscovery.findAuroraCluster] Error finding Aurora cluster:', error.message);
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async findDBSubnetGroup(vpcId) {
|
|
781
|
+
try {
|
|
782
|
+
console.log(`[AWSDiscovery.findDBSubnetGroup] Looking for DB subnet groups in VPC ${vpcId}...`);
|
|
783
|
+
|
|
784
|
+
const command = new DescribeDBSubnetGroupsCommand({});
|
|
785
|
+
const response = await this.rdsClient.send(command);
|
|
786
|
+
|
|
787
|
+
if (!response.DBSubnetGroups || response.DBSubnetGroups.length === 0) {
|
|
788
|
+
console.log('[AWSDiscovery.findDBSubnetGroup] No DB subnet groups found');
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Filter by VPC ID
|
|
793
|
+
const vpcSubnetGroups = response.DBSubnetGroups.filter(
|
|
794
|
+
group => group.VpcId === vpcId
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
if (vpcSubnetGroups.length === 0) {
|
|
798
|
+
console.log(`[AWSDiscovery.findDBSubnetGroup] No DB subnet groups found in VPC ${vpcId}`);
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Priority 1: Frigg-managed subnet group
|
|
803
|
+
const friggSubnetGroup = vpcSubnetGroups.find(group =>
|
|
804
|
+
this._isFriggManaged(group.Tags || [])
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
if (friggSubnetGroup) {
|
|
808
|
+
console.log(`[AWSDiscovery.findDBSubnetGroup] Found Frigg-managed subnet group: ${friggSubnetGroup.DBSubnetGroupName}`);
|
|
809
|
+
return {
|
|
810
|
+
name: friggSubnetGroup.DBSubnetGroupName,
|
|
811
|
+
vpcId: friggSubnetGroup.VpcId,
|
|
812
|
+
subnets: friggSubnetGroup.Subnets.map(s => s.SubnetIdentifier),
|
|
813
|
+
description: friggSubnetGroup.DBSubnetGroupDescription
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Priority 2: First available subnet group
|
|
818
|
+
const subnetGroup = vpcSubnetGroups[0];
|
|
819
|
+
console.log(`[AWSDiscovery.findDBSubnetGroup] Found subnet group: ${subnetGroup.DBSubnetGroupName}`);
|
|
820
|
+
return {
|
|
821
|
+
name: subnetGroup.DBSubnetGroupName,
|
|
822
|
+
vpcId: subnetGroup.VpcId,
|
|
823
|
+
subnets: subnetGroup.Subnets.map(s => s.SubnetIdentifier),
|
|
824
|
+
description: subnetGroup.DBSubnetGroupDescription
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
} catch (error) {
|
|
828
|
+
console.error('[AWSDiscovery.findDBSubnetGroup] Error finding DB subnet group:', error.message);
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async findDatabaseSecret(serviceName, stage) {
|
|
834
|
+
try {
|
|
835
|
+
console.log(`[AWSDiscovery.findDatabaseSecret] Looking for database secret (service: ${serviceName}, stage: ${stage})...`);
|
|
836
|
+
|
|
837
|
+
const command = new ListSecretsCommand({
|
|
838
|
+
Filters: [
|
|
839
|
+
{
|
|
840
|
+
Key: 'tag-key',
|
|
841
|
+
Values: ['ManagedBy']
|
|
842
|
+
}
|
|
843
|
+
]
|
|
844
|
+
});
|
|
845
|
+
const response = await this.secretsManagerClient.send(command);
|
|
846
|
+
|
|
847
|
+
if (!response.SecretList || response.SecretList.length === 0) {
|
|
848
|
+
console.log('[AWSDiscovery.findDatabaseSecret] No secrets found');
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Filter for Frigg-managed database secrets
|
|
853
|
+
const friggSecrets = response.SecretList.filter(secret => {
|
|
854
|
+
const tags = secret.Tags || [];
|
|
855
|
+
const isFrigg = this._isFriggManaged(tags);
|
|
856
|
+
const isDatabase = secret.Name?.includes('aurora') || secret.Name?.includes('database');
|
|
857
|
+
return isFrigg && isDatabase;
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
if (friggSecrets.length === 0) {
|
|
861
|
+
console.log('[AWSDiscovery.findDatabaseSecret] No Frigg-managed database secrets found');
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Priority 1: Secret with matching service and stage tags
|
|
866
|
+
if (serviceName && stage) {
|
|
867
|
+
const matchingSecret = friggSecrets.find(secret => {
|
|
868
|
+
const tags = secret.Tags || [];
|
|
869
|
+
const matchesService = tags.some(tag => tag.Key === 'Service' && tag.Value === serviceName);
|
|
870
|
+
const matchesStage = tags.some(tag => tag.Key === 'Stage' && tag.Value === stage);
|
|
871
|
+
return matchesService && matchesStage;
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
if (matchingSecret) {
|
|
875
|
+
console.log(`[AWSDiscovery.findDatabaseSecret] Found matching secret: ${matchingSecret.Name}`);
|
|
876
|
+
return {
|
|
877
|
+
arn: matchingSecret.ARN,
|
|
878
|
+
name: matchingSecret.Name
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Priority 2: First Frigg-managed database secret
|
|
884
|
+
const secret = friggSecrets[0];
|
|
885
|
+
console.log(`[AWSDiscovery.findDatabaseSecret] Found Frigg-managed secret: ${secret.Name}`);
|
|
886
|
+
return {
|
|
887
|
+
arn: secret.ARN,
|
|
888
|
+
name: secret.Name
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
} catch (error) {
|
|
892
|
+
console.error('[AWSDiscovery.findDatabaseSecret] Error finding database secret:', error.message);
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
async discoverAuroraResources(options = {}) {
|
|
898
|
+
try {
|
|
899
|
+
console.log('\n🔍 Discovering Aurora PostgreSQL resources...');
|
|
900
|
+
console.log('═'.repeat(60));
|
|
901
|
+
|
|
902
|
+
const {
|
|
903
|
+
vpcId,
|
|
904
|
+
serviceName,
|
|
905
|
+
stage,
|
|
906
|
+
management = 'discover',
|
|
907
|
+
clusterIdentifier = null
|
|
908
|
+
} = options;
|
|
909
|
+
|
|
910
|
+
const result = {
|
|
911
|
+
clusterIdentifier: null,
|
|
912
|
+
endpoint: null,
|
|
913
|
+
port: null,
|
|
914
|
+
engine: null,
|
|
915
|
+
engineVersion: null,
|
|
916
|
+
status: null,
|
|
917
|
+
dbSubnetGroupName: null,
|
|
918
|
+
secretArn: null,
|
|
919
|
+
secretName: null,
|
|
920
|
+
needsCreation: false
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// For 'use-existing' mode, cluster identifier is required
|
|
924
|
+
if (management === 'use-existing' && !clusterIdentifier) {
|
|
925
|
+
throw new Error('clusterIdentifier is required when management mode is "use-existing"');
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// For 'create-new' mode, skip discovery
|
|
929
|
+
if (management === 'create-new') {
|
|
930
|
+
console.log('💡 Management mode is "create-new" - will provision new Aurora cluster');
|
|
931
|
+
result.needsCreation = true;
|
|
932
|
+
return result;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Discover Aurora cluster
|
|
936
|
+
const cluster = await this.findAuroraCluster(clusterIdentifier, serviceName, stage);
|
|
937
|
+
|
|
938
|
+
if (!cluster) {
|
|
939
|
+
if (management === 'discover') {
|
|
940
|
+
console.log('⚠️ No Aurora cluster found - will provision new cluster');
|
|
941
|
+
result.needsCreation = true;
|
|
942
|
+
return result;
|
|
943
|
+
}
|
|
944
|
+
throw new Error(`No Aurora cluster found with identifier: ${clusterIdentifier}`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
result.clusterIdentifier = cluster.identifier;
|
|
948
|
+
result.endpoint = cluster.endpoint;
|
|
949
|
+
result.port = cluster.port;
|
|
950
|
+
result.engine = cluster.engine;
|
|
951
|
+
result.engineVersion = cluster.engineVersion;
|
|
952
|
+
result.status = cluster.status;
|
|
953
|
+
result.masterUsername = cluster.masterUsername;
|
|
954
|
+
result.isFriggManaged = cluster.isFriggManaged;
|
|
955
|
+
|
|
956
|
+
console.log(`\n✅ Found Aurora Cluster: ${cluster.identifier}`);
|
|
957
|
+
console.log(` Endpoint: ${cluster.endpoint}:${cluster.port}`);
|
|
958
|
+
console.log(` Engine: ${cluster.engine} ${cluster.engineVersion}`);
|
|
959
|
+
console.log(` Status: ${cluster.status}`);
|
|
960
|
+
|
|
961
|
+
// Discover DB subnet group
|
|
962
|
+
const subnetGroup = await this.findDBSubnetGroup(vpcId);
|
|
963
|
+
if (subnetGroup) {
|
|
964
|
+
result.dbSubnetGroupName = subnetGroup.name;
|
|
965
|
+
console.log(`\n✅ Found DB Subnet Group: ${subnetGroup.name}`);
|
|
966
|
+
console.log(` Subnets: ${subnetGroup.subnets.join(', ')}`);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Discover database secret
|
|
970
|
+
const secret = await this.findDatabaseSecret(serviceName, stage);
|
|
971
|
+
if (secret) {
|
|
972
|
+
result.secretArn = secret.arn;
|
|
973
|
+
result.secretName = secret.name;
|
|
974
|
+
console.log(`\n✅ Found Database Secret: ${secret.name}`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
console.log(`\n${'═'.repeat(60)}`);
|
|
978
|
+
console.log('📋 Aurora Discovery Summary:');
|
|
979
|
+
console.log(` Cluster: ${result.clusterIdentifier || 'Not found'}`);
|
|
980
|
+
console.log(` Subnet Group: ${result.dbSubnetGroupName || 'Not found'}`);
|
|
981
|
+
console.log(` Secret: ${result.secretName || 'Not found'}`);
|
|
982
|
+
console.log(`${'═'.repeat(60)}\n`);
|
|
983
|
+
|
|
984
|
+
return result;
|
|
985
|
+
|
|
986
|
+
} catch (error) {
|
|
987
|
+
console.error('❌ Aurora resource discovery failed:', error.message);
|
|
988
|
+
throw error;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
_formatAuroraCluster(cluster) {
|
|
993
|
+
return {
|
|
994
|
+
identifier: cluster.DBClusterIdentifier,
|
|
995
|
+
endpoint: cluster.Endpoint,
|
|
996
|
+
readerEndpoint: cluster.ReaderEndpoint,
|
|
997
|
+
port: cluster.Port,
|
|
998
|
+
engine: cluster.Engine,
|
|
999
|
+
engineVersion: cluster.EngineVersion,
|
|
1000
|
+
status: cluster.Status,
|
|
1001
|
+
masterUsername: cluster.MasterUsername,
|
|
1002
|
+
databaseName: cluster.DatabaseName,
|
|
1003
|
+
vpcSecurityGroups: (cluster.VpcSecurityGroups || []).map(sg => sg.VpcSecurityGroupId),
|
|
1004
|
+
dbSubnetGroup: cluster.DBSubnetGroup,
|
|
1005
|
+
arn: cluster.DBClusterArn,
|
|
1006
|
+
isFriggManaged: this._isFriggManaged(cluster.TagList || [])
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
513
1010
|
async detectMisconfiguredResources(vpcId) {
|
|
514
1011
|
try {
|
|
515
1012
|
const misconfigurations = {
|
|
@@ -649,10 +1146,14 @@ class AWSDiscovery {
|
|
|
649
1146
|
);
|
|
650
1147
|
console.log('═'.repeat(60));
|
|
651
1148
|
|
|
1149
|
+
// Validate credentials before attempting any AWS operations
|
|
1150
|
+
await this.validateCredentials();
|
|
1151
|
+
console.log(''); // Add spacing after validation
|
|
1152
|
+
|
|
652
1153
|
const vpc = await this.findDefaultVpc();
|
|
653
1154
|
console.log(`\n✅ Found VPC: ${vpc.VpcId}`);
|
|
654
1155
|
|
|
655
|
-
const autoConvert = options.selfHeal || false;
|
|
1156
|
+
const autoConvert = options.vpc?.selfHeal || false;
|
|
656
1157
|
|
|
657
1158
|
const privateSubnets = await this.findPrivateSubnets(
|
|
658
1159
|
vpc.VpcId,
|
|
@@ -664,10 +1165,10 @@ class AWSDiscovery {
|
|
|
664
1165
|
.join(', ')}`
|
|
665
1166
|
);
|
|
666
1167
|
|
|
667
|
-
const
|
|
668
|
-
if (
|
|
1168
|
+
const publicSubnets = await this.findPublicSubnets(vpc.VpcId);
|
|
1169
|
+
if (publicSubnets.primary) {
|
|
669
1170
|
console.log(
|
|
670
|
-
`\n✅ Found public subnet for NAT Gateway
|
|
1171
|
+
`\n✅ Found public subnet(s) for NAT Gateway and Aurora`
|
|
671
1172
|
);
|
|
672
1173
|
} else {
|
|
673
1174
|
console.log(
|
|
@@ -690,6 +1191,26 @@ class AWSDiscovery {
|
|
|
690
1191
|
console.log('ℹ️ No KMS key found');
|
|
691
1192
|
}
|
|
692
1193
|
|
|
1194
|
+
// Check if KMS alias already exists
|
|
1195
|
+
let kmsAliasExists = false;
|
|
1196
|
+
if (options.serviceName && options.stage) {
|
|
1197
|
+
const aliasName = `alias/${options.serviceName}-${options.stage}-frigg-kms`;
|
|
1198
|
+
const existingAlias = await this.findKmsAlias(aliasName);
|
|
1199
|
+
kmsAliasExists = existingAlias !== null;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Discover Aurora PostgreSQL resources if enabled
|
|
1203
|
+
let auroraResources = {};
|
|
1204
|
+
if (options.database?.postgres?.enable) {
|
|
1205
|
+
auroraResources = await this.discoverAuroraResources({
|
|
1206
|
+
vpcId: vpc.VpcId,
|
|
1207
|
+
serviceName: options.serviceName,
|
|
1208
|
+
stage: options.stage,
|
|
1209
|
+
management: options.database.postgres.management,
|
|
1210
|
+
clusterIdentifier: options.database.postgres.clusterIdentifier,
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
693
1214
|
const existingNatGateway = await this.findExistingNatGateway(
|
|
694
1215
|
vpc.VpcId
|
|
695
1216
|
);
|
|
@@ -718,15 +1239,15 @@ class AWSDiscovery {
|
|
|
718
1239
|
|
|
719
1240
|
const subnet1IsActuallyPrivate = privateSubnets[0]
|
|
720
1241
|
? await this.isSubnetPrivate(
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1242
|
+
privateSubnets[0].SubnetId,
|
|
1243
|
+
privateSubnets[0].VpcId || vpc.VpcId
|
|
1244
|
+
)
|
|
724
1245
|
: false;
|
|
725
1246
|
const subnet2IsActuallyPrivate = privateSubnets[1]
|
|
726
1247
|
? await this.isSubnetPrivate(
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1248
|
+
privateSubnets[1].SubnetId,
|
|
1249
|
+
privateSubnets[1].VpcId || vpc.VpcId
|
|
1250
|
+
)
|
|
730
1251
|
: subnet1IsActuallyPrivate;
|
|
731
1252
|
|
|
732
1253
|
const subnetStatus = {
|
|
@@ -762,16 +1283,19 @@ class AWSDiscovery {
|
|
|
762
1283
|
.join(', ')}`
|
|
763
1284
|
);
|
|
764
1285
|
console.log(
|
|
765
|
-
` NAT Subnet: ${
|
|
766
|
-
publicSubnet?.SubnetId || 'None (needs creation)'
|
|
1286
|
+
` NAT Subnet: ${publicSubnets.primary?.SubnetId || 'None (needs creation)'
|
|
767
1287
|
}`
|
|
768
1288
|
);
|
|
1289
|
+
if (publicSubnets.secondary && publicSubnets.secondary.SubnetId !== publicSubnets.primary?.SubnetId) {
|
|
1290
|
+
console.log(
|
|
1291
|
+
` Aurora Public Subnet 2: ${publicSubnets.secondary.SubnetId}`
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
769
1294
|
console.log(
|
|
770
1295
|
` NAT Gateway: ${natGatewayId || 'None (will be created)'}`
|
|
771
1296
|
);
|
|
772
1297
|
console.log(
|
|
773
|
-
` Elastic IP: ${
|
|
774
|
-
elasticIpAllocationId || 'None (will be allocated)'
|
|
1298
|
+
` Elastic IP: ${elasticIpAllocationId || 'None (will be allocated)'
|
|
775
1299
|
}`
|
|
776
1300
|
);
|
|
777
1301
|
if (subnetStatus.requiresConversion) {
|
|
@@ -786,9 +1310,12 @@ class AWSDiscovery {
|
|
|
786
1310
|
privateSubnetId1: privateSubnets[0]?.SubnetId,
|
|
787
1311
|
privateSubnetId2:
|
|
788
1312
|
privateSubnets[1]?.SubnetId || privateSubnets[0]?.SubnetId,
|
|
789
|
-
publicSubnetId:
|
|
1313
|
+
publicSubnetId: publicSubnets.primary?.SubnetId || null, // Keep for NAT Gateway backward compat
|
|
1314
|
+
publicSubnetId1: publicSubnets.primary?.SubnetId || null,
|
|
1315
|
+
publicSubnetId2: publicSubnets.secondary?.SubnetId || null,
|
|
790
1316
|
privateRouteTableId: routeTable.RouteTableId,
|
|
791
1317
|
defaultKmsKeyId: kmsKeyArn,
|
|
1318
|
+
kmsAliasExists: kmsAliasExists,
|
|
792
1319
|
existingNatGatewayId: natGatewayId,
|
|
793
1320
|
existingElasticIpAllocationId: elasticIpAllocationId,
|
|
794
1321
|
natGatewayInPrivateSubnet: natGatewayInPrivateSubnet,
|
|
@@ -809,6 +1336,7 @@ class AWSDiscovery {
|
|
|
809
1336
|
}
|
|
810
1337
|
return wrongRoutes;
|
|
811
1338
|
})(),
|
|
1339
|
+
aurora: auroraResources,
|
|
812
1340
|
};
|
|
813
1341
|
} catch (error) {
|
|
814
1342
|
console.error('Error discovering AWS resources:', error);
|
|
@@ -1102,8 +1630,8 @@ class AWSDiscovery {
|
|
|
1102
1630
|
|
|
1103
1631
|
throw new Error(
|
|
1104
1632
|
`No private subnets found in VPC ${vpcId}. ` +
|
|
1105
|
-
|
|
1106
|
-
|
|
1633
|
+
`Found ${publicSubnets.length} public subnets. ` +
|
|
1634
|
+
`Lambda requires private subnets. Enable selfHeal or create private subnets.`
|
|
1107
1635
|
);
|
|
1108
1636
|
}
|
|
1109
1637
|
|