@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.
- package/README.md +662 -658
- package/package.json +10 -3
- 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/index.js +26 -0
- package/src/services/lambda/server.js +12 -1
- package/src/services/lambda/simulator.js +4 -0
- 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,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kinesis Data Streams Simulator
|
|
3
|
+
* Simulador de Kinesis Data Streams com suporte a shards, records, e consumidores
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { Stream, Shard } = require('./models/stream');
|
|
7
|
+
const Record = require('./models/record');
|
|
8
|
+
const LocalStore = require('../../utils/local-store');
|
|
9
|
+
const logger = require('../../utils/logger');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { CloudTrailAudit } = require('../../utils/cloudtrail-audit');
|
|
12
|
+
const { v4: uuidv4 } = require('uuid');
|
|
13
|
+
|
|
14
|
+
class KinesisDataStreamsSimulator {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
const dataDir = process.env.AWS_LOCAL_SIMULATOR_DATA_DIR || config.dataDir || './.aws-local-simulator-data';
|
|
18
|
+
this.dataDir = path.join(dataDir, 'kinesis-data-streams');
|
|
19
|
+
|
|
20
|
+
this.store = new LocalStore(this.dataDir);
|
|
21
|
+
this.streams = new Map();
|
|
22
|
+
this.consumers = new Map();
|
|
23
|
+
this.audit = new CloudTrailAudit('kinesis.amazonaws.com');
|
|
24
|
+
|
|
25
|
+
// Mutexes por stream para operações thread-safe
|
|
26
|
+
this._streamLocks = new Map();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Executa operação com lock exclusivo por stream
|
|
31
|
+
*/
|
|
32
|
+
_withStreamLock(streamName, fn) {
|
|
33
|
+
const prev = this._streamLocks.get(streamName) || Promise.resolve();
|
|
34
|
+
const next = prev.then(() => fn());
|
|
35
|
+
this._streamLocks.set(streamName, next.catch(() => {}));
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async initialize() {
|
|
40
|
+
logger.debug('Inicializando Kinesis Data Streams Simulator...');
|
|
41
|
+
this.loadStreams();
|
|
42
|
+
|
|
43
|
+
// Cria streams da configuração se não existirem
|
|
44
|
+
if (this.config['kinesis-data-streams']?.streams) {
|
|
45
|
+
for (const streamConfig of this.config['kinesis-data-streams'].streams) {
|
|
46
|
+
if (!this.streams.has(streamConfig.name)) {
|
|
47
|
+
this.createStream(streamConfig.name, streamConfig);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
logger.debug(`✅ Kinesis Data Streams Simulator inicializado com ${this.streams.size} streams`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Carrega streams persistidos
|
|
57
|
+
*/
|
|
58
|
+
loadStreams() {
|
|
59
|
+
const metadata = this.store.read('__streams__');
|
|
60
|
+
if (!metadata) return;
|
|
61
|
+
|
|
62
|
+
for (const [streamName, streamData] of Object.entries(metadata)) {
|
|
63
|
+
const stream = new Stream(streamName, streamData);
|
|
64
|
+
stream.shards = streamData.shards.map(shardData => {
|
|
65
|
+
const shard = new Shard(0, streamName);
|
|
66
|
+
shard.shardId = shardData.shardId;
|
|
67
|
+
shard.shardStatus = shardData.shardStatus;
|
|
68
|
+
shard.records = (streamData.records || []).filter(r => r.shardId === shard.shardId);
|
|
69
|
+
shard.recordCount = shard.records.length;
|
|
70
|
+
shard.lastSequenceNumber = shardData.lastSequenceNumber || 0;
|
|
71
|
+
return shard;
|
|
72
|
+
});
|
|
73
|
+
this.streams.set(streamName, stream);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logger.debug(`Carregados ${this.streams.size} streams`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Persiste metadados de streams
|
|
81
|
+
*/
|
|
82
|
+
persistStreams() {
|
|
83
|
+
const metadata = {};
|
|
84
|
+
|
|
85
|
+
for (const [streamName, stream] of this.streams) {
|
|
86
|
+
metadata[streamName] = {
|
|
87
|
+
streamName: stream.streamName,
|
|
88
|
+
streamStatus: stream.streamStatus,
|
|
89
|
+
streamModeDetails: stream.streamModeDetails,
|
|
90
|
+
shardCount: stream.shardCount,
|
|
91
|
+
creationTimestamp: stream.creationTimestamp,
|
|
92
|
+
retentionPeriod: stream.retentionPeriod,
|
|
93
|
+
shards: stream.shards.map(s => ({
|
|
94
|
+
shardId: s.shardId,
|
|
95
|
+
shardStatus: s.shardStatus,
|
|
96
|
+
lastSequenceNumber: s.lastSequenceNumber
|
|
97
|
+
})),
|
|
98
|
+
records: Array.from(stream.shards).flatMap(s =>
|
|
99
|
+
s.records.map(r => ({
|
|
100
|
+
recordId: r.recordId,
|
|
101
|
+
shardId: s.shardId,
|
|
102
|
+
data: r.data,
|
|
103
|
+
partitionKey: r.partitionKey,
|
|
104
|
+
sequenceNumber: r.sequenceNumber,
|
|
105
|
+
timestamp: r.timestamp
|
|
106
|
+
}))
|
|
107
|
+
)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.store.write('__streams__', metadata);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Cria um novo stream
|
|
116
|
+
*/
|
|
117
|
+
createStream(streamName, config = {}) {
|
|
118
|
+
if (this.streams.has(streamName)) {
|
|
119
|
+
throw new Error(`Stream '${streamName}' already exists`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const stream = new Stream(streamName, config);
|
|
123
|
+
this.streams.set(streamName, stream);
|
|
124
|
+
this.persistStreams();
|
|
125
|
+
|
|
126
|
+
this.audit.record({
|
|
127
|
+
eventName: 'CreateStream',
|
|
128
|
+
readOnly: false,
|
|
129
|
+
resources: [{
|
|
130
|
+
ARN: stream.streamArn,
|
|
131
|
+
type: 'AWS::Kinesis::Stream'
|
|
132
|
+
}],
|
|
133
|
+
requestParameters: { streamName, shardCount: stream.shardCount }
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
logger.info(`✅ Stream criado: ${streamName}`);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
StreamDescription: stream.describe().StreamDescription
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Descreve um stream
|
|
145
|
+
*/
|
|
146
|
+
describeStream(streamName) {
|
|
147
|
+
const stream = this.streams.get(streamName);
|
|
148
|
+
if (!stream) {
|
|
149
|
+
throw new Error(`Stream '${streamName}' not found`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return stream.describe();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Lista todos os streams
|
|
157
|
+
*/
|
|
158
|
+
listStreams(params = {}) {
|
|
159
|
+
const { Limit = 100, ExclusiveStartStreamName } = params;
|
|
160
|
+
const streamNames = Array.from(this.streams.keys());
|
|
161
|
+
|
|
162
|
+
let startIndex = 0;
|
|
163
|
+
if (ExclusiveStartStreamName) {
|
|
164
|
+
const index = streamNames.indexOf(ExclusiveStartStreamName);
|
|
165
|
+
if (index !== -1) startIndex = index + 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const result = streamNames.slice(startIndex, startIndex + Limit);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
StreamNames: result,
|
|
172
|
+
HasMoreStreams: result.length === Limit,
|
|
173
|
+
StreamCreationTimestamps: result.map(name => {
|
|
174
|
+
const stream = this.streams.get(name);
|
|
175
|
+
return stream?.creationTimestamp || new Date().toISOString();
|
|
176
|
+
}),
|
|
177
|
+
StreamStatuses: result.map(() => 'ACTIVE')
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Deleta um stream
|
|
183
|
+
*/
|
|
184
|
+
deleteStream(streamName) {
|
|
185
|
+
const stream = this.streams.get(streamName);
|
|
186
|
+
if (!stream) {
|
|
187
|
+
throw new Error(`Stream '${streamName}' not found`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.streams.delete(streamName);
|
|
191
|
+
this.persistStreams();
|
|
192
|
+
|
|
193
|
+
// Remove consumidores do stream
|
|
194
|
+
for (const [consumerArn] of this.consumers) {
|
|
195
|
+
if (consumerArn.includes(streamName)) {
|
|
196
|
+
this.consumers.delete(consumerArn);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.audit.record({
|
|
201
|
+
eventName: 'DeleteStream',
|
|
202
|
+
readOnly: false,
|
|
203
|
+
resources: [{
|
|
204
|
+
ARN: stream.streamArn,
|
|
205
|
+
type: 'AWS::Kinesis::Stream'
|
|
206
|
+
}],
|
|
207
|
+
requestParameters: { streamName }
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
logger.info(`✅ Stream deletado: ${streamName}`);
|
|
211
|
+
|
|
212
|
+
return {};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Coloca um record no stream
|
|
217
|
+
*/
|
|
218
|
+
async putRecord(streamName, data, partitionKey, config = {}) {
|
|
219
|
+
const stream = this.streams.get(streamName);
|
|
220
|
+
if (!stream) {
|
|
221
|
+
throw new Error(`Stream '${streamName}' not found`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return this._withStreamLock(streamName, () => {
|
|
225
|
+
const shard = stream.selectShardByPartitionKey(partitionKey);
|
|
226
|
+
if (!shard) {
|
|
227
|
+
throw new Error(`No shards available in stream '${streamName}'`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const record = new Record(data, partitionKey, {
|
|
231
|
+
shardId: shard.shardId,
|
|
232
|
+
encryptionType: config.encryptionType || 'NONE'
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
shard.addRecord(record);
|
|
236
|
+
stream.recordPutRecord(data);
|
|
237
|
+
this.persistStreams();
|
|
238
|
+
|
|
239
|
+
this.audit.record({
|
|
240
|
+
eventName: 'PutRecord',
|
|
241
|
+
readOnly: false,
|
|
242
|
+
isDataEvent: true,
|
|
243
|
+
resources: [{
|
|
244
|
+
ARN: stream.streamArn,
|
|
245
|
+
type: 'AWS::Kinesis::Stream'
|
|
246
|
+
}],
|
|
247
|
+
requestParameters: { streamName, partitionKey }
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
ShardId: shard.shardId,
|
|
252
|
+
SequenceNumber: record.sequenceNumber,
|
|
253
|
+
EncryptionType: record.encryptionType
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Coloca múltiplos records no stream
|
|
260
|
+
*/
|
|
261
|
+
async putRecords(streamName, records) {
|
|
262
|
+
const stream = this.streams.get(streamName);
|
|
263
|
+
if (!stream) {
|
|
264
|
+
throw new Error(`Stream '${streamName}' not found`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return this._withStreamLock(streamName, () => {
|
|
268
|
+
const results = [];
|
|
269
|
+
let failedRecordCount = 0;
|
|
270
|
+
|
|
271
|
+
for (const recordData of records) {
|
|
272
|
+
try {
|
|
273
|
+
const shard = stream.selectShardByPartitionKey(recordData.PartitionKey);
|
|
274
|
+
const record = new Record(recordData.Data, recordData.PartitionKey, {
|
|
275
|
+
shardId: shard.shardId
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
shard.addRecord(record);
|
|
279
|
+
stream.recordPutRecord(recordData.Data);
|
|
280
|
+
|
|
281
|
+
results.push({
|
|
282
|
+
ShardId: shard.shardId,
|
|
283
|
+
SequenceNumber: record.sequenceNumber
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
failedRecordCount++;
|
|
287
|
+
results.push({
|
|
288
|
+
ErrorCode: 'InternalFailure',
|
|
289
|
+
ErrorMessage: error.message
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this.persistStreams();
|
|
295
|
+
|
|
296
|
+
this.audit.record({
|
|
297
|
+
eventName: 'PutRecords',
|
|
298
|
+
readOnly: false,
|
|
299
|
+
isDataEvent: true,
|
|
300
|
+
resources: [{
|
|
301
|
+
ARN: stream.streamArn,
|
|
302
|
+
type: 'AWS::Kinesis::Stream'
|
|
303
|
+
}],
|
|
304
|
+
requestParameters: { streamName, recordCount: records.length }
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
FailedRecordCount: failedRecordCount,
|
|
309
|
+
Records: results
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Obtém records de um shard
|
|
316
|
+
*/
|
|
317
|
+
getRecords(shardIterator, config = {}) {
|
|
318
|
+
const { Limit = 100 } = config;
|
|
319
|
+
|
|
320
|
+
// Parse iterator (formato simplificado: streamName:shardId:startSeqNum)
|
|
321
|
+
const [streamName, shardId, startSeqNum] = shardIterator.split(':');
|
|
322
|
+
|
|
323
|
+
const stream = this.streams.get(streamName);
|
|
324
|
+
if (!stream) {
|
|
325
|
+
throw new Error(`Stream '${streamName}' not found`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const shard = stream.getShard(shardId);
|
|
329
|
+
if (!shard) {
|
|
330
|
+
throw new Error(`Shard '${shardId}' not found in stream '${streamName}'`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const records = shard.getRecords(startSeqNum, Limit);
|
|
334
|
+
const lastSeqNum = shard.getLastSequenceNumber();
|
|
335
|
+
|
|
336
|
+
// Gera novo iterator
|
|
337
|
+
const nextSeqNum = records.length > 0
|
|
338
|
+
? parseInt(records[records.length - 1].sequenceNumber)
|
|
339
|
+
: parseInt(startSeqNum || 0);
|
|
340
|
+
|
|
341
|
+
const nextShardIterator = `${streamName}:${shardId}:${nextSeqNum}`;
|
|
342
|
+
|
|
343
|
+
this.audit.record({
|
|
344
|
+
eventName: 'GetRecords',
|
|
345
|
+
readOnly: true,
|
|
346
|
+
isDataEvent: true,
|
|
347
|
+
resources: [{
|
|
348
|
+
ARN: stream.streamArn,
|
|
349
|
+
type: 'AWS::Kinesis::Stream'
|
|
350
|
+
}],
|
|
351
|
+
requestParameters: { streamName, shardId }
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
Records: records.map(r => r.toJSON()),
|
|
356
|
+
NextShardIterator: nextShardIterator,
|
|
357
|
+
MillisBehindLatest: 0
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Obtém shard iterator
|
|
363
|
+
*/
|
|
364
|
+
getSharIterator(streamName, shardId, shardIteratorType, config = {}) {
|
|
365
|
+
const stream = this.streams.get(streamName);
|
|
366
|
+
if (!stream) {
|
|
367
|
+
throw new Error(`Stream '${streamName}' not found`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const shard = stream.getShard(shardId);
|
|
371
|
+
if (!shard) {
|
|
372
|
+
throw new Error(`Shard '${shardId}' not found in stream '${streamName}'`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let startSeqNum = '0';
|
|
376
|
+
|
|
377
|
+
switch (shardIteratorType) {
|
|
378
|
+
case 'TRIM_HORIZON':
|
|
379
|
+
startSeqNum = '0';
|
|
380
|
+
break;
|
|
381
|
+
case 'LATEST':
|
|
382
|
+
startSeqNum = shard.getLastSequenceNumber();
|
|
383
|
+
break;
|
|
384
|
+
case 'AT_SEQUENCE_NUMBER':
|
|
385
|
+
startSeqNum = config.StartingSequenceNumber || '0';
|
|
386
|
+
break;
|
|
387
|
+
case 'AFTER_SEQUENCE_NUMBER':
|
|
388
|
+
startSeqNum = (parseInt(config.StartingSequenceNumber || '0') + 1).toString();
|
|
389
|
+
break;
|
|
390
|
+
case 'AT_TIMESTAMP':
|
|
391
|
+
// Implementação simplificada
|
|
392
|
+
startSeqNum = '0';
|
|
393
|
+
break;
|
|
394
|
+
default:
|
|
395
|
+
throw new Error(`Invalid ShardIteratorType: ${shardIteratorType}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const shardIterator = `${streamName}:${shardId}:${startSeqNum}`;
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
ShardIterator: shardIterator
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Registra um consumidor
|
|
407
|
+
*/
|
|
408
|
+
registerStreamConsumer(streamArn, consumerName) {
|
|
409
|
+
const streamName = streamArn.split(':').pop();
|
|
410
|
+
const stream = this.streams.get(streamName);
|
|
411
|
+
|
|
412
|
+
if (!stream) {
|
|
413
|
+
throw new Error(`Stream not found: ${streamName}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const consumerArn = `${streamArn}/consumer/${consumerName}:${uuidv4().substring(0, 8)}`;
|
|
417
|
+
const consumer = stream.registerConsumer(consumerArn, consumerName);
|
|
418
|
+
|
|
419
|
+
this.consumers.set(consumerArn, {
|
|
420
|
+
consumerArn,
|
|
421
|
+
consumerName,
|
|
422
|
+
streamArn,
|
|
423
|
+
streamName,
|
|
424
|
+
creationTimestamp: new Date().toISOString(),
|
|
425
|
+
consumerStatus: 'ACTIVE'
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
logger.info(`✅ Consumidor registrado: ${consumerName} para stream ${streamName}`);
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
Consumer: {
|
|
432
|
+
ConsumerName: consumerName,
|
|
433
|
+
ConsumerArn: consumerArn,
|
|
434
|
+
ConsumerStatus: 'ACTIVE',
|
|
435
|
+
ConsumerCreationTimestamp: new Date().toISOString()
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Descreve um consumidor
|
|
442
|
+
*/
|
|
443
|
+
describeStreamConsumer(consumerArn) {
|
|
444
|
+
const consumer = this.consumers.get(consumerArn);
|
|
445
|
+
if (!consumer) {
|
|
446
|
+
throw new Error(`Consumer not found: ${consumerArn}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
ConsumerDescription: {
|
|
451
|
+
ConsumerName: consumer.consumerName,
|
|
452
|
+
ConsumerArn: consumer.consumerArn,
|
|
453
|
+
ConsumerStatus: consumer.consumerStatus,
|
|
454
|
+
ConsumerCreationTimestamp: consumer.creationTimestamp
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Lista consumidores
|
|
461
|
+
*/
|
|
462
|
+
listStreamConsumers(streamArn) {
|
|
463
|
+
const streamName = streamArn.split(':').pop();
|
|
464
|
+
const consumers = Array.from(this.consumers.values())
|
|
465
|
+
.filter(c => c.streamName === streamName);
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
Consumers: consumers.map(c => ({
|
|
469
|
+
ConsumerName: c.consumerName,
|
|
470
|
+
ConsumerArn: c.consumerArn,
|
|
471
|
+
ConsumerStatus: c.consumerStatus,
|
|
472
|
+
ConsumerCreationTimestamp: c.creationTimestamp
|
|
473
|
+
}))
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Obtém estatísticas do simulator
|
|
479
|
+
*/
|
|
480
|
+
getStats() {
|
|
481
|
+
const stats = {
|
|
482
|
+
streamsCount: this.streams.size,
|
|
483
|
+
consumersCount: this.consumers.size,
|
|
484
|
+
totalRecords: 0,
|
|
485
|
+
totalShards: 0,
|
|
486
|
+
streams: []
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
for (const [streamName, stream] of this.streams) {
|
|
490
|
+
const streamStats = stream.toJSON();
|
|
491
|
+
stats.totalRecords += streamStats.recordCount;
|
|
492
|
+
stats.totalShards += stream.shards.length;
|
|
493
|
+
stats.streams.push({
|
|
494
|
+
...streamStats,
|
|
495
|
+
shardCount: stream.shards.length
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return stats;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Reset - limpa todos os dados
|
|
504
|
+
*/
|
|
505
|
+
async reset() {
|
|
506
|
+
this.streams.clear();
|
|
507
|
+
this.consumers.clear();
|
|
508
|
+
this.store.write('__streams__', {});
|
|
509
|
+
|
|
510
|
+
logger.info('✅ Kinesis Data Streams Simulator resetado');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
module.exports = KinesisDataStreamsSimulator;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kinesis Data Streams Store - Persistência de streams e records em JSON
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const logger = require('../../utils/logger');
|
|
8
|
+
|
|
9
|
+
class KinesisDataStreamsStore {
|
|
10
|
+
constructor(dataDir) {
|
|
11
|
+
this.dataDir = dataDir;
|
|
12
|
+
this.streamsDir = path.join(dataDir, 'kinesis-data-streams');
|
|
13
|
+
this.metadataFile = path.join(this.streamsDir, '__streams__.json');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(this.streamsDir)) {
|
|
16
|
+
fs.mkdirSync(this.streamsDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Carrega metadados de todos os streams
|
|
22
|
+
*/
|
|
23
|
+
loadMetadata() {
|
|
24
|
+
if (fs.existsSync(this.metadataFile)) {
|
|
25
|
+
try {
|
|
26
|
+
const data = fs.readFileSync(this.metadataFile, 'utf8');
|
|
27
|
+
return JSON.parse(data);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logger.warn(`Erro ao ler ${this.metadataFile}: ${error.message}`);
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Salva metadados de todos os streams
|
|
38
|
+
*/
|
|
39
|
+
saveMetadata(metadata) {
|
|
40
|
+
try {
|
|
41
|
+
fs.writeFileSync(this.metadataFile, JSON.stringify(metadata, null, 2), 'utf8');
|
|
42
|
+
logger.debug(`Metadados de streams salvos em ${this.metadataFile}`);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
logger.error(`Erro ao salvar ${this.metadataFile}: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Obtém caminho do arquivo de stream
|
|
50
|
+
*/
|
|
51
|
+
getStreamPath(streamName) {
|
|
52
|
+
return path.join(this.streamsDir, `${streamName}.json`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Carrega records de um stream
|
|
57
|
+
*/
|
|
58
|
+
loadStreamRecords(streamName) {
|
|
59
|
+
const streamPath = this.getStreamPath(streamName);
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(streamPath)) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const data = fs.readFileSync(streamPath, 'utf8');
|
|
67
|
+
return JSON.parse(data);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
logger.warn(`Erro ao ler stream ${streamName}: ${error.message}`);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Salva records de um stream
|
|
76
|
+
*/
|
|
77
|
+
saveStreamRecords(streamName, records) {
|
|
78
|
+
try {
|
|
79
|
+
const streamPath = this.getStreamPath(streamName);
|
|
80
|
+
fs.writeFileSync(streamPath, JSON.stringify(records, null, 2), 'utf8');
|
|
81
|
+
logger.debug(`${records.length} records salvos no stream ${streamName}`);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
logger.error(`Erro ao salvar stream ${streamName}: ${error.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Adiciona record ao stream
|
|
89
|
+
*/
|
|
90
|
+
addRecord(streamName, record) {
|
|
91
|
+
const records = this.loadStreamRecords(streamName);
|
|
92
|
+
records.push(record);
|
|
93
|
+
this.saveStreamRecords(streamName, records);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Adiciona múltiplos records
|
|
98
|
+
*/
|
|
99
|
+
addRecords(streamName, newRecords) {
|
|
100
|
+
const records = this.loadStreamRecords(streamName);
|
|
101
|
+
records.push(...newRecords);
|
|
102
|
+
this.saveStreamRecords(streamName, records);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Remove record do stream
|
|
107
|
+
*/
|
|
108
|
+
removeRecord(streamName, recordId) {
|
|
109
|
+
const records = this.loadStreamRecords(streamName);
|
|
110
|
+
const filtered = records.filter(r => r.recordId !== recordId);
|
|
111
|
+
|
|
112
|
+
if (filtered.length < records.length) {
|
|
113
|
+
this.saveStreamRecords(streamName, filtered);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Deleta stream completamente
|
|
122
|
+
*/
|
|
123
|
+
deleteStream(streamName) {
|
|
124
|
+
try {
|
|
125
|
+
const streamPath = this.getStreamPath(streamName);
|
|
126
|
+
if (fs.existsSync(streamPath)) {
|
|
127
|
+
fs.unlinkSync(streamPath);
|
|
128
|
+
logger.debug(`Stream ${streamName} deletado`);
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger.error(`Erro ao deletar stream ${streamName}: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Calcula tamanho total do stream em bytes
|
|
137
|
+
*/
|
|
138
|
+
getStreamSize(streamName) {
|
|
139
|
+
const records = this.loadStreamRecords(streamName);
|
|
140
|
+
return records.reduce((sum, record) => sum + Buffer.byteLength(record.data), 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Limpa todos os streams
|
|
145
|
+
*/
|
|
146
|
+
clear() {
|
|
147
|
+
try {
|
|
148
|
+
if (fs.existsSync(this.streamsDir)) {
|
|
149
|
+
const files = fs.readdirSync(this.streamsDir);
|
|
150
|
+
files.forEach(file => {
|
|
151
|
+
if (file.endsWith('.json')) {
|
|
152
|
+
fs.unlinkSync(path.join(this.streamsDir, file));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
logger.debug('Todos os streams foram limpos');
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error(`Erro ao limpar streams: ${error.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = KinesisDataStreamsStore;
|