@gugananuvem/aws-local-simulator 1.0.34 → 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.
- package/README.md +662 -658
- package/package.json +11 -4
- package/src/config/default-config.js +71 -0
- package/src/config/env-loader.js +4 -2
- package/src/middleware/authorization.js +271 -0
- package/src/middleware/cloudtrail-logger.js +333 -0
- package/src/middleware/error-handler.js +138 -0
- package/src/middleware/index.js +79 -0
- package/src/middleware/policy-enforcement.js +407 -0
- package/src/server.js +74 -4
- package/src/services/acm/index.js +66 -0
- package/src/services/acm/models/certificate.js +202 -0
- package/src/services/acm/server.js +224 -0
- package/src/services/acm/simulator.js +293 -0
- package/src/services/alb/index.js +104 -0
- package/src/services/alb/models/loadBalancer.js +88 -0
- package/src/services/alb/models/targetGroup.js +128 -0
- package/src/services/alb/server.js +160 -0
- package/src/services/alb/simulator.js +262 -0
- package/src/services/apigateway/index.js +22 -0
- package/src/services/apigateway/server.js +247 -35
- package/src/services/apigateway/simulator.js +780 -26
- package/src/services/asg/index.js +100 -0
- package/src/services/asg/models/autoScalingGroup.js +110 -0
- package/src/services/asg/models/launchTemplate.js +41 -0
- package/src/services/asg/models/scalingPolicy.js +51 -0
- package/src/services/asg/server.js +236 -0
- package/src/services/asg/simulator.js +410 -0
- package/src/services/aurora/cluster-manager.js +731 -0
- package/src/services/aurora/data-api.js +514 -0
- package/src/services/aurora/db-engine.js +270 -0
- package/src/services/aurora/index.js +159 -0
- package/src/services/aurora/rds-api.js +189 -0
- package/src/services/aurora/server.js +213 -0
- package/src/services/aurora/sql-translator.js +334 -0
- package/src/services/cloudformation/index.js +2 -2
- package/src/services/cloudformation/server.js +1 -0
- package/src/services/cloudfront/cache-engine.js +225 -0
- package/src/services/cloudfront/geo-restrictions.js +377 -0
- package/src/services/cloudfront/index.js +220 -0
- package/src/services/cloudfront/models/behavior.js +158 -0
- package/src/services/cloudfront/models/distribution.js +284 -0
- package/src/services/cloudfront/models/index.js +99 -0
- package/src/services/cloudfront/models/invalidation.js +109 -0
- package/src/services/cloudfront/models/origin.js +155 -0
- package/src/services/cloudfront/origin-router.js +552 -0
- package/src/services/cloudfront/origin-validator.js +398 -0
- package/src/services/cloudfront/server.js +512 -0
- package/src/services/cloudfront/signed-url-generator.js +377 -0
- package/src/services/cloudfront/simulator.js +425 -0
- package/src/services/cloudfront/store.js +326 -0
- package/src/services/cloudshell/env-builder.js +27 -0
- package/src/services/cloudshell/index.js +48 -0
- package/src/services/cloudshell/routes.js +60 -0
- package/src/services/cloudshell/session-manager.js +226 -0
- package/src/services/cloudshell/static/.gitkeep +0 -0
- package/src/services/cloudshell/static/index.html +406 -0
- package/src/services/cloudshell/websocket-handler.js +107 -0
- package/src/services/cloudtrail/index.js +2 -3
- package/src/services/cloudtrail/server.js +68 -0
- package/src/services/cognito/server.js +4 -1
- package/src/services/dynamodb/server.js +4 -1
- package/src/services/dynamodb/simulator.js +2 -1
- package/src/services/ec2/index.js +101 -0
- package/src/services/ec2/instanceServer.js +311 -0
- package/src/services/ec2/models/ami.js +39 -0
- package/src/services/ec2/models/instance.js +98 -0
- package/src/services/ec2/models/networkInterface.js +74 -0
- package/src/services/ec2/models/securityGroup.js +71 -0
- package/src/services/ec2/server.js +350 -0
- package/src/services/ec2/simulator.js +436 -0
- package/src/services/eventbridge/server.js +36 -0
- package/src/services/eventbridge/simulator.js +230 -45
- package/src/services/iam/index.js +65 -0
- package/src/services/iam/middleware.js +99 -0
- package/src/services/iam/server.js +331 -0
- package/src/services/iam/simulator.js +1432 -0
- package/src/services/index.js +28 -0
- package/src/services/kinesis-analytics/index.js +105 -0
- package/src/services/kinesis-analytics/models/application.js +325 -0
- package/src/services/kinesis-analytics/models/index.js +12 -0
- package/src/services/kinesis-analytics/server.js +250 -0
- package/src/services/kinesis-analytics/simulator.js +407 -0
- package/src/services/kinesis-analytics/store.js +176 -0
- package/src/services/kinesis-data-streams/index.js +106 -0
- package/src/services/kinesis-data-streams/models/index.js +12 -0
- package/src/services/kinesis-data-streams/models/record.js +70 -0
- package/src/services/kinesis-data-streams/models/stream.js +280 -0
- package/src/services/kinesis-data-streams/server.js +255 -0
- package/src/services/kinesis-data-streams/simulator.js +514 -0
- package/src/services/kinesis-data-streams/store.js +163 -0
- package/src/services/kinesis-firehose/index.js +106 -0
- package/src/services/kinesis-firehose/models/delivery-stream.js +265 -0
- package/src/services/kinesis-firehose/models/index.js +10 -0
- package/src/services/kinesis-firehose/server.js +215 -0
- package/src/services/kinesis-firehose/simulator.js +388 -0
- package/src/services/kinesis-firehose/store.js +163 -0
- package/src/services/kms/server.js +4 -1
- package/src/services/lambda/handler-loader.js +50 -5
- package/src/services/lambda/index.js +26 -0
- package/src/services/lambda/server.js +18 -12
- package/src/services/lambda/simulator.js +73 -17
- package/src/services/opensearch/index.js +105 -0
- package/src/services/opensearch/models/document.js +115 -0
- package/src/services/opensearch/models/index.js +73 -0
- package/src/services/opensearch/server.js +310 -0
- package/src/services/opensearch/simulator.js +605 -0
- package/src/services/opensearch/store.js +162 -0
- package/src/services/organizations/control-tower-simulator.js +321 -0
- package/src/services/organizations/control-tower-store.js +182 -0
- package/src/services/organizations/iam-integration.js +413 -0
- package/src/services/organizations/index.js +172 -0
- package/src/services/organizations/models/index.js +13 -0
- package/src/services/organizations/models/invitation.js +48 -0
- package/src/services/organizations/models/organization.js +566 -0
- package/src/services/organizations/models/role.js +244 -0
- package/src/services/organizations/models/scp.js +131 -0
- package/src/services/organizations/server.js +736 -0
- package/src/services/organizations/simulator.js +593 -0
- package/src/services/organizations/store.js +235 -0
- package/src/services/organizations/vpc-integration.js +453 -0
- package/src/services/route53/index.js +67 -0
- package/src/services/route53/models/healthCheck.js +101 -0
- package/src/services/route53/models/hostedZone.js +37 -0
- package/src/services/route53/models/resourceRecord.js +106 -0
- package/src/services/route53/server.js +841 -0
- package/src/services/route53/simulator.js +350 -0
- package/src/services/s3/index.js +62 -0
- package/src/services/s3/presigned-url-generator.js +337 -0
- package/src/services/s3/server.js +231 -0
- package/src/services/s3/simulator.js +551 -4
- package/src/services/sns/index.js +31 -0
- package/src/services/sns/server.js +9 -0
- package/src/services/sqs/index.js +38 -0
- package/src/services/sqs/server.js +57 -4
- package/src/services/sqs/simulator.js +423 -33
- package/src/services/sts/simulator.js +209 -17
- package/src/services/vpc/README.md +459 -0
- package/src/services/vpc/ec2ResponseFormatter.js +431 -0
- package/src/services/vpc/index.js +90 -0
- package/src/services/vpc/models/internetGateway.js +71 -0
- package/src/services/vpc/models/routeTable.js +160 -0
- package/src/services/vpc/models/subnet.js +48 -0
- package/src/services/vpc/models/vpc.js +37 -0
- package/src/services/vpc/models/vpcEndpoint.js +100 -0
- package/src/services/vpc/models/vpcGateway.js +134 -0
- package/src/services/vpc/server.js +836 -0
- package/src/services/vpc/simulator.js +613 -0
- package/src/utils/policy-cache.js +215 -0
- 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 };
|