@gugananuvem/aws-local-simulator 1.0.35 → 1.0.39

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.
Files changed (149) hide show
  1. package/README.md +662 -658
  2. package/package.json +10 -3
  3. package/src/config/default-config.js +71 -0
  4. package/src/config/env-loader.js +4 -2
  5. package/src/middleware/authorization.js +271 -0
  6. package/src/middleware/cloudtrail-logger.js +333 -0
  7. package/src/middleware/error-handler.js +138 -0
  8. package/src/middleware/index.js +79 -0
  9. package/src/middleware/policy-enforcement.js +407 -0
  10. package/src/server.js +74 -4
  11. package/src/services/acm/index.js +66 -0
  12. package/src/services/acm/models/certificate.js +202 -0
  13. package/src/services/acm/server.js +224 -0
  14. package/src/services/acm/simulator.js +293 -0
  15. package/src/services/alb/index.js +104 -0
  16. package/src/services/alb/models/loadBalancer.js +88 -0
  17. package/src/services/alb/models/targetGroup.js +128 -0
  18. package/src/services/alb/server.js +160 -0
  19. package/src/services/alb/simulator.js +262 -0
  20. package/src/services/apigateway/index.js +22 -0
  21. package/src/services/apigateway/server.js +247 -35
  22. package/src/services/apigateway/simulator.js +780 -26
  23. package/src/services/asg/index.js +100 -0
  24. package/src/services/asg/models/autoScalingGroup.js +110 -0
  25. package/src/services/asg/models/launchTemplate.js +41 -0
  26. package/src/services/asg/models/scalingPolicy.js +51 -0
  27. package/src/services/asg/server.js +236 -0
  28. package/src/services/asg/simulator.js +410 -0
  29. package/src/services/aurora/cluster-manager.js +731 -0
  30. package/src/services/aurora/data-api.js +514 -0
  31. package/src/services/aurora/db-engine.js +270 -0
  32. package/src/services/aurora/index.js +159 -0
  33. package/src/services/aurora/rds-api.js +189 -0
  34. package/src/services/aurora/server.js +213 -0
  35. package/src/services/aurora/sql-translator.js +334 -0
  36. package/src/services/cloudformation/index.js +2 -2
  37. package/src/services/cloudformation/server.js +1 -0
  38. package/src/services/cloudfront/cache-engine.js +225 -0
  39. package/src/services/cloudfront/geo-restrictions.js +377 -0
  40. package/src/services/cloudfront/index.js +220 -0
  41. package/src/services/cloudfront/models/behavior.js +158 -0
  42. package/src/services/cloudfront/models/distribution.js +284 -0
  43. package/src/services/cloudfront/models/index.js +99 -0
  44. package/src/services/cloudfront/models/invalidation.js +109 -0
  45. package/src/services/cloudfront/models/origin.js +155 -0
  46. package/src/services/cloudfront/origin-router.js +552 -0
  47. package/src/services/cloudfront/origin-validator.js +398 -0
  48. package/src/services/cloudfront/server.js +512 -0
  49. package/src/services/cloudfront/signed-url-generator.js +377 -0
  50. package/src/services/cloudfront/simulator.js +425 -0
  51. package/src/services/cloudfront/store.js +326 -0
  52. package/src/services/cloudshell/env-builder.js +27 -0
  53. package/src/services/cloudshell/index.js +48 -0
  54. package/src/services/cloudshell/routes.js +60 -0
  55. package/src/services/cloudshell/session-manager.js +226 -0
  56. package/src/services/cloudshell/static/.gitkeep +0 -0
  57. package/src/services/cloudshell/static/index.html +406 -0
  58. package/src/services/cloudshell/websocket-handler.js +107 -0
  59. package/src/services/cloudtrail/index.js +2 -3
  60. package/src/services/cloudtrail/server.js +68 -0
  61. package/src/services/cognito/server.js +4 -1
  62. package/src/services/dynamodb/server.js +4 -1
  63. package/src/services/dynamodb/simulator.js +2 -1
  64. package/src/services/ec2/index.js +101 -0
  65. package/src/services/ec2/instanceServer.js +311 -0
  66. package/src/services/ec2/models/ami.js +39 -0
  67. package/src/services/ec2/models/instance.js +98 -0
  68. package/src/services/ec2/models/networkInterface.js +74 -0
  69. package/src/services/ec2/models/securityGroup.js +71 -0
  70. package/src/services/ec2/server.js +350 -0
  71. package/src/services/ec2/simulator.js +436 -0
  72. package/src/services/eventbridge/server.js +36 -0
  73. package/src/services/eventbridge/simulator.js +230 -45
  74. package/src/services/iam/index.js +65 -0
  75. package/src/services/iam/middleware.js +99 -0
  76. package/src/services/iam/server.js +331 -0
  77. package/src/services/iam/simulator.js +1432 -0
  78. package/src/services/index.js +28 -0
  79. package/src/services/kinesis-analytics/index.js +105 -0
  80. package/src/services/kinesis-analytics/models/application.js +325 -0
  81. package/src/services/kinesis-analytics/models/index.js +12 -0
  82. package/src/services/kinesis-analytics/server.js +250 -0
  83. package/src/services/kinesis-analytics/simulator.js +407 -0
  84. package/src/services/kinesis-analytics/store.js +176 -0
  85. package/src/services/kinesis-data-streams/index.js +106 -0
  86. package/src/services/kinesis-data-streams/models/index.js +12 -0
  87. package/src/services/kinesis-data-streams/models/record.js +70 -0
  88. package/src/services/kinesis-data-streams/models/stream.js +280 -0
  89. package/src/services/kinesis-data-streams/server.js +255 -0
  90. package/src/services/kinesis-data-streams/simulator.js +514 -0
  91. package/src/services/kinesis-data-streams/store.js +163 -0
  92. package/src/services/kinesis-firehose/index.js +106 -0
  93. package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
  94. package/src/services/kinesis-firehose/models/index.js +10 -0
  95. package/src/services/kinesis-firehose/server.js +215 -0
  96. package/src/services/kinesis-firehose/simulator.js +388 -0
  97. package/src/services/kinesis-firehose/store.js +163 -0
  98. package/src/services/kms/server.js +4 -1
  99. package/src/services/lambda/index.js +26 -0
  100. package/src/services/lambda/server.js +12 -1
  101. package/src/services/lambda/simulator.js +4 -0
  102. package/src/services/opensearch/index.js +105 -0
  103. package/src/services/opensearch/models/document.js +115 -0
  104. package/src/services/opensearch/models/index.js +73 -0
  105. package/src/services/opensearch/server.js +310 -0
  106. package/src/services/opensearch/simulator.js +605 -0
  107. package/src/services/opensearch/store.js +162 -0
  108. package/src/services/organizations/control-tower-simulator.js +321 -0
  109. package/src/services/organizations/control-tower-store.js +182 -0
  110. package/src/services/organizations/iam-integration.js +413 -0
  111. package/src/services/organizations/index.js +172 -0
  112. package/src/services/organizations/models/index.js +13 -0
  113. package/src/services/organizations/models/invitation.js +48 -0
  114. package/src/services/organizations/models/organization.js +566 -0
  115. package/src/services/organizations/models/role.js +244 -0
  116. package/src/services/organizations/models/scp.js +131 -0
  117. package/src/services/organizations/server.js +736 -0
  118. package/src/services/organizations/simulator.js +593 -0
  119. package/src/services/organizations/store.js +235 -0
  120. package/src/services/organizations/vpc-integration.js +453 -0
  121. package/src/services/route53/index.js +67 -0
  122. package/src/services/route53/models/healthCheck.js +101 -0
  123. package/src/services/route53/models/hostedZone.js +37 -0
  124. package/src/services/route53/models/resourceRecord.js +106 -0
  125. package/src/services/route53/server.js +841 -0
  126. package/src/services/route53/simulator.js +350 -0
  127. package/src/services/s3/index.js +62 -0
  128. package/src/services/s3/presigned-url-generator.js +337 -0
  129. package/src/services/s3/server.js +231 -0
  130. package/src/services/s3/simulator.js +551 -4
  131. package/src/services/sns/index.js +31 -0
  132. package/src/services/sns/server.js +9 -0
  133. package/src/services/sqs/index.js +38 -0
  134. package/src/services/sqs/server.js +57 -4
  135. package/src/services/sqs/simulator.js +423 -33
  136. package/src/services/sts/simulator.js +209 -17
  137. package/src/services/vpc/README.md +459 -0
  138. package/src/services/vpc/ec2ResponseFormatter.js +431 -0
  139. package/src/services/vpc/index.js +90 -0
  140. package/src/services/vpc/models/internetGateway.js +71 -0
  141. package/src/services/vpc/models/routeTable.js +160 -0
  142. package/src/services/vpc/models/subnet.js +48 -0
  143. package/src/services/vpc/models/vpc.js +37 -0
  144. package/src/services/vpc/models/vpcEndpoint.js +100 -0
  145. package/src/services/vpc/models/vpcGateway.js +134 -0
  146. package/src/services/vpc/server.js +836 -0
  147. package/src/services/vpc/simulator.js +613 -0
  148. package/src/utils/policy-cache.js +215 -0
  149. package/aws-config +0 -154
@@ -0,0 +1,731 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cluster Manager - Gerencia metadados de clusters e instâncias Aurora.
5
+ * Persiste dados em metadata.db usando better-sqlite3.
6
+ */
7
+
8
+ const Database = require('better-sqlite3');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const crypto = require('crypto');
12
+ const logger = require('../../utils/logger');
13
+
14
+ // Valid engine versions
15
+ const VALID_ENGINE_VERSIONS = {
16
+ 'aurora-mysql': [
17
+ '5.7.mysql_aurora.2.11.3',
18
+ '8.0.mysql_aurora.3.04.0',
19
+ '8.0.mysql_aurora.3.05.2'
20
+ ],
21
+ 'aurora-postgresql': [
22
+ '13.12',
23
+ '14.9',
24
+ '15.4',
25
+ '16.1'
26
+ ]
27
+ };
28
+
29
+ // Validation patterns
30
+ const IDENTIFIER_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
31
+ const INSTANCE_CLASS_PATTERN = /^db\..+$/;
32
+
33
+ // Default ports by engine
34
+ const DEFAULT_PORTS = {
35
+ 'aurora-mysql': 3306,
36
+ 'aurora-postgresql': 5432
37
+ };
38
+
39
+ class ClusterManager {
40
+ constructor(dataDir, store = null) {
41
+ this.dataDir = dataDir;
42
+ this.metaDb = null;
43
+ this.store = store;
44
+
45
+ // Ensure data directory exists
46
+ fs.mkdirSync(dataDir, { recursive: true });
47
+ }
48
+
49
+ /**
50
+ * Initializes the metadata database and creates schema tables.
51
+ */
52
+ initialize() {
53
+ const dbPath = path.join(this.dataDir, 'metadata.db');
54
+ this.metaDb = new Database(dbPath);
55
+
56
+ // Performance optimizations
57
+ this.metaDb.pragma('journal_mode = WAL');
58
+ this.metaDb.pragma('foreign_keys = ON');
59
+
60
+ this._createSchema();
61
+ this.loadClusters();
62
+ logger.debug('[Aurora ClusterManager] Initialized metadata.db');
63
+ }
64
+
65
+ /**
66
+ * Creates the schema tables if they don't exist.
67
+ * @private
68
+ */
69
+ _createSchema() {
70
+ this.metaDb.exec(`
71
+ CREATE TABLE IF NOT EXISTS clusters (
72
+ db_cluster_identifier TEXT PRIMARY KEY,
73
+ db_cluster_arn TEXT UNIQUE NOT NULL,
74
+ engine TEXT NOT NULL CHECK(engine IN ('aurora-mysql', 'aurora-postgresql')),
75
+ engine_version TEXT NOT NULL,
76
+ status TEXT NOT NULL DEFAULT 'available',
77
+ endpoint TEXT,
78
+ reader_endpoint TEXT,
79
+ port INTEGER NOT NULL,
80
+ database_name TEXT,
81
+ master_username TEXT NOT NULL,
82
+ storage_encrypted INTEGER DEFAULT 0,
83
+ kms_key_id TEXT,
84
+ db_cluster_resource_id TEXT UNIQUE NOT NULL,
85
+ availability_zones TEXT,
86
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
87
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
88
+ );
89
+
90
+ CREATE TABLE IF NOT EXISTS instances (
91
+ db_instance_identifier TEXT PRIMARY KEY,
92
+ db_instance_arn TEXT UNIQUE NOT NULL,
93
+ db_cluster_identifier TEXT NOT NULL REFERENCES clusters(db_cluster_identifier),
94
+ db_instance_class TEXT NOT NULL,
95
+ engine TEXT NOT NULL,
96
+ engine_version TEXT NOT NULL,
97
+ db_instance_status TEXT NOT NULL DEFAULT 'available',
98
+ endpoint_address TEXT,
99
+ endpoint_port INTEGER,
100
+ is_cluster_writer INTEGER DEFAULT 0,
101
+ availability_zone TEXT,
102
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
103
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
104
+ );
105
+
106
+ CREATE TABLE IF NOT EXISTS databases (
107
+ name TEXT NOT NULL,
108
+ cluster_identifier TEXT NOT NULL REFERENCES clusters(db_cluster_identifier),
109
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
110
+ PRIMARY KEY (name, cluster_identifier)
111
+ );
112
+ `);
113
+ }
114
+
115
+ // ─── Cluster Operations ─────────────────────────────────────────────────────
116
+
117
+ /**
118
+ * Creates a new Aurora cluster with full validation.
119
+ * @param {Object} params - Cluster creation parameters
120
+ * @returns {Object} The created cluster object
121
+ */
122
+ createCluster(params) {
123
+ const {
124
+ DBClusterIdentifier,
125
+ Engine,
126
+ EngineVersion,
127
+ MasterUsername,
128
+ MasterUserPassword,
129
+ Port,
130
+ DatabaseName,
131
+ StorageEncrypted,
132
+ KmsKeyId,
133
+ AvailabilityZones
134
+ } = params;
135
+
136
+ // Validate identifier
137
+ this._validateIdentifier(DBClusterIdentifier, 'DBClusterIdentifier');
138
+
139
+ // Validate engine
140
+ if (!['aurora-mysql', 'aurora-postgresql'].includes(Engine)) {
141
+ this._throwError('InvalidParameterValue', `Invalid engine: ${Engine}. Must be 'aurora-mysql' or 'aurora-postgresql'.`, 400);
142
+ }
143
+
144
+ // Validate engine version
145
+ const validVersions = VALID_ENGINE_VERSIONS[Engine];
146
+ if (!validVersions || !validVersions.includes(EngineVersion)) {
147
+ this._throwError('InvalidParameterValue', `Invalid engine version: ${EngineVersion} for engine ${Engine}. Valid versions: ${validVersions.join(', ')}`, 400);
148
+ }
149
+
150
+ // Validate master username
151
+ const maxUsernameLen = Engine === 'aurora-mysql' ? 16 : 63;
152
+ if (!MasterUsername || MasterUsername.length < 1 || MasterUsername.length > maxUsernameLen) {
153
+ this._throwError('InvalidParameterValue', `MasterUsername must be 1-${maxUsernameLen} characters for ${Engine}.`, 400);
154
+ }
155
+
156
+ // Validate master password
157
+ if (!MasterUserPassword || MasterUserPassword.length < 8 || MasterUserPassword.length > 128) {
158
+ this._throwError('InvalidParameterValue', 'MasterUserPassword must be 8-128 characters.', 400);
159
+ }
160
+
161
+ // Validate port
162
+ const port = Port || DEFAULT_PORTS[Engine];
163
+ if (port < 1150 || port > 65535) {
164
+ this._throwError('InvalidParameterValue', `Port must be between 1150 and 65535. Got: ${port}`, 400);
165
+ }
166
+
167
+ // Check for duplicates
168
+ const existing = this._getClusterRow(DBClusterIdentifier);
169
+ if (existing) {
170
+ this._throwError('DBClusterAlreadyExistsFault', `DB Cluster already exists: ${DBClusterIdentifier}`, 400);
171
+ }
172
+
173
+ // Generate ARN and endpoints
174
+ const arn = `arn:aws:rds:us-east-1:000000000000:cluster:${DBClusterIdentifier}`;
175
+ const endpoint = `${DBClusterIdentifier}.cluster-local.us-east-1.rds.amazonaws.com`;
176
+ const readerEndpoint = `${DBClusterIdentifier}.cluster-ro-local.us-east-1.rds.amazonaws.com`;
177
+ const resourceId = `cluster-${crypto.randomUUID().replace(/-/g, '').substring(0, 26)}`;
178
+ const zones = AvailabilityZones || ['us-east-1a', 'us-east-1b', 'us-east-1c'];
179
+
180
+ // Insert into database
181
+ const stmt = this.metaDb.prepare(`
182
+ INSERT INTO clusters (
183
+ db_cluster_identifier, db_cluster_arn, engine, engine_version,
184
+ status, endpoint, reader_endpoint, port, database_name,
185
+ master_username, storage_encrypted, kms_key_id,
186
+ db_cluster_resource_id, availability_zones
187
+ ) VALUES (?, ?, ?, ?, 'available', ?, ?, ?, ?, ?, ?, ?, ?, ?)
188
+ `);
189
+
190
+ stmt.run(
191
+ DBClusterIdentifier,
192
+ arn,
193
+ Engine,
194
+ EngineVersion,
195
+ endpoint,
196
+ readerEndpoint,
197
+ port,
198
+ DatabaseName || null,
199
+ MasterUsername,
200
+ StorageEncrypted ? 1 : 0,
201
+ KmsKeyId || null,
202
+ resourceId,
203
+ JSON.stringify(zones)
204
+ );
205
+
206
+ // If a DatabaseName is provided, register it in the databases table
207
+ if (DatabaseName) {
208
+ const dbStmt = this.metaDb.prepare(
209
+ 'INSERT OR IGNORE INTO databases (name, cluster_identifier) VALUES (?, ?)'
210
+ );
211
+ dbStmt.run(DatabaseName, DBClusterIdentifier);
212
+ }
213
+
214
+ logger.debug(`[Aurora ClusterManager] Cluster '${DBClusterIdentifier}' created`);
215
+ this._persistClusters();
216
+
217
+ return this._formatCluster(this._getClusterRow(DBClusterIdentifier));
218
+ }
219
+
220
+ /**
221
+ * Deletes a cluster and all associated instances and databases.
222
+ * @param {string} identifier - DBClusterIdentifier
223
+ */
224
+ deleteCluster(identifier) {
225
+ const cluster = this._getClusterRow(identifier);
226
+ if (!cluster) {
227
+ this._throwError('DBClusterNotFoundFault', `DB Cluster not found: ${identifier}`, 404);
228
+ }
229
+
230
+ // Delete associated databases
231
+ this.metaDb.prepare('DELETE FROM databases WHERE cluster_identifier = ?').run(identifier);
232
+
233
+ // Delete associated instances
234
+ this.metaDb.prepare('DELETE FROM instances WHERE db_cluster_identifier = ?').run(identifier);
235
+
236
+ // Delete the cluster
237
+ this.metaDb.prepare('DELETE FROM clusters WHERE db_cluster_identifier = ?').run(identifier);
238
+
239
+ logger.debug(`[Aurora ClusterManager] Cluster '${identifier}' deleted`);
240
+ this._persistClusters();
241
+ }
242
+
243
+ /**
244
+ * Gets a single cluster by identifier.
245
+ * @param {string} identifier - DBClusterIdentifier
246
+ * @returns {Object} The cluster object
247
+ */
248
+ getCluster(identifier) {
249
+ const row = this._getClusterRow(identifier);
250
+ if (!row) {
251
+ this._throwError('DBClusterNotFoundFault', `DB Cluster not found: ${identifier}`, 404);
252
+ }
253
+ return this._formatCluster(row);
254
+ }
255
+
256
+ /**
257
+ * Lists all clusters, optionally filtered.
258
+ * @param {Object} [filters] - Optional filters (e.g., { DBClusterIdentifier })
259
+ * @returns {Object[]} Array of cluster objects
260
+ */
261
+ listClusters(filters) {
262
+ if (filters && filters.DBClusterIdentifier) {
263
+ const row = this._getClusterRow(filters.DBClusterIdentifier);
264
+ if (!row) {
265
+ this._throwError('DBClusterNotFoundFault', `DB Cluster not found: ${filters.DBClusterIdentifier}`, 404);
266
+ }
267
+ return [this._formatCluster(row)];
268
+ }
269
+
270
+ const rows = this.metaDb.prepare('SELECT * FROM clusters').all();
271
+ return rows.map(row => this._formatCluster(row));
272
+ }
273
+
274
+ /**
275
+ * Gets a cluster by its ARN (used by Data API for resourceArn resolution).
276
+ * @param {string} arn - The cluster ARN
277
+ * @returns {Object|null} The cluster object or null if not found
278
+ */
279
+ getClusterByArn(arn) {
280
+ const row = this.metaDb.prepare('SELECT * FROM clusters WHERE db_cluster_arn = ?').get(arn);
281
+ if (!row) {
282
+ return null;
283
+ }
284
+ return this._formatCluster(row);
285
+ }
286
+
287
+ /**
288
+ * Updates a cluster's fields.
289
+ * @param {string} identifier - DBClusterIdentifier
290
+ * @param {Object} fields - Fields to update
291
+ * @returns {Object} The updated cluster object
292
+ */
293
+ updateCluster(identifier, fields) {
294
+ const cluster = this._getClusterRow(identifier);
295
+ if (!cluster) {
296
+ this._throwError('DBClusterNotFoundFault', `DB Cluster not found: ${identifier}`, 404);
297
+ }
298
+
299
+ const allowedFields = {
300
+ engine_version: fields.EngineVersion,
301
+ port: fields.Port,
302
+ master_username: fields.MasterUsername,
303
+ database_name: fields.DatabaseName,
304
+ storage_encrypted: fields.StorageEncrypted !== undefined ? (fields.StorageEncrypted ? 1 : 0) : undefined,
305
+ kms_key_id: fields.KmsKeyId,
306
+ status: fields.Status
307
+ };
308
+
309
+ const setClauses = [];
310
+ const values = [];
311
+
312
+ for (const [col, val] of Object.entries(allowedFields)) {
313
+ if (val !== undefined) {
314
+ setClauses.push(`${col} = ?`);
315
+ values.push(val);
316
+ }
317
+ }
318
+
319
+ if (setClauses.length > 0) {
320
+ setClauses.push("updated_at = datetime('now')");
321
+ values.push(identifier);
322
+
323
+ const sql = `UPDATE clusters SET ${setClauses.join(', ')} WHERE db_cluster_identifier = ?`;
324
+ this.metaDb.prepare(sql).run(...values);
325
+ }
326
+
327
+ logger.debug(`[Aurora ClusterManager] Cluster '${identifier}' updated`);
328
+ this._persistClusters();
329
+ return this._formatCluster(this._getClusterRow(identifier));
330
+ }
331
+
332
+ // ─── Instance Operations ────────────────────────────────────────────────────
333
+
334
+ /**
335
+ * Creates a new instance within a cluster.
336
+ * @param {Object} params - Instance creation parameters
337
+ * @returns {Object} The created instance object
338
+ */
339
+ createInstance(params) {
340
+ const {
341
+ DBInstanceIdentifier,
342
+ DBClusterIdentifier,
343
+ DBInstanceClass,
344
+ Engine,
345
+ EngineVersion
346
+ } = params;
347
+
348
+ // Validate instance identifier
349
+ this._validateIdentifier(DBInstanceIdentifier, 'DBInstanceIdentifier');
350
+
351
+ // Validate instance class
352
+ if (!DBInstanceClass || !INSTANCE_CLASS_PATTERN.test(DBInstanceClass)) {
353
+ this._throwError('InvalidParameterValue', `Invalid DBInstanceClass: ${DBInstanceClass}. Must match pattern 'db.*' (e.g., db.r5.large).`, 400);
354
+ }
355
+
356
+ // Verify cluster exists
357
+ const cluster = this._getClusterRow(DBClusterIdentifier);
358
+ if (!cluster) {
359
+ this._throwError('DBClusterNotFoundFault', `DB Cluster not found: ${DBClusterIdentifier}`, 404);
360
+ }
361
+
362
+ // Check for duplicate instance
363
+ const existingInstance = this._getInstanceRow(DBInstanceIdentifier);
364
+ if (existingInstance) {
365
+ this._throwError('InvalidParameterValue', `DB Instance already exists: ${DBInstanceIdentifier}`, 400);
366
+ }
367
+
368
+ // Determine engine info from cluster if not provided
369
+ const engine = Engine || cluster.engine;
370
+ const engineVersion = EngineVersion || cluster.engine_version;
371
+
372
+ // Generate ARN and endpoint
373
+ const arn = `arn:aws:rds:us-east-1:000000000000:db:${DBInstanceIdentifier}`;
374
+ const endpointAddress = `${DBInstanceIdentifier}.local.us-east-1.rds.amazonaws.com`;
375
+ const endpointPort = cluster.port;
376
+
377
+ // Determine if this is the first instance (writer)
378
+ const existingInstances = this.metaDb.prepare(
379
+ 'SELECT COUNT(*) as count FROM instances WHERE db_cluster_identifier = ?'
380
+ ).get(DBClusterIdentifier);
381
+ const isWriter = existingInstances.count === 0 ? 1 : 0;
382
+
383
+ const availabilityZone = 'us-east-1a';
384
+
385
+ // Insert into database
386
+ const stmt = this.metaDb.prepare(`
387
+ INSERT INTO instances (
388
+ db_instance_identifier, db_instance_arn, db_cluster_identifier,
389
+ db_instance_class, engine, engine_version, db_instance_status,
390
+ endpoint_address, endpoint_port, is_cluster_writer, availability_zone
391
+ ) VALUES (?, ?, ?, ?, ?, ?, 'available', ?, ?, ?, ?)
392
+ `);
393
+
394
+ stmt.run(
395
+ DBInstanceIdentifier,
396
+ arn,
397
+ DBClusterIdentifier,
398
+ DBInstanceClass,
399
+ engine,
400
+ engineVersion,
401
+ endpointAddress,
402
+ endpointPort,
403
+ isWriter,
404
+ availabilityZone
405
+ );
406
+
407
+ logger.debug(`[Aurora ClusterManager] Instance '${DBInstanceIdentifier}' created in cluster '${DBClusterIdentifier}'`);
408
+ this._persistClusters();
409
+
410
+ return this._formatInstance(this._getInstanceRow(DBInstanceIdentifier));
411
+ }
412
+
413
+ /**
414
+ * Deletes an instance.
415
+ * @param {string} identifier - DBInstanceIdentifier
416
+ */
417
+ deleteInstance(identifier) {
418
+ const instance = this._getInstanceRow(identifier);
419
+ if (!instance) {
420
+ this._throwError('DBInstanceNotFoundFault', `DB Instance not found: ${identifier}`, 404);
421
+ }
422
+
423
+ this.metaDb.prepare('DELETE FROM instances WHERE db_instance_identifier = ?').run(identifier);
424
+ logger.debug(`[Aurora ClusterManager] Instance '${identifier}' deleted`);
425
+ this._persistClusters();
426
+ }
427
+
428
+ /**
429
+ * Gets a single instance by identifier.
430
+ * @param {string} identifier - DBInstanceIdentifier
431
+ * @returns {Object} The instance object
432
+ */
433
+ getInstance(identifier) {
434
+ const row = this._getInstanceRow(identifier);
435
+ if (!row) {
436
+ this._throwError('DBInstanceNotFoundFault', `DB Instance not found: ${identifier}`, 404);
437
+ }
438
+ return this._formatInstance(row);
439
+ }
440
+
441
+ /**
442
+ * Lists all instances, optionally filtered.
443
+ * @param {Object} [filters] - Optional filters
444
+ * @returns {Object[]} Array of instance objects
445
+ */
446
+ listInstances(filters) {
447
+ if (filters && filters.DBInstanceIdentifier) {
448
+ const row = this._getInstanceRow(filters.DBInstanceIdentifier);
449
+ if (!row) {
450
+ this._throwError('DBInstanceNotFoundFault', `DB Instance not found: ${filters.DBInstanceIdentifier}`, 404);
451
+ }
452
+ return [this._formatInstance(row)];
453
+ }
454
+
455
+ if (filters && filters.DBClusterIdentifier) {
456
+ const rows = this.metaDb.prepare(
457
+ 'SELECT * FROM instances WHERE db_cluster_identifier = ?'
458
+ ).all(filters.DBClusterIdentifier);
459
+ return rows.map(row => this._formatInstance(row));
460
+ }
461
+
462
+ const rows = this.metaDb.prepare('SELECT * FROM instances').all();
463
+ return rows.map(row => this._formatInstance(row));
464
+ }
465
+
466
+ // ─── Reset ──────────────────────────────────────────────────────────────────
467
+
468
+ /**
469
+ * Clears all metadata (clusters, instances, databases).
470
+ */
471
+ reset() {
472
+ this.metaDb.prepare('DELETE FROM databases').run();
473
+ this.metaDb.prepare('DELETE FROM instances').run();
474
+ this.metaDb.prepare('DELETE FROM clusters').run();
475
+ logger.debug('[Aurora ClusterManager] All metadata cleared');
476
+ }
477
+
478
+ /**
479
+ * Persists all clusters and instances to __aurora__.json via LocalStore.
480
+ * No-op if store is null.
481
+ * @private
482
+ */
483
+ _persistClusters() {
484
+ if (!this.store) return;
485
+
486
+ const clusters = this.listClusters();
487
+ const clustersObj = {};
488
+
489
+ for (const cluster of clusters) {
490
+ const instances = this.listInstances({ DBClusterIdentifier: cluster.DBClusterIdentifier });
491
+
492
+ clustersObj[cluster.DBClusterIdentifier] = {
493
+ DBClusterIdentifier: cluster.DBClusterIdentifier,
494
+ DBClusterArn: cluster.DBClusterArn,
495
+ Engine: cluster.Engine,
496
+ EngineVersion: cluster.EngineVersion,
497
+ Status: cluster.Status,
498
+ Endpoint: cluster.Endpoint,
499
+ ReaderEndpoint: cluster.ReaderEndpoint,
500
+ Port: cluster.Port,
501
+ DatabaseName: cluster.DatabaseName,
502
+ MasterUsername: cluster.MasterUsername,
503
+ StorageEncrypted: cluster.StorageEncrypted,
504
+ KmsKeyId: cluster.KmsKeyId,
505
+ DbClusterResourceId: cluster.DbClusterResourceId,
506
+ AvailabilityZones: cluster.AvailabilityZones,
507
+ CreatedAt: cluster.CreatedAt,
508
+ UpdatedAt: cluster.UpdatedAt,
509
+ instances: instances.map(inst => ({
510
+ DBInstanceIdentifier: inst.DBInstanceIdentifier,
511
+ DBInstanceArn: inst.DBInstanceArn,
512
+ DBInstanceClass: inst.DBInstanceClass,
513
+ Status: inst.DBInstanceStatus,
514
+ CreatedAt: inst.CreatedAt,
515
+ UpdatedAt: inst.UpdatedAt
516
+ }))
517
+ };
518
+ }
519
+
520
+ this.store.write('__aurora__', { clusters: clustersObj });
521
+ logger.debug('[Aurora ClusterManager] Persisted clusters to __aurora__.json');
522
+ }
523
+
524
+ /**
525
+ * Loads clusters and instances from __aurora__.json if it exists.
526
+ * Idempotent: skips clusters/instances that already exist in metadata.db.
527
+ * No-op if store is null.
528
+ */
529
+ loadClusters() {
530
+ if (!this.store) return;
531
+
532
+ const data = this.store.read('__aurora__');
533
+ if (!data || !data.clusters) {
534
+ // File doesn't exist or is malformed
535
+ return;
536
+ }
537
+
538
+ let loadedCount = 0;
539
+ const clusterIds = Object.keys(data.clusters);
540
+
541
+ for (const clusterId of clusterIds) {
542
+ const clusterData = data.clusters[clusterId];
543
+
544
+ // Check if cluster already exists (idempotent)
545
+ if (this._getClusterRow(clusterId)) {
546
+ continue;
547
+ }
548
+
549
+ // Insert cluster row
550
+ try {
551
+ const stmt = this.metaDb.prepare(`
552
+ INSERT OR IGNORE INTO clusters (
553
+ db_cluster_identifier, db_cluster_arn, engine, engine_version,
554
+ status, endpoint, reader_endpoint, port, database_name,
555
+ master_username, storage_encrypted, kms_key_id,
556
+ db_cluster_resource_id, availability_zones, created_at, updated_at
557
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
558
+ `);
559
+
560
+ stmt.run(
561
+ clusterData.DBClusterIdentifier,
562
+ clusterData.DBClusterArn,
563
+ clusterData.Engine,
564
+ clusterData.EngineVersion,
565
+ clusterData.Status,
566
+ clusterData.Endpoint,
567
+ clusterData.ReaderEndpoint,
568
+ clusterData.Port,
569
+ clusterData.DatabaseName || null,
570
+ clusterData.MasterUsername,
571
+ clusterData.StorageEncrypted ? 1 : 0,
572
+ clusterData.KmsKeyId || null,
573
+ clusterData.DbClusterResourceId,
574
+ JSON.stringify(clusterData.AvailabilityZones || []),
575
+ clusterData.CreatedAt,
576
+ clusterData.UpdatedAt
577
+ );
578
+
579
+ loadedCount++;
580
+
581
+ // Insert instances for this cluster
582
+ if (clusterData.instances && Array.isArray(clusterData.instances)) {
583
+ const instStmt = this.metaDb.prepare(`
584
+ INSERT OR IGNORE INTO instances (
585
+ db_instance_identifier, db_instance_arn, db_cluster_identifier,
586
+ db_instance_class, engine, engine_version, db_instance_status,
587
+ endpoint_address, endpoint_port, is_cluster_writer, availability_zone,
588
+ created_at, updated_at
589
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
590
+ `);
591
+
592
+ // Infer engine info from cluster
593
+ for (let i = 0; i < clusterData.instances.length; i++) {
594
+ const inst = clusterData.instances[i];
595
+ instStmt.run(
596
+ inst.DBInstanceIdentifier,
597
+ inst.DBInstanceArn,
598
+ clusterId,
599
+ inst.DBInstanceClass,
600
+ clusterData.Engine,
601
+ clusterData.EngineVersion,
602
+ inst.Status,
603
+ `${inst.DBInstanceIdentifier}.local.us-east-1.rds.amazonaws.com`,
604
+ clusterData.Port,
605
+ i === 0 ? 1 : 0, // First instance is writer
606
+ 'us-east-1a',
607
+ inst.CreatedAt,
608
+ inst.UpdatedAt
609
+ );
610
+ }
611
+ }
612
+ } catch (e) {
613
+ logger.debug(`[Aurora ClusterManager] Failed to load cluster ${clusterId}: ${e.message}`);
614
+ }
615
+ }
616
+
617
+ if (loadedCount > 0) {
618
+ logger.debug(`[Aurora ClusterManager] Loaded ${loadedCount} clusters from __aurora__.json`);
619
+ }
620
+ }
621
+
622
+ // ─── Private Helpers ────────────────────────────────────────────────────────
623
+
624
+ /**
625
+ * Validates a cluster or instance identifier.
626
+ * @private
627
+ */
628
+ _validateIdentifier(identifier, fieldName) {
629
+ if (!identifier || !IDENTIFIER_PATTERN.test(identifier)) {
630
+ this._throwError(
631
+ 'InvalidParameterValue',
632
+ `Invalid ${fieldName}: '${identifier}'. Must be 1-63 characters, start with a letter, and contain only lowercase letters, numbers, and hyphens.`,
633
+ 400
634
+ );
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Gets a raw cluster row from the database.
640
+ * @private
641
+ */
642
+ _getClusterRow(identifier) {
643
+ return this.metaDb.prepare('SELECT * FROM clusters WHERE db_cluster_identifier = ?').get(identifier);
644
+ }
645
+
646
+ /**
647
+ * Gets a raw instance row from the database.
648
+ * @private
649
+ */
650
+ _getInstanceRow(identifier) {
651
+ return this.metaDb.prepare('SELECT * FROM instances WHERE db_instance_identifier = ?').get(identifier);
652
+ }
653
+
654
+ /**
655
+ * Formats a raw cluster row into the AuroraCluster object shape.
656
+ * @private
657
+ */
658
+ _formatCluster(row) {
659
+ if (!row) return null;
660
+
661
+ // Get associated instances
662
+ const instances = this.metaDb.prepare(
663
+ 'SELECT db_instance_identifier, is_cluster_writer FROM instances WHERE db_cluster_identifier = ?'
664
+ ).all(row.db_cluster_identifier);
665
+
666
+ const members = instances.map(inst => ({
667
+ DBInstanceIdentifier: inst.db_instance_identifier,
668
+ IsClusterWriter: inst.is_cluster_writer === 1
669
+ }));
670
+
671
+ return {
672
+ DBClusterIdentifier: row.db_cluster_identifier,
673
+ DBClusterArn: row.db_cluster_arn,
674
+ Engine: row.engine,
675
+ EngineVersion: row.engine_version,
676
+ Status: row.status,
677
+ Endpoint: row.endpoint,
678
+ ReaderEndpoint: row.reader_endpoint,
679
+ Port: row.port,
680
+ DatabaseName: row.database_name,
681
+ MasterUsername: row.master_username,
682
+ StorageEncrypted: row.storage_encrypted === 1,
683
+ KmsKeyId: row.kms_key_id,
684
+ DbClusterResourceId: row.db_cluster_resource_id,
685
+ AvailabilityZones: row.availability_zones ? JSON.parse(row.availability_zones) : [],
686
+ DBClusterMembers: members,
687
+ CreatedAt: row.created_at,
688
+ UpdatedAt: row.updated_at
689
+ };
690
+ }
691
+
692
+ /**
693
+ * Formats a raw instance row into the AuroraInstance object shape.
694
+ * @private
695
+ */
696
+ _formatInstance(row) {
697
+ if (!row) return null;
698
+
699
+ return {
700
+ DBInstanceIdentifier: row.db_instance_identifier,
701
+ DBInstanceArn: row.db_instance_arn,
702
+ DBClusterIdentifier: row.db_cluster_identifier,
703
+ DBInstanceClass: row.db_instance_class,
704
+ Engine: row.engine,
705
+ EngineVersion: row.engine_version,
706
+ DBInstanceStatus: row.db_instance_status,
707
+ Endpoint: {
708
+ Address: row.endpoint_address,
709
+ Port: row.endpoint_port,
710
+ HostedZoneId: 'Z1234567890'
711
+ },
712
+ IsClusterWriter: row.is_cluster_writer === 1,
713
+ AvailabilityZone: row.availability_zone,
714
+ CreatedAt: row.created_at,
715
+ UpdatedAt: row.updated_at
716
+ };
717
+ }
718
+
719
+ /**
720
+ * Throws a structured error matching AWS error format.
721
+ * @private
722
+ */
723
+ _throwError(code, message, statusCode) {
724
+ const error = new Error(message);
725
+ error.code = code;
726
+ error.statusCode = statusCode;
727
+ throw error;
728
+ }
729
+ }
730
+
731
+ module.exports = { ClusterManager };