@gugananuvem/aws-local-simulator 1.0.31 → 1.0.34

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 (79) hide show
  1. package/README.md +834 -834
  2. package/aws-config +153 -153
  3. package/bin/aws-local-simulator.js +63 -63
  4. package/package.json +3 -2
  5. package/src/config/config-loader.js +114 -114
  6. package/src/config/default-config.js +79 -79
  7. package/src/config/env-loader.js +68 -68
  8. package/src/index.js +146 -146
  9. package/src/index.mjs +123 -123
  10. package/src/server.js +463 -463
  11. package/src/services/apigateway/index.js +75 -75
  12. package/src/services/apigateway/server.js +607 -607
  13. package/src/services/apigateway/simulator.js +1405 -1405
  14. package/src/services/athena/index.js +75 -75
  15. package/src/services/athena/server.js +101 -101
  16. package/src/services/athena/simulador.js +998 -998
  17. package/src/services/athena/simulator.js +346 -346
  18. package/src/services/cloudformation/index.js +106 -106
  19. package/src/services/cloudformation/server.js +417 -417
  20. package/src/services/cloudformation/simulador.js +1020 -1020
  21. package/src/services/cloudtrail/index.js +84 -84
  22. package/src/services/cloudtrail/server.js +235 -235
  23. package/src/services/cloudtrail/simulador.js +719 -719
  24. package/src/services/cloudwatch/index.js +84 -84
  25. package/src/services/cloudwatch/server.js +366 -366
  26. package/src/services/cloudwatch/simulador.js +1173 -1173
  27. package/src/services/cognito/index.js +79 -79
  28. package/src/services/cognito/server.js +297 -297
  29. package/src/services/cognito/simulator.js +1992 -1761
  30. package/src/services/config/index.js +96 -96
  31. package/src/services/config/server.js +215 -215
  32. package/src/services/config/simulador.js +1260 -1260
  33. package/src/services/dynamodb/index.js +74 -74
  34. package/src/services/dynamodb/server.js +139 -139
  35. package/src/services/dynamodb/simulator.js +1005 -982
  36. package/src/services/dynamodb/sqlite-store.js +722 -0
  37. package/src/services/ecs/index.js +65 -65
  38. package/src/services/ecs/server.js +235 -235
  39. package/src/services/ecs/simulator.js +844 -844
  40. package/src/services/eventbridge/index.js +89 -89
  41. package/src/services/eventbridge/server.js +209 -209
  42. package/src/services/eventbridge/simulator.js +684 -684
  43. package/src/services/index.js +45 -45
  44. package/src/services/kms/index.js +75 -75
  45. package/src/services/kms/server.js +81 -81
  46. package/src/services/kms/simulator.js +344 -344
  47. package/src/services/lambda/handler-loader.js +183 -183
  48. package/src/services/lambda/index.js +81 -81
  49. package/src/services/lambda/route-registry.js +274 -274
  50. package/src/services/lambda/server.js +191 -191
  51. package/src/services/lambda/simulator.js +364 -364
  52. package/src/services/parameter-store/index.js +80 -80
  53. package/src/services/parameter-store/server.js +50 -50
  54. package/src/services/parameter-store/simulator.js +201 -201
  55. package/src/services/s3/index.js +73 -73
  56. package/src/services/s3/server.js +350 -350
  57. package/src/services/s3/simulator.js +568 -568
  58. package/src/services/secret-manager/index.js +80 -80
  59. package/src/services/secret-manager/server.js +51 -51
  60. package/src/services/secret-manager/simulator.js +182 -182
  61. package/src/services/sns/index.js +89 -89
  62. package/src/services/sns/server.js +607 -607
  63. package/src/services/sns/simulator.js +1482 -1482
  64. package/src/services/sqs/index.js +98 -98
  65. package/src/services/sqs/server.js +360 -360
  66. package/src/services/sqs/simulator.js +509 -509
  67. package/src/services/sts/index.js +37 -37
  68. package/src/services/sts/server.js +144 -144
  69. package/src/services/sts/simulator.js +69 -69
  70. package/src/services/xray/index.js +83 -83
  71. package/src/services/xray/server.js +308 -308
  72. package/src/services/xray/simulador.js +994 -994
  73. package/src/template/aws-config-template.js +87 -87
  74. package/src/template/aws-config-template.mjs +90 -90
  75. package/src/template/config-template.json +203 -203
  76. package/src/utils/aws-config.js +91 -91
  77. package/src/utils/cloudtrail-audit.js +129 -129
  78. package/src/utils/local-store.js +83 -83
  79. package/src/utils/logger.js +59 -59
@@ -1,845 +1,845 @@
1
- /**
2
- * ECS Simulator Core
3
- * Simula clusters, serviços, tarefas e containers
4
- */
5
-
6
- const crypto = require('crypto');
7
- const { spawn, exec } = require('child_process');
8
- const path = require('path');
9
- const fs = require('fs');
10
- const logger = require('../../utils/logger');
11
- const LocalStore = require('../../utils/local-store');
12
-
13
- class ECSSimulator {
14
- constructor(config) {
15
- this.config = config;
16
- this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, 'ecs');
17
- this.store = new LocalStore(this.dataDir);
18
- this.clusters = new Map();
19
- this.services = new Map();
20
- this.tasks = new Map();
21
- this.containerProcesses = new Map();
22
- this.availablePorts = new Set();
23
- this.nextPort = 50000;
24
- }
25
-
26
- async initialize() {
27
- logger.debug('Inicializando ECS Simulator...');
28
- this.loadClusters();
29
- this.loadServices();
30
- this.loadTasks();
31
-
32
- // Inicializa range de portas para containers
33
- for (let i = 50000; i <= 51000; i++) {
34
- this.availablePorts.add(i);
35
- }
36
- this.nextPort = 50000;
37
-
38
- logger.debug(`✅ ECS Simulator inicializado com ${this.clusters.size} clusters, ${this.services.size} serviços, ${this.tasks.size} tarefas`);
39
- }
40
-
41
- loadClusters() {
42
- const savedClusters = this.store.read('__clusters__');
43
- if (savedClusters) {
44
- for (const [name, data] of Object.entries(savedClusters)) {
45
- this.clusters.set(name, {
46
- name: data.name,
47
- arn: data.arn,
48
- status: data.status,
49
- createdAt: new Date(data.createdAt),
50
- services: data.services || [],
51
- tasks: data.tasks || []
52
- });
53
- }
54
- }
55
-
56
- // Cria clusters da configuração
57
- if (this.config.ecs?.clusters) {
58
- for (const clusterName of this.config.ecs.clusters) {
59
- this.createCluster(clusterName);
60
- }
61
- }
62
- }
63
-
64
- loadServices() {
65
- const savedServices = this.store.read('__services__');
66
- if (savedServices) {
67
- for (const [name, data] of Object.entries(savedServices)) {
68
- this.services.set(name, {
69
- name: data.name,
70
- arn: data.arn,
71
- clusterArn: data.clusterArn,
72
- taskDefinition: data.taskDefinition,
73
- desiredCount: data.desiredCount,
74
- runningCount: data.runningCount,
75
- status: data.status,
76
- loadBalancers: data.loadBalancers,
77
- networkConfiguration: data.networkConfiguration,
78
- schedulingStrategy: data.schedulingStrategy,
79
- createdAt: new Date(data.createdAt)
80
- });
81
- }
82
- }
83
- }
84
-
85
- loadTasks() {
86
- const savedTasks = this.store.read('__tasks__');
87
- if (savedTasks) {
88
- for (const [id, data] of Object.entries(savedTasks)) {
89
- this.tasks.set(id, {
90
- taskArn: data.taskArn,
91
- taskDefinitionArn: data.taskDefinitionArn,
92
- clusterArn: data.clusterArn,
93
- serviceName: data.serviceName,
94
- containers: data.containers,
95
- lastStatus: data.lastStatus,
96
- desiredStatus: data.desiredStatus,
97
- startedAt: new Date(data.startedAt),
98
- stoppedAt: data.stoppedAt ? new Date(data.stoppedAt) : null,
99
- stopCode: data.stopCode,
100
- createdAt: new Date(data.createdAt)
101
- });
102
-
103
- // Reconstitui processos de container se estavam rodando
104
- if (data.lastStatus === 'RUNNING') {
105
- this.startContainerProcess(data.taskArn, data.containers);
106
- }
107
- }
108
- }
109
- }
110
-
111
- // ============ Cluster Operations ============
112
-
113
- createCluster(clusterName) {
114
- if (this.clusters.has(clusterName)) {
115
- return { error: { code: 'ClusterExists', message: 'Cluster already exists' }, status: 409 };
116
- }
117
-
118
- const cluster = {
119
- name: clusterName,
120
- arn: `arn:aws:ecs:local:000000000000:cluster/${clusterName}`,
121
- status: 'ACTIVE',
122
- createdAt: new Date(),
123
- services: [],
124
- tasks: []
125
- };
126
-
127
- this.clusters.set(clusterName, cluster);
128
- this.persistClusters();
129
-
130
- logger.debug(`✅ Cluster ECS criado: ${clusterName}`);
131
-
132
- return { cluster };
133
- }
134
-
135
- listClusters() {
136
- return Array.from(this.clusters.keys());
137
- }
138
-
139
- describeCluster(clusterName) {
140
- const cluster = this.clusters.get(clusterName);
141
- if (!cluster) {
142
- throw new Error(`Cluster ${clusterName} not found`);
143
- }
144
-
145
- return {
146
- cluster: {
147
- clusterName: cluster.name,
148
- clusterArn: cluster.arn,
149
- status: cluster.status,
150
- registeredContainerInstancesCount: 0,
151
- runningTasksCount: this.getRunningTasksCount(clusterName),
152
- pendingTasksCount: this.getPendingTasksCount(clusterName),
153
- activeServicesCount: this.getActiveServicesCount(clusterName),
154
- statistics: []
155
- }
156
- };
157
- }
158
-
159
- deleteCluster(clusterName) {
160
- const cluster = this.clusters.get(clusterName);
161
- if (!cluster) {
162
- return { error: { code: 'ClusterNotFound', message: 'Cluster not found' }, status: 404 };
163
- }
164
-
165
- const services = this.getServicesInCluster(clusterName);
166
- if (services.length > 0) {
167
- return { error: { code: 'ClusterNotEmpty', message: 'Cluster has active services' }, status: 409 };
168
- }
169
-
170
- const tasks = this.getTasksInCluster(clusterName);
171
- if (tasks.length > 0) {
172
- return { error: { code: 'ClusterNotEmpty', message: 'Cluster has active tasks' }, status: 409 };
173
- }
174
-
175
- this.clusters.delete(clusterName);
176
- this.persistClusters();
177
-
178
- return { success: true };
179
- }
180
-
181
- // ============ Task Definition Operations ============
182
-
183
- registerTaskDefinition(params) {
184
- const { family, containerDefinitions, networkMode, cpu, memory, executionRoleArn, taskRoleArn } = params;
185
-
186
- const taskDefinitionArn = `arn:aws:ecs:local:000000000000:task-definition/${family}:${this.getNextRevision(family)}`;
187
-
188
- const taskDefinition = {
189
- family,
190
- taskDefinitionArn,
191
- revision: this.getNextRevision(family),
192
- containerDefinitions: containerDefinitions.map(this.normalizeContainerDefinition.bind(this)),
193
- networkMode: networkMode || 'awsvpc',
194
- cpu: cpu || '256',
195
- memory: memory || '512',
196
- executionRoleArn,
197
- taskRoleArn,
198
- status: 'ACTIVE',
199
- registeredAt: new Date().toISOString()
200
- };
201
-
202
- this.store.write(`taskdef_${family}_${taskDefinition.revision}`, taskDefinition);
203
- this.persistTaskDefinitions();
204
-
205
- logger.debug(`✅ Task Definition registrada: ${taskDefinitionArn}`);
206
-
207
- return { taskDefinition };
208
- }
209
-
210
- getNextRevision(family) {
211
- const files = this.store.list();
212
- const revisions = files
213
- .filter(f => f.startsWith(`taskdef_${family}_`))
214
- .map(f => parseInt(f.split('_')[2]) || 0);
215
-
216
- return Math.max(0, ...revisions) + 1;
217
- }
218
-
219
- normalizeContainerDefinition(container) {
220
- return {
221
- name: container.name,
222
- image: container.image,
223
- cpu: container.cpu || 0,
224
- memory: container.memory || 0,
225
- memoryReservation: container.memoryReservation || 0,
226
- essential: container.essential !== false,
227
- portMappings: (container.portMappings || []).map(pm => ({
228
- containerPort: pm.containerPort,
229
- hostPort: pm.hostPort || 0,
230
- protocol: pm.protocol || 'tcp'
231
- })),
232
- environment: container.environment || [],
233
- environmentFiles: container.environmentFiles || [],
234
- secrets: container.secrets || [],
235
- mountPoints: container.mountPoints || [],
236
- volumesFrom: container.volumesFrom || [],
237
- linuxParameters: container.linuxParameters || {},
238
- logConfiguration: container.logConfiguration || {
239
- logDriver: 'awslogs',
240
- options: {
241
- 'awslogs-group': `/ecs/${container.name}`,
242
- 'awslogs-region': 'local',
243
- 'awslogs-stream-prefix': 'ecs'
244
- }
245
- }
246
- };
247
- }
248
-
249
- // ============ Service Operations ============
250
-
251
- createService(params) {
252
- const { cluster, serviceName, taskDefinition, desiredCount, loadBalancers, networkConfiguration, schedulingStrategy } = params;
253
-
254
- const clusterObj = this.clusters.get(cluster);
255
- if (!clusterObj) {
256
- return { error: { code: 'ClusterNotFound', message: 'Cluster not found' }, status: 404 };
257
- }
258
-
259
- if (this.services.has(serviceName)) {
260
- return { error: { code: 'ServiceExists', message: 'Service already exists' }, status: 409 };
261
- }
262
-
263
- const serviceArn = `arn:aws:ecs:local:000000000000:service/${cluster}/${serviceName}`;
264
-
265
- const service = {
266
- name: serviceName,
267
- arn: serviceArn,
268
- clusterArn: clusterObj.arn,
269
- taskDefinition,
270
- desiredCount: desiredCount || 1,
271
- runningCount: 0,
272
- pendingCount: 0,
273
- status: 'ACTIVE',
274
- loadBalancers: loadBalancers || [],
275
- networkConfiguration: networkConfiguration || {
276
- awsvpcConfiguration: {
277
- subnets: ['subnet-local'],
278
- securityGroups: ['sg-local'],
279
- assignPublicIp: 'ENABLED'
280
- }
281
- },
282
- schedulingStrategy: schedulingStrategy || 'REPLICA',
283
- createdAt: new Date(),
284
- tasks: []
285
- };
286
-
287
- this.services.set(serviceName, service);
288
- clusterObj.services.push(serviceName);
289
- this.persistServices();
290
- this.persistClusters();
291
-
292
- // Inicia as tarefas do serviço
293
- if (desiredCount > 0) {
294
- this.scaleService(serviceName, desiredCount);
295
- }
296
-
297
- logger.debug(`✅ Serviço ECS criado: ${serviceName} (${desiredCount} tarefas)`);
298
-
299
- return { service };
300
- }
301
-
302
- updateService(params) {
303
- const { cluster, service, desiredCount, taskDefinition } = params;
304
-
305
- const serviceObj = this.services.get(service);
306
- if (!serviceObj) {
307
- return { error: { code: 'ServiceNotFound', message: 'Service not found' }, status: 404 };
308
- }
309
-
310
- if (desiredCount !== undefined) {
311
- serviceObj.desiredCount = desiredCount;
312
- this.scaleService(service, desiredCount);
313
- }
314
-
315
- if (taskDefinition) {
316
- serviceObj.taskDefinition = taskDefinition;
317
- // Em produção, faria uma atualização gradual
318
- this.updateServiceTasks(service, taskDefinition);
319
- }
320
-
321
- this.persistServices();
322
-
323
- return { service: serviceObj };
324
- }
325
-
326
- scaleService(serviceName, desiredCount) {
327
- const service = this.services.get(serviceName);
328
- if (!service) return;
329
-
330
- const currentCount = this.getRunningTasksCountForService(serviceName);
331
- const diff = desiredCount - currentCount;
332
-
333
- if (diff > 0) {
334
- // Scale up
335
- for (let i = 0; i < diff; i++) {
336
- this.runTask({
337
- cluster: service.clusterArn.split('/').pop(),
338
- taskDefinition: service.taskDefinition,
339
- serviceName: service.name
340
- });
341
- }
342
- } else if (diff < 0) {
343
- // Scale down
344
- const tasks = this.getTasksForService(serviceName);
345
- const toStop = tasks.slice(0, -diff);
346
- for (const task of toStop) {
347
- this.stopTask(task.taskArn);
348
- }
349
- }
350
-
351
- service.runningCount = this.getRunningTasksCountForService(serviceName);
352
- service.pendingCount = this.getPendingTasksCountForService(serviceName);
353
- }
354
-
355
- // ============ Task Operations ============
356
-
357
- async runTask(params) {
358
- const { cluster, taskDefinition, serviceName, overrides } = params;
359
-
360
- const clusterObj = this.clusters.get(cluster);
361
- if (!clusterObj) {
362
- return { error: { code: 'ClusterNotFound', message: 'Cluster not found' }, status: 404 };
363
- }
364
-
365
- // Busca a task definition
366
- const taskDef = this.getTaskDefinition(taskDefinition);
367
- if (!taskDef) {
368
- return { error: { code: 'TaskDefinitionNotFound', message: 'Task definition not found' }, status: 404 };
369
- }
370
-
371
- const taskId = crypto.randomUUID();
372
- const taskArn = `arn:aws:ecs:local:000000000000:task/${cluster}/${taskId}`;
373
-
374
- // Prepara containers com portas mapeadas
375
- const containers = await this.prepareContainers(taskDef.containerDefinitions, overrides);
376
-
377
- const task = {
378
- taskArn,
379
- taskDefinitionArn: taskDef.taskDefinitionArn,
380
- clusterArn: clusterObj.arn,
381
- serviceName: serviceName || null,
382
- containers,
383
- lastStatus: 'PROVISIONING',
384
- desiredStatus: 'RUNNING',
385
- startedAt: null,
386
- stoppedAt: null,
387
- stopCode: null,
388
- createdAt: new Date(),
389
- overrides: overrides || {}
390
- };
391
-
392
- this.tasks.set(taskArn, task);
393
- clusterObj.tasks.push(taskArn);
394
-
395
- if (serviceName) {
396
- const service = this.services.get(serviceName);
397
- if (service) {
398
- service.tasks.push(taskArn);
399
- service.pendingCount++;
400
- }
401
- }
402
-
403
- this.persistTasks();
404
- this.persistClusters();
405
- this.persistServices();
406
-
407
- logger.debug(`📦 Tarefa ECS criada: ${taskArn}`);
408
-
409
- // Inicia os containers
410
- this.startTask(task);
411
-
412
- return { task };
413
- }
414
-
415
- async prepareContainers(containerDefs, overrides) {
416
- const containers = [];
417
-
418
- for (const containerDef of containerDefs) {
419
- // Aloca portas para o container
420
- const portMappings = [];
421
- for (const mapping of containerDef.portMappings || []) {
422
- const hostPort = this.allocatePort();
423
- portMappings.push({
424
- containerPort: mapping.containerPort,
425
- hostPort,
426
- protocol: mapping.protocol
427
- });
428
- }
429
-
430
- containers.push({
431
- name: containerDef.name,
432
- image: containerDef.image,
433
- containerArn: `arn:aws:ecs:local:container/${crypto.randomUUID()}`,
434
- lastStatus: 'PROVISIONING',
435
- desiredStatus: 'RUNNING',
436
- portMappings,
437
- environment: containerDef.environment,
438
- command: overrides?.containerOverrides?.find(c => c.name === containerDef.name)?.command || null,
439
- startedAt: null,
440
- stoppedAt: null
441
- });
442
- }
443
-
444
- return containers;
445
- }
446
-
447
- allocatePort() {
448
- if (this.availablePorts.size === 0) {
449
- // Expande range de portas
450
- for (let i = this.nextPort; i <= this.nextPort + 100; i++) {
451
- this.availablePorts.add(i);
452
- }
453
- this.nextPort += 100;
454
- }
455
-
456
- const port = Array.from(this.availablePorts)[0];
457
- this.availablePorts.delete(port);
458
- return port;
459
- }
460
-
461
- releasePort(port) {
462
- this.availablePorts.add(port);
463
- }
464
-
465
- async startTask(task) {
466
- task.lastStatus = 'PENDING';
467
- this.persistTasks();
468
-
469
- // Simula tempo de provisão
470
- setTimeout(async () => {
471
- task.lastStatus = 'RUNNING';
472
- task.startedAt = new Date();
473
-
474
- if (task.serviceName) {
475
- const service = this.services.get(task.serviceName);
476
- if (service) {
477
- service.runningCount++;
478
- service.pendingCount--;
479
- this.persistServices();
480
- }
481
- }
482
-
483
- this.persistTasks();
484
-
485
- // Inicia cada container
486
- for (const container of task.containers) {
487
- await this.startContainer(task.taskArn, container);
488
- }
489
-
490
- logger.success(`✅ Tarefa ECS iniciada: ${task.taskArn}`);
491
- }, 1000);
492
- }
493
-
494
- async startContainer(taskArn, container) {
495
- const containerId = crypto.randomUUID();
496
- container.lastStatus = 'RUNNING';
497
- container.startedAt = new Date();
498
- container.containerId = containerId;
499
-
500
- logger.info(`🐳 Container iniciado: ${container.name} (${container.image})`);
501
- logger.info(` Portas: ${container.portMappings.map(p => `${p.containerPort}:${p.hostPort}`).join(', ')}`);
502
-
503
- // Simula execução do container
504
- // Em um cenário real, aqui você poderia realmente executar o container Docker
505
- this.containerProcesses.set(container.containerId, {
506
- taskArn,
507
- container,
508
- running: true,
509
- startTime: new Date()
510
- });
511
-
512
- this.persistTasks();
513
- }
514
-
515
- async stopTask(taskArn) {
516
- const task = this.tasks.get(taskArn);
517
- if (!task) {
518
- return { error: { code: 'TaskNotFound', message: 'Task not found' }, status: 404 };
519
- }
520
-
521
- // Para todos os containers
522
- for (const container of task.containers) {
523
- if (container.containerId) {
524
- const process = this.containerProcesses.get(container.containerId);
525
- if (process) {
526
- process.running = false;
527
- this.containerProcesses.delete(container.containerId);
528
- }
529
- container.lastStatus = 'STOPPED';
530
- container.stoppedAt = new Date();
531
- this.releasePorts(container.portMappings);
532
- }
533
- }
534
-
535
- task.lastStatus = 'STOPPED';
536
- task.desiredStatus = 'STOPPED';
537
- task.stoppedAt = new Date();
538
- task.stopCode = 'UserInitiated';
539
-
540
- if (task.serviceName) {
541
- const service = this.services.get(task.serviceName);
542
- if (service && service.runningCount > 0) {
543
- service.runningCount--;
544
- this.persistServices();
545
- }
546
- }
547
-
548
- this.persistTasks();
549
-
550
- logger.debug(`🛑 Tarefa ECS parada: ${taskArn}`);
551
-
552
- return { task };
553
- }
554
-
555
- releasePorts(portMappings) {
556
- for (const mapping of portMappings) {
557
- if (mapping.hostPort) {
558
- this.releasePort(mapping.hostPort);
559
- }
560
- }
561
- }
562
-
563
- listTasks(params) {
564
- const { cluster, serviceName } = params;
565
- let tasks = Array.from(this.tasks.values());
566
-
567
- if (cluster) {
568
- tasks = tasks.filter(t => t.clusterArn.includes(cluster));
569
- }
570
-
571
- if (serviceName) {
572
- tasks = tasks.filter(t => t.serviceName === serviceName);
573
- }
574
-
575
- return {
576
- taskArns: tasks.map(t => t.taskArn)
577
- };
578
- }
579
-
580
- describeTasks(params) {
581
- const { cluster, tasks } = params;
582
- const taskList = [];
583
-
584
- for (const taskArn of tasks) {
585
- const task = this.tasks.get(taskArn);
586
- if (task) {
587
- taskList.push({
588
- taskArn: task.taskArn,
589
- taskDefinitionArn: task.taskDefinitionArn,
590
- clusterArn: task.clusterArn,
591
- serviceName: task.serviceName,
592
- containers: task.containers.map(c => ({
593
- name: c.name,
594
- containerArn: c.containerArn,
595
- lastStatus: c.lastStatus,
596
- networkBindings: c.portMappings.map(pm => ({
597
- containerPort: pm.containerPort,
598
- hostPort: pm.hostPort,
599
- protocol: pm.protocol
600
- }))
601
- })),
602
- lastStatus: task.lastStatus,
603
- desiredStatus: task.desiredStatus,
604
- startedAt: task.startedAt,
605
- stoppedAt: task.stoppedAt,
606
- stopCode: task.stopCode,
607
- createdAt: task.createdAt
608
- });
609
- }
610
- }
611
-
612
- return { tasks: taskList, failures: [] };
613
- }
614
-
615
- // ============ Helper Methods ============
616
-
617
- getTaskDefinition(taskDefinition) {
618
- const [family, revision] = taskDefinition.split(':').pop().split('/').pop().split(':');
619
- return this.store.read(`taskdef_${family}_${revision || this.getLatestRevision(family)}`);
620
- }
621
-
622
- getLatestRevision(family) {
623
- const files = this.store.list();
624
- const revisions = files
625
- .filter(f => f.startsWith(`taskdef_${family}_`))
626
- .map(f => parseInt(f.split('_')[2]) || 0);
627
-
628
- return Math.max(0, ...revisions);
629
- }
630
-
631
- getRunningTasksCount(clusterName) {
632
- const cluster = this.clusters.get(clusterName);
633
- if (!cluster) return 0;
634
-
635
- let count = 0;
636
- for (const taskArn of cluster.tasks) {
637
- const task = this.tasks.get(taskArn);
638
- if (task && task.lastStatus === 'RUNNING') {
639
- count++;
640
- }
641
- }
642
- return count;
643
- }
644
-
645
- getPendingTasksCount(clusterName) {
646
- const cluster = this.clusters.get(clusterName);
647
- if (!cluster) return 0;
648
-
649
- let count = 0;
650
- for (const taskArn of cluster.tasks) {
651
- const task = this.tasks.get(taskArn);
652
- if (task && task.lastStatus === 'PENDING') {
653
- count++;
654
- }
655
- }
656
- return count;
657
- }
658
-
659
- getActiveServicesCount(clusterName) {
660
- const cluster = this.clusters.get(clusterName);
661
- if (!cluster) return 0;
662
- return cluster.services.length;
663
- }
664
-
665
- getServicesInCluster(clusterName) {
666
- const cluster = this.clusters.get(clusterName);
667
- if (!cluster) return [];
668
- return cluster.services;
669
- }
670
-
671
- getTasksInCluster(clusterName) {
672
- const cluster = this.clusters.get(clusterName);
673
- if (!cluster) return [];
674
- return cluster.tasks;
675
- }
676
-
677
- getRunningTasksCountForService(serviceName) {
678
- const service = this.services.get(serviceName);
679
- if (!service) return 0;
680
-
681
- let count = 0;
682
- for (const taskArn of service.tasks) {
683
- const task = this.tasks.get(taskArn);
684
- if (task && task.lastStatus === 'RUNNING') {
685
- count++;
686
- }
687
- }
688
- return count;
689
- }
690
-
691
- getPendingTasksCountForService(serviceName) {
692
- const service = this.services.get(serviceName);
693
- if (!service) return 0;
694
-
695
- let count = 0;
696
- for (const taskArn of service.tasks) {
697
- const task = this.tasks.get(taskArn);
698
- if (task && task.lastStatus === 'PENDING') {
699
- count++;
700
- }
701
- }
702
- return count;
703
- }
704
-
705
- getTasksForService(serviceName) {
706
- const service = this.services.get(serviceName);
707
- if (!service) return [];
708
-
709
- const tasks = [];
710
- for (const taskArn of service.tasks) {
711
- const task = this.tasks.get(taskArn);
712
- if (task) {
713
- tasks.push(task);
714
- }
715
- }
716
- return tasks;
717
- }
718
-
719
- updateServiceTasks(serviceName, newTaskDefinition) {
720
- const service = this.services.get(serviceName);
721
- if (!service) return;
722
-
723
- const tasks = this.getTasksForService(serviceName);
724
- for (const task of tasks) {
725
- if (task.lastStatus === 'RUNNING') {
726
- // Para a tarefa antiga e inicia nova
727
- this.stopTask(task.taskArn);
728
- this.runTask({
729
- cluster: service.clusterArn.split('/').pop(),
730
- taskDefinition: newTaskDefinition,
731
- serviceName: service.name
732
- });
733
- }
734
- }
735
- }
736
-
737
- // ============ Persistence ============
738
-
739
- persistClusters() {
740
- const clustersObj = {};
741
- for (const [name, cluster] of this.clusters.entries()) {
742
- clustersObj[name] = {
743
- name: cluster.name,
744
- arn: cluster.arn,
745
- status: cluster.status,
746
- createdAt: cluster.createdAt.toISOString(),
747
- services: cluster.services,
748
- tasks: cluster.tasks
749
- };
750
- }
751
- this.store.write('__clusters__', clustersObj);
752
- }
753
-
754
- persistServices() {
755
- const servicesObj = {};
756
- for (const [name, service] of this.services.entries()) {
757
- servicesObj[name] = {
758
- name: service.name,
759
- arn: service.arn,
760
- clusterArn: service.clusterArn,
761
- taskDefinition: service.taskDefinition,
762
- desiredCount: service.desiredCount,
763
- runningCount: service.runningCount,
764
- status: service.status,
765
- loadBalancers: service.loadBalancers,
766
- networkConfiguration: service.networkConfiguration,
767
- schedulingStrategy: service.schedulingStrategy,
768
- createdAt: service.createdAt.toISOString(),
769
- tasks: service.tasks
770
- };
771
- }
772
- this.store.write('__services__', servicesObj);
773
- }
774
-
775
- persistTasks() {
776
- const tasksObj = {};
777
- for (const [arn, task] of this.tasks.entries()) {
778
- tasksObj[arn] = {
779
- taskArn: task.taskArn,
780
- taskDefinitionArn: task.taskDefinitionArn,
781
- clusterArn: task.clusterArn,
782
- serviceName: task.serviceName,
783
- containers: task.containers.map(c => ({
784
- name: c.name,
785
- image: c.image,
786
- containerArn: c.containerArn,
787
- lastStatus: c.lastStatus,
788
- desiredStatus: c.desiredStatus,
789
- portMappings: c.portMappings,
790
- environment: c.environment,
791
- startedAt: c.startedAt,
792
- stoppedAt: c.stoppedAt
793
- })),
794
- lastStatus: task.lastStatus,
795
- desiredStatus: task.desiredStatus,
796
- startedAt: task.startedAt,
797
- stoppedAt: task.stoppedAt,
798
- stopCode: task.stopCode,
799
- createdAt: task.createdAt.toISOString()
800
- };
801
- }
802
- this.store.write('__tasks__', tasksObj);
803
- }
804
-
805
- persistTaskDefinitions() {
806
- // Task definitions são persistidas individualmente
807
- // Já salvas no registerTaskDefinition
808
- }
809
-
810
- async reset() {
811
- // Para todas as tarefas em execução
812
- for (const [taskArn] of this.tasks) {
813
- await this.stopTask(taskArn);
814
- }
815
-
816
- this.clusters.clear();
817
- this.services.clear();
818
- this.tasks.clear();
819
- this.containerProcesses.clear();
820
-
821
- this.persistClusters();
822
- this.persistServices();
823
- this.persistTasks();
824
-
825
- logger.debug('ECS: Todos os dados resetados');
826
- }
827
-
828
- getClustersCount() {
829
- return this.clusters.size;
830
- }
831
-
832
- getServicesCount() {
833
- return this.services.size;
834
- }
835
-
836
- getTasksCount() {
837
- return this.tasks.size;
838
- }
839
-
840
- getRunningContainers() {
841
- return this.containerProcesses.size;
842
- }
843
- }
844
-
1
+ /**
2
+ * ECS Simulator Core
3
+ * Simula clusters, serviços, tarefas e containers
4
+ */
5
+
6
+ const crypto = require('crypto');
7
+ const { spawn, exec } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const logger = require('../../utils/logger');
11
+ const LocalStore = require('../../utils/local-store');
12
+
13
+ class ECSSimulator {
14
+ constructor(config) {
15
+ this.config = config;
16
+ this.dataDir = path.join(process.env.AWS_LOCAL_SIMULATOR_DATA_DIR, 'ecs');
17
+ this.store = new LocalStore(this.dataDir);
18
+ this.clusters = new Map();
19
+ this.services = new Map();
20
+ this.tasks = new Map();
21
+ this.containerProcesses = new Map();
22
+ this.availablePorts = new Set();
23
+ this.nextPort = 50000;
24
+ }
25
+
26
+ async initialize() {
27
+ logger.debug('Inicializando ECS Simulator...');
28
+ this.loadClusters();
29
+ this.loadServices();
30
+ this.loadTasks();
31
+
32
+ // Inicializa range de portas para containers
33
+ for (let i = 50000; i <= 51000; i++) {
34
+ this.availablePorts.add(i);
35
+ }
36
+ this.nextPort = 50000;
37
+
38
+ logger.debug(`✅ ECS Simulator inicializado com ${this.clusters.size} clusters, ${this.services.size} serviços, ${this.tasks.size} tarefas`);
39
+ }
40
+
41
+ loadClusters() {
42
+ const savedClusters = this.store.read('__clusters__');
43
+ if (savedClusters) {
44
+ for (const [name, data] of Object.entries(savedClusters)) {
45
+ this.clusters.set(name, {
46
+ name: data.name,
47
+ arn: data.arn,
48
+ status: data.status,
49
+ createdAt: new Date(data.createdAt),
50
+ services: data.services || [],
51
+ tasks: data.tasks || []
52
+ });
53
+ }
54
+ }
55
+
56
+ // Cria clusters da configuração
57
+ if (this.config.ecs?.clusters) {
58
+ for (const clusterName of this.config.ecs.clusters) {
59
+ this.createCluster(clusterName);
60
+ }
61
+ }
62
+ }
63
+
64
+ loadServices() {
65
+ const savedServices = this.store.read('__services__');
66
+ if (savedServices) {
67
+ for (const [name, data] of Object.entries(savedServices)) {
68
+ this.services.set(name, {
69
+ name: data.name,
70
+ arn: data.arn,
71
+ clusterArn: data.clusterArn,
72
+ taskDefinition: data.taskDefinition,
73
+ desiredCount: data.desiredCount,
74
+ runningCount: data.runningCount,
75
+ status: data.status,
76
+ loadBalancers: data.loadBalancers,
77
+ networkConfiguration: data.networkConfiguration,
78
+ schedulingStrategy: data.schedulingStrategy,
79
+ createdAt: new Date(data.createdAt)
80
+ });
81
+ }
82
+ }
83
+ }
84
+
85
+ loadTasks() {
86
+ const savedTasks = this.store.read('__tasks__');
87
+ if (savedTasks) {
88
+ for (const [id, data] of Object.entries(savedTasks)) {
89
+ this.tasks.set(id, {
90
+ taskArn: data.taskArn,
91
+ taskDefinitionArn: data.taskDefinitionArn,
92
+ clusterArn: data.clusterArn,
93
+ serviceName: data.serviceName,
94
+ containers: data.containers,
95
+ lastStatus: data.lastStatus,
96
+ desiredStatus: data.desiredStatus,
97
+ startedAt: new Date(data.startedAt),
98
+ stoppedAt: data.stoppedAt ? new Date(data.stoppedAt) : null,
99
+ stopCode: data.stopCode,
100
+ createdAt: new Date(data.createdAt)
101
+ });
102
+
103
+ // Reconstitui processos de container se estavam rodando
104
+ if (data.lastStatus === 'RUNNING') {
105
+ this.startContainerProcess(data.taskArn, data.containers);
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ // ============ Cluster Operations ============
112
+
113
+ createCluster(clusterName) {
114
+ if (this.clusters.has(clusterName)) {
115
+ return { error: { code: 'ClusterExists', message: 'Cluster already exists' }, status: 409 };
116
+ }
117
+
118
+ const cluster = {
119
+ name: clusterName,
120
+ arn: `arn:aws:ecs:local:000000000000:cluster/${clusterName}`,
121
+ status: 'ACTIVE',
122
+ createdAt: new Date(),
123
+ services: [],
124
+ tasks: []
125
+ };
126
+
127
+ this.clusters.set(clusterName, cluster);
128
+ this.persistClusters();
129
+
130
+ logger.debug(`✅ Cluster ECS criado: ${clusterName}`);
131
+
132
+ return { cluster };
133
+ }
134
+
135
+ listClusters() {
136
+ return Array.from(this.clusters.keys());
137
+ }
138
+
139
+ describeCluster(clusterName) {
140
+ const cluster = this.clusters.get(clusterName);
141
+ if (!cluster) {
142
+ throw new Error(`Cluster ${clusterName} not found`);
143
+ }
144
+
145
+ return {
146
+ cluster: {
147
+ clusterName: cluster.name,
148
+ clusterArn: cluster.arn,
149
+ status: cluster.status,
150
+ registeredContainerInstancesCount: 0,
151
+ runningTasksCount: this.getRunningTasksCount(clusterName),
152
+ pendingTasksCount: this.getPendingTasksCount(clusterName),
153
+ activeServicesCount: this.getActiveServicesCount(clusterName),
154
+ statistics: []
155
+ }
156
+ };
157
+ }
158
+
159
+ deleteCluster(clusterName) {
160
+ const cluster = this.clusters.get(clusterName);
161
+ if (!cluster) {
162
+ return { error: { code: 'ClusterNotFound', message: 'Cluster not found' }, status: 404 };
163
+ }
164
+
165
+ const services = this.getServicesInCluster(clusterName);
166
+ if (services.length > 0) {
167
+ return { error: { code: 'ClusterNotEmpty', message: 'Cluster has active services' }, status: 409 };
168
+ }
169
+
170
+ const tasks = this.getTasksInCluster(clusterName);
171
+ if (tasks.length > 0) {
172
+ return { error: { code: 'ClusterNotEmpty', message: 'Cluster has active tasks' }, status: 409 };
173
+ }
174
+
175
+ this.clusters.delete(clusterName);
176
+ this.persistClusters();
177
+
178
+ return { success: true };
179
+ }
180
+
181
+ // ============ Task Definition Operations ============
182
+
183
+ registerTaskDefinition(params) {
184
+ const { family, containerDefinitions, networkMode, cpu, memory, executionRoleArn, taskRoleArn } = params;
185
+
186
+ const taskDefinitionArn = `arn:aws:ecs:local:000000000000:task-definition/${family}:${this.getNextRevision(family)}`;
187
+
188
+ const taskDefinition = {
189
+ family,
190
+ taskDefinitionArn,
191
+ revision: this.getNextRevision(family),
192
+ containerDefinitions: containerDefinitions.map(this.normalizeContainerDefinition.bind(this)),
193
+ networkMode: networkMode || 'awsvpc',
194
+ cpu: cpu || '256',
195
+ memory: memory || '512',
196
+ executionRoleArn,
197
+ taskRoleArn,
198
+ status: 'ACTIVE',
199
+ registeredAt: new Date().toISOString()
200
+ };
201
+
202
+ this.store.write(`taskdef_${family}_${taskDefinition.revision}`, taskDefinition);
203
+ this.persistTaskDefinitions();
204
+
205
+ logger.debug(`✅ Task Definition registrada: ${taskDefinitionArn}`);
206
+
207
+ return { taskDefinition };
208
+ }
209
+
210
+ getNextRevision(family) {
211
+ const files = this.store.list();
212
+ const revisions = files
213
+ .filter(f => f.startsWith(`taskdef_${family}_`))
214
+ .map(f => parseInt(f.split('_')[2]) || 0);
215
+
216
+ return Math.max(0, ...revisions) + 1;
217
+ }
218
+
219
+ normalizeContainerDefinition(container) {
220
+ return {
221
+ name: container.name,
222
+ image: container.image,
223
+ cpu: container.cpu || 0,
224
+ memory: container.memory || 0,
225
+ memoryReservation: container.memoryReservation || 0,
226
+ essential: container.essential !== false,
227
+ portMappings: (container.portMappings || []).map(pm => ({
228
+ containerPort: pm.containerPort,
229
+ hostPort: pm.hostPort || 0,
230
+ protocol: pm.protocol || 'tcp'
231
+ })),
232
+ environment: container.environment || [],
233
+ environmentFiles: container.environmentFiles || [],
234
+ secrets: container.secrets || [],
235
+ mountPoints: container.mountPoints || [],
236
+ volumesFrom: container.volumesFrom || [],
237
+ linuxParameters: container.linuxParameters || {},
238
+ logConfiguration: container.logConfiguration || {
239
+ logDriver: 'awslogs',
240
+ options: {
241
+ 'awslogs-group': `/ecs/${container.name}`,
242
+ 'awslogs-region': 'local',
243
+ 'awslogs-stream-prefix': 'ecs'
244
+ }
245
+ }
246
+ };
247
+ }
248
+
249
+ // ============ Service Operations ============
250
+
251
+ createService(params) {
252
+ const { cluster, serviceName, taskDefinition, desiredCount, loadBalancers, networkConfiguration, schedulingStrategy } = params;
253
+
254
+ const clusterObj = this.clusters.get(cluster);
255
+ if (!clusterObj) {
256
+ return { error: { code: 'ClusterNotFound', message: 'Cluster not found' }, status: 404 };
257
+ }
258
+
259
+ if (this.services.has(serviceName)) {
260
+ return { error: { code: 'ServiceExists', message: 'Service already exists' }, status: 409 };
261
+ }
262
+
263
+ const serviceArn = `arn:aws:ecs:local:000000000000:service/${cluster}/${serviceName}`;
264
+
265
+ const service = {
266
+ name: serviceName,
267
+ arn: serviceArn,
268
+ clusterArn: clusterObj.arn,
269
+ taskDefinition,
270
+ desiredCount: desiredCount || 1,
271
+ runningCount: 0,
272
+ pendingCount: 0,
273
+ status: 'ACTIVE',
274
+ loadBalancers: loadBalancers || [],
275
+ networkConfiguration: networkConfiguration || {
276
+ awsvpcConfiguration: {
277
+ subnets: ['subnet-local'],
278
+ securityGroups: ['sg-local'],
279
+ assignPublicIp: 'ENABLED'
280
+ }
281
+ },
282
+ schedulingStrategy: schedulingStrategy || 'REPLICA',
283
+ createdAt: new Date(),
284
+ tasks: []
285
+ };
286
+
287
+ this.services.set(serviceName, service);
288
+ clusterObj.services.push(serviceName);
289
+ this.persistServices();
290
+ this.persistClusters();
291
+
292
+ // Inicia as tarefas do serviço
293
+ if (desiredCount > 0) {
294
+ this.scaleService(serviceName, desiredCount);
295
+ }
296
+
297
+ logger.debug(`✅ Serviço ECS criado: ${serviceName} (${desiredCount} tarefas)`);
298
+
299
+ return { service };
300
+ }
301
+
302
+ updateService(params) {
303
+ const { cluster, service, desiredCount, taskDefinition } = params;
304
+
305
+ const serviceObj = this.services.get(service);
306
+ if (!serviceObj) {
307
+ return { error: { code: 'ServiceNotFound', message: 'Service not found' }, status: 404 };
308
+ }
309
+
310
+ if (desiredCount !== undefined) {
311
+ serviceObj.desiredCount = desiredCount;
312
+ this.scaleService(service, desiredCount);
313
+ }
314
+
315
+ if (taskDefinition) {
316
+ serviceObj.taskDefinition = taskDefinition;
317
+ // Em produção, faria uma atualização gradual
318
+ this.updateServiceTasks(service, taskDefinition);
319
+ }
320
+
321
+ this.persistServices();
322
+
323
+ return { service: serviceObj };
324
+ }
325
+
326
+ scaleService(serviceName, desiredCount) {
327
+ const service = this.services.get(serviceName);
328
+ if (!service) return;
329
+
330
+ const currentCount = this.getRunningTasksCountForService(serviceName);
331
+ const diff = desiredCount - currentCount;
332
+
333
+ if (diff > 0) {
334
+ // Scale up
335
+ for (let i = 0; i < diff; i++) {
336
+ this.runTask({
337
+ cluster: service.clusterArn.split('/').pop(),
338
+ taskDefinition: service.taskDefinition,
339
+ serviceName: service.name
340
+ });
341
+ }
342
+ } else if (diff < 0) {
343
+ // Scale down
344
+ const tasks = this.getTasksForService(serviceName);
345
+ const toStop = tasks.slice(0, -diff);
346
+ for (const task of toStop) {
347
+ this.stopTask(task.taskArn);
348
+ }
349
+ }
350
+
351
+ service.runningCount = this.getRunningTasksCountForService(serviceName);
352
+ service.pendingCount = this.getPendingTasksCountForService(serviceName);
353
+ }
354
+
355
+ // ============ Task Operations ============
356
+
357
+ async runTask(params) {
358
+ const { cluster, taskDefinition, serviceName, overrides } = params;
359
+
360
+ const clusterObj = this.clusters.get(cluster);
361
+ if (!clusterObj) {
362
+ return { error: { code: 'ClusterNotFound', message: 'Cluster not found' }, status: 404 };
363
+ }
364
+
365
+ // Busca a task definition
366
+ const taskDef = this.getTaskDefinition(taskDefinition);
367
+ if (!taskDef) {
368
+ return { error: { code: 'TaskDefinitionNotFound', message: 'Task definition not found' }, status: 404 };
369
+ }
370
+
371
+ const taskId = crypto.randomUUID();
372
+ const taskArn = `arn:aws:ecs:local:000000000000:task/${cluster}/${taskId}`;
373
+
374
+ // Prepara containers com portas mapeadas
375
+ const containers = await this.prepareContainers(taskDef.containerDefinitions, overrides);
376
+
377
+ const task = {
378
+ taskArn,
379
+ taskDefinitionArn: taskDef.taskDefinitionArn,
380
+ clusterArn: clusterObj.arn,
381
+ serviceName: serviceName || null,
382
+ containers,
383
+ lastStatus: 'PROVISIONING',
384
+ desiredStatus: 'RUNNING',
385
+ startedAt: null,
386
+ stoppedAt: null,
387
+ stopCode: null,
388
+ createdAt: new Date(),
389
+ overrides: overrides || {}
390
+ };
391
+
392
+ this.tasks.set(taskArn, task);
393
+ clusterObj.tasks.push(taskArn);
394
+
395
+ if (serviceName) {
396
+ const service = this.services.get(serviceName);
397
+ if (service) {
398
+ service.tasks.push(taskArn);
399
+ service.pendingCount++;
400
+ }
401
+ }
402
+
403
+ this.persistTasks();
404
+ this.persistClusters();
405
+ this.persistServices();
406
+
407
+ logger.debug(`📦 Tarefa ECS criada: ${taskArn}`);
408
+
409
+ // Inicia os containers
410
+ this.startTask(task);
411
+
412
+ return { task };
413
+ }
414
+
415
+ async prepareContainers(containerDefs, overrides) {
416
+ const containers = [];
417
+
418
+ for (const containerDef of containerDefs) {
419
+ // Aloca portas para o container
420
+ const portMappings = [];
421
+ for (const mapping of containerDef.portMappings || []) {
422
+ const hostPort = this.allocatePort();
423
+ portMappings.push({
424
+ containerPort: mapping.containerPort,
425
+ hostPort,
426
+ protocol: mapping.protocol
427
+ });
428
+ }
429
+
430
+ containers.push({
431
+ name: containerDef.name,
432
+ image: containerDef.image,
433
+ containerArn: `arn:aws:ecs:local:container/${crypto.randomUUID()}`,
434
+ lastStatus: 'PROVISIONING',
435
+ desiredStatus: 'RUNNING',
436
+ portMappings,
437
+ environment: containerDef.environment,
438
+ command: overrides?.containerOverrides?.find(c => c.name === containerDef.name)?.command || null,
439
+ startedAt: null,
440
+ stoppedAt: null
441
+ });
442
+ }
443
+
444
+ return containers;
445
+ }
446
+
447
+ allocatePort() {
448
+ if (this.availablePorts.size === 0) {
449
+ // Expande range de portas
450
+ for (let i = this.nextPort; i <= this.nextPort + 100; i++) {
451
+ this.availablePorts.add(i);
452
+ }
453
+ this.nextPort += 100;
454
+ }
455
+
456
+ const port = Array.from(this.availablePorts)[0];
457
+ this.availablePorts.delete(port);
458
+ return port;
459
+ }
460
+
461
+ releasePort(port) {
462
+ this.availablePorts.add(port);
463
+ }
464
+
465
+ async startTask(task) {
466
+ task.lastStatus = 'PENDING';
467
+ this.persistTasks();
468
+
469
+ // Simula tempo de provisão
470
+ setTimeout(async () => {
471
+ task.lastStatus = 'RUNNING';
472
+ task.startedAt = new Date();
473
+
474
+ if (task.serviceName) {
475
+ const service = this.services.get(task.serviceName);
476
+ if (service) {
477
+ service.runningCount++;
478
+ service.pendingCount--;
479
+ this.persistServices();
480
+ }
481
+ }
482
+
483
+ this.persistTasks();
484
+
485
+ // Inicia cada container
486
+ for (const container of task.containers) {
487
+ await this.startContainer(task.taskArn, container);
488
+ }
489
+
490
+ logger.success(`✅ Tarefa ECS iniciada: ${task.taskArn}`);
491
+ }, 1000);
492
+ }
493
+
494
+ async startContainer(taskArn, container) {
495
+ const containerId = crypto.randomUUID();
496
+ container.lastStatus = 'RUNNING';
497
+ container.startedAt = new Date();
498
+ container.containerId = containerId;
499
+
500
+ logger.info(`🐳 Container iniciado: ${container.name} (${container.image})`);
501
+ logger.info(` Portas: ${container.portMappings.map(p => `${p.containerPort}:${p.hostPort}`).join(', ')}`);
502
+
503
+ // Simula execução do container
504
+ // Em um cenário real, aqui você poderia realmente executar o container Docker
505
+ this.containerProcesses.set(container.containerId, {
506
+ taskArn,
507
+ container,
508
+ running: true,
509
+ startTime: new Date()
510
+ });
511
+
512
+ this.persistTasks();
513
+ }
514
+
515
+ async stopTask(taskArn) {
516
+ const task = this.tasks.get(taskArn);
517
+ if (!task) {
518
+ return { error: { code: 'TaskNotFound', message: 'Task not found' }, status: 404 };
519
+ }
520
+
521
+ // Para todos os containers
522
+ for (const container of task.containers) {
523
+ if (container.containerId) {
524
+ const process = this.containerProcesses.get(container.containerId);
525
+ if (process) {
526
+ process.running = false;
527
+ this.containerProcesses.delete(container.containerId);
528
+ }
529
+ container.lastStatus = 'STOPPED';
530
+ container.stoppedAt = new Date();
531
+ this.releasePorts(container.portMappings);
532
+ }
533
+ }
534
+
535
+ task.lastStatus = 'STOPPED';
536
+ task.desiredStatus = 'STOPPED';
537
+ task.stoppedAt = new Date();
538
+ task.stopCode = 'UserInitiated';
539
+
540
+ if (task.serviceName) {
541
+ const service = this.services.get(task.serviceName);
542
+ if (service && service.runningCount > 0) {
543
+ service.runningCount--;
544
+ this.persistServices();
545
+ }
546
+ }
547
+
548
+ this.persistTasks();
549
+
550
+ logger.debug(`🛑 Tarefa ECS parada: ${taskArn}`);
551
+
552
+ return { task };
553
+ }
554
+
555
+ releasePorts(portMappings) {
556
+ for (const mapping of portMappings) {
557
+ if (mapping.hostPort) {
558
+ this.releasePort(mapping.hostPort);
559
+ }
560
+ }
561
+ }
562
+
563
+ listTasks(params) {
564
+ const { cluster, serviceName } = params;
565
+ let tasks = Array.from(this.tasks.values());
566
+
567
+ if (cluster) {
568
+ tasks = tasks.filter(t => t.clusterArn.includes(cluster));
569
+ }
570
+
571
+ if (serviceName) {
572
+ tasks = tasks.filter(t => t.serviceName === serviceName);
573
+ }
574
+
575
+ return {
576
+ taskArns: tasks.map(t => t.taskArn)
577
+ };
578
+ }
579
+
580
+ describeTasks(params) {
581
+ const { cluster, tasks } = params;
582
+ const taskList = [];
583
+
584
+ for (const taskArn of tasks) {
585
+ const task = this.tasks.get(taskArn);
586
+ if (task) {
587
+ taskList.push({
588
+ taskArn: task.taskArn,
589
+ taskDefinitionArn: task.taskDefinitionArn,
590
+ clusterArn: task.clusterArn,
591
+ serviceName: task.serviceName,
592
+ containers: task.containers.map(c => ({
593
+ name: c.name,
594
+ containerArn: c.containerArn,
595
+ lastStatus: c.lastStatus,
596
+ networkBindings: c.portMappings.map(pm => ({
597
+ containerPort: pm.containerPort,
598
+ hostPort: pm.hostPort,
599
+ protocol: pm.protocol
600
+ }))
601
+ })),
602
+ lastStatus: task.lastStatus,
603
+ desiredStatus: task.desiredStatus,
604
+ startedAt: task.startedAt,
605
+ stoppedAt: task.stoppedAt,
606
+ stopCode: task.stopCode,
607
+ createdAt: task.createdAt
608
+ });
609
+ }
610
+ }
611
+
612
+ return { tasks: taskList, failures: [] };
613
+ }
614
+
615
+ // ============ Helper Methods ============
616
+
617
+ getTaskDefinition(taskDefinition) {
618
+ const [family, revision] = taskDefinition.split(':').pop().split('/').pop().split(':');
619
+ return this.store.read(`taskdef_${family}_${revision || this.getLatestRevision(family)}`);
620
+ }
621
+
622
+ getLatestRevision(family) {
623
+ const files = this.store.list();
624
+ const revisions = files
625
+ .filter(f => f.startsWith(`taskdef_${family}_`))
626
+ .map(f => parseInt(f.split('_')[2]) || 0);
627
+
628
+ return Math.max(0, ...revisions);
629
+ }
630
+
631
+ getRunningTasksCount(clusterName) {
632
+ const cluster = this.clusters.get(clusterName);
633
+ if (!cluster) return 0;
634
+
635
+ let count = 0;
636
+ for (const taskArn of cluster.tasks) {
637
+ const task = this.tasks.get(taskArn);
638
+ if (task && task.lastStatus === 'RUNNING') {
639
+ count++;
640
+ }
641
+ }
642
+ return count;
643
+ }
644
+
645
+ getPendingTasksCount(clusterName) {
646
+ const cluster = this.clusters.get(clusterName);
647
+ if (!cluster) return 0;
648
+
649
+ let count = 0;
650
+ for (const taskArn of cluster.tasks) {
651
+ const task = this.tasks.get(taskArn);
652
+ if (task && task.lastStatus === 'PENDING') {
653
+ count++;
654
+ }
655
+ }
656
+ return count;
657
+ }
658
+
659
+ getActiveServicesCount(clusterName) {
660
+ const cluster = this.clusters.get(clusterName);
661
+ if (!cluster) return 0;
662
+ return cluster.services.length;
663
+ }
664
+
665
+ getServicesInCluster(clusterName) {
666
+ const cluster = this.clusters.get(clusterName);
667
+ if (!cluster) return [];
668
+ return cluster.services;
669
+ }
670
+
671
+ getTasksInCluster(clusterName) {
672
+ const cluster = this.clusters.get(clusterName);
673
+ if (!cluster) return [];
674
+ return cluster.tasks;
675
+ }
676
+
677
+ getRunningTasksCountForService(serviceName) {
678
+ const service = this.services.get(serviceName);
679
+ if (!service) return 0;
680
+
681
+ let count = 0;
682
+ for (const taskArn of service.tasks) {
683
+ const task = this.tasks.get(taskArn);
684
+ if (task && task.lastStatus === 'RUNNING') {
685
+ count++;
686
+ }
687
+ }
688
+ return count;
689
+ }
690
+
691
+ getPendingTasksCountForService(serviceName) {
692
+ const service = this.services.get(serviceName);
693
+ if (!service) return 0;
694
+
695
+ let count = 0;
696
+ for (const taskArn of service.tasks) {
697
+ const task = this.tasks.get(taskArn);
698
+ if (task && task.lastStatus === 'PENDING') {
699
+ count++;
700
+ }
701
+ }
702
+ return count;
703
+ }
704
+
705
+ getTasksForService(serviceName) {
706
+ const service = this.services.get(serviceName);
707
+ if (!service) return [];
708
+
709
+ const tasks = [];
710
+ for (const taskArn of service.tasks) {
711
+ const task = this.tasks.get(taskArn);
712
+ if (task) {
713
+ tasks.push(task);
714
+ }
715
+ }
716
+ return tasks;
717
+ }
718
+
719
+ updateServiceTasks(serviceName, newTaskDefinition) {
720
+ const service = this.services.get(serviceName);
721
+ if (!service) return;
722
+
723
+ const tasks = this.getTasksForService(serviceName);
724
+ for (const task of tasks) {
725
+ if (task.lastStatus === 'RUNNING') {
726
+ // Para a tarefa antiga e inicia nova
727
+ this.stopTask(task.taskArn);
728
+ this.runTask({
729
+ cluster: service.clusterArn.split('/').pop(),
730
+ taskDefinition: newTaskDefinition,
731
+ serviceName: service.name
732
+ });
733
+ }
734
+ }
735
+ }
736
+
737
+ // ============ Persistence ============
738
+
739
+ persistClusters() {
740
+ const clustersObj = {};
741
+ for (const [name, cluster] of this.clusters.entries()) {
742
+ clustersObj[name] = {
743
+ name: cluster.name,
744
+ arn: cluster.arn,
745
+ status: cluster.status,
746
+ createdAt: cluster.createdAt.toISOString(),
747
+ services: cluster.services,
748
+ tasks: cluster.tasks
749
+ };
750
+ }
751
+ this.store.write('__clusters__', clustersObj);
752
+ }
753
+
754
+ persistServices() {
755
+ const servicesObj = {};
756
+ for (const [name, service] of this.services.entries()) {
757
+ servicesObj[name] = {
758
+ name: service.name,
759
+ arn: service.arn,
760
+ clusterArn: service.clusterArn,
761
+ taskDefinition: service.taskDefinition,
762
+ desiredCount: service.desiredCount,
763
+ runningCount: service.runningCount,
764
+ status: service.status,
765
+ loadBalancers: service.loadBalancers,
766
+ networkConfiguration: service.networkConfiguration,
767
+ schedulingStrategy: service.schedulingStrategy,
768
+ createdAt: service.createdAt.toISOString(),
769
+ tasks: service.tasks
770
+ };
771
+ }
772
+ this.store.write('__services__', servicesObj);
773
+ }
774
+
775
+ persistTasks() {
776
+ const tasksObj = {};
777
+ for (const [arn, task] of this.tasks.entries()) {
778
+ tasksObj[arn] = {
779
+ taskArn: task.taskArn,
780
+ taskDefinitionArn: task.taskDefinitionArn,
781
+ clusterArn: task.clusterArn,
782
+ serviceName: task.serviceName,
783
+ containers: task.containers.map(c => ({
784
+ name: c.name,
785
+ image: c.image,
786
+ containerArn: c.containerArn,
787
+ lastStatus: c.lastStatus,
788
+ desiredStatus: c.desiredStatus,
789
+ portMappings: c.portMappings,
790
+ environment: c.environment,
791
+ startedAt: c.startedAt,
792
+ stoppedAt: c.stoppedAt
793
+ })),
794
+ lastStatus: task.lastStatus,
795
+ desiredStatus: task.desiredStatus,
796
+ startedAt: task.startedAt,
797
+ stoppedAt: task.stoppedAt,
798
+ stopCode: task.stopCode,
799
+ createdAt: task.createdAt.toISOString()
800
+ };
801
+ }
802
+ this.store.write('__tasks__', tasksObj);
803
+ }
804
+
805
+ persistTaskDefinitions() {
806
+ // Task definitions são persistidas individualmente
807
+ // Já salvas no registerTaskDefinition
808
+ }
809
+
810
+ async reset() {
811
+ // Para todas as tarefas em execução
812
+ for (const [taskArn] of this.tasks) {
813
+ await this.stopTask(taskArn);
814
+ }
815
+
816
+ this.clusters.clear();
817
+ this.services.clear();
818
+ this.tasks.clear();
819
+ this.containerProcesses.clear();
820
+
821
+ this.persistClusters();
822
+ this.persistServices();
823
+ this.persistTasks();
824
+
825
+ logger.debug('ECS: Todos os dados resetados');
826
+ }
827
+
828
+ getClustersCount() {
829
+ return this.clusters.size;
830
+ }
831
+
832
+ getServicesCount() {
833
+ return this.services.size;
834
+ }
835
+
836
+ getTasksCount() {
837
+ return this.tasks.size;
838
+ }
839
+
840
+ getRunningContainers() {
841
+ return this.containerProcesses.size;
842
+ }
843
+ }
844
+
845
845
  module.exports = ECSSimulator;