@gugananuvem/aws-local-simulator 1.0.9 → 1.0.10

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 CHANGED
@@ -24,9 +24,10 @@ Simulador local completo para serviços AWS. Desenvolva e teste suas aplicaçõe
24
24
 
25
25
  ```bash
26
26
  npm install --save-dev aws-local-simulator
27
+ ```
27
28
  🚀 Uso Rápido
28
29
  1. Crie um arquivo de configuração aws-local-simulator.json:
29
- json
30
+ ```json
30
31
  {
31
32
  "services": {
32
33
  "dynamodb": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gugananuvem/aws-local-simulator",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Simulador local completo para serviços AWS",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -109,6 +109,6 @@
109
109
  "optional": true
110
110
  }
111
111
  },
112
- "buildDate": "2026-03-29T02:36:24.504Z",
112
+ "buildDate": "2026-03-29T15:37:11.579Z",
113
113
  "published": true
114
114
  }
@@ -9,8 +9,8 @@ module.exports = {
9
9
  s3: true,
10
10
  sqs: true,
11
11
  lambda: true,
12
- sns: false, // Desabilitado por padrão
13
- eventbridge: false, // Desabilitado por padrão
12
+ sns: false,
13
+ eventbridge: false,
14
14
  ecs: false,
15
15
  cognito: false,
16
16
  apigateway: false,
@@ -13,8 +13,9 @@ class EnvLoader {
13
13
  dynamodb: this.getEnvBool('AWS_LOCAL_SIMULATOR_DYNAMODB', true),
14
14
  s3: this.getEnvBool('AWS_LOCAL_SIMULATOR_S3', true),
15
15
  sqs: this.getEnvBool('AWS_LOCAL_SIMULATOR_SQS', true),
16
- lambda: this.getEnvBool('AWS_LOCAL_SIMULATOR_LAMBDA', true),
17
16
  sns: this.getEnvBool('AWS_LOCAL_SIMULATOR_SNS', false),
17
+ lambda: this.getEnvBool('AWS_LOCAL_SIMULATOR_LAMBDA', true),
18
+ apigateway: this.getEnvBool('AWS_LOCAL_SIMULATOR_APIGATEWAY', false),
18
19
  eventbridge: this.getEnvBool('AWS_LOCAL_SIMULATOR_EVENTBRIDGE', false)
19
20
  };
20
21
 
@@ -25,6 +26,7 @@ class EnvLoader {
25
26
  sqs: this.getEnvInt('AWS_LOCAL_SIMULATOR_SQS_PORT', 9324),
26
27
  lambda: this.getEnvInt('AWS_LOCAL_SIMULATOR_LAMBDA_PORT', 3001),
27
28
  sns: this.getEnvInt('AWS_LOCAL_SIMULATOR_SNS_PORT', 9911),
29
+ apigateway: this.getEnvInt('AWS_LOCAL_SIMULATOR_APIGATEWAY_PORT', 4567),
28
30
  eventbridge: this.getEnvInt('AWS_LOCAL_SIMULATOR_EVENTBRIDGE_PORT', 4010)
29
31
  };
30
32
 
package/src/server.js CHANGED
@@ -16,6 +16,7 @@ const SNSService = require("./services/sns");
16
16
  const EventBridgeService = require("./services/eventbridge");
17
17
  const CognitoService = require("./services/cognito");
18
18
  const APIGatewayService = require("./services/apigateway");
19
+ const ECSService = require("./services/ecs");
19
20
 
20
21
  class Server {
21
22
  constructor(config) {
@@ -71,6 +72,8 @@ class Server {
71
72
  { name: "sns", class: SNSService, depends: [] },
72
73
  { name: "eventbridge", class: EventBridgeService, depends: [] },
73
74
  { name: "cognito", class: CognitoService, depends: [] },
75
+ { name: "ecs", class: ECSService, depends: [] },
76
+ { name: "cognito", class:CognitoService, depends: [] },
74
77
  { name: "apigateway", class: APIGatewayService, depends: [] },
75
78
  ];
76
79
 
@@ -0,0 +1,66 @@
1
+ /**
2
+ * ECS Service - Simulador de Elastic Container Service
3
+ * Suporta: Fargate, EC2 launch types, Tasks, Services
4
+ */
5
+
6
+ const ECSServer = require('./server');
7
+ const ECSSimulator = require('./simulator');
8
+
9
+ class ECSService {
10
+ constructor(config) {
11
+ this.config = config;
12
+ this.name = 'ecs';
13
+ this.port = config.ports.ecs || 8080;
14
+ this.server = null;
15
+ this.simulator = null;
16
+ this.isRunning = false;
17
+ }
18
+
19
+ async initialize() {
20
+ const logger = require('../../utils/logger');
21
+ logger.debug(`Inicializando ECS Service na porta ${this.port}...`);
22
+
23
+ this.simulator = new ECSSimulator(this.config);
24
+ await this.simulator.initialize();
25
+
26
+ this.server = new ECSServer(this.port, this.config);
27
+ this.server.simulator = this.simulator;
28
+
29
+ await this.server.initialize();
30
+
31
+ logger.debug('ECS Service inicializado');
32
+ }
33
+
34
+ async start() {
35
+ if (this.isRunning) return;
36
+ await this.server.start();
37
+ this.isRunning = true;
38
+ }
39
+
40
+ async stop() {
41
+ if (!this.isRunning) return;
42
+ await this.server.stop();
43
+ this.isRunning = false;
44
+ }
45
+
46
+ async reset() {
47
+ await this.simulator.reset();
48
+ }
49
+
50
+ getStatus() {
51
+ return {
52
+ running: this.isRunning,
53
+ port: this.port,
54
+ endpoint: `http://localhost:${this.port}`,
55
+ clustersCount: this.simulator?.getClustersCount() || 0,
56
+ servicesCount: this.simulator?.getServicesCount() || 0,
57
+ tasksCount: this.simulator?.getTasksCount() || 0
58
+ };
59
+ }
60
+
61
+ getSimulator() {
62
+ return this.simulator;
63
+ }
64
+ }
65
+
66
+ module.exports = ECSService;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * ECS Server - Servidor HTTP para ECS API
3
+ */
4
+
5
+ const express = require('express');
6
+ const logger = require('../../utils/logger');
7
+
8
+ class ECSServer {
9
+ constructor(port, config) {
10
+ this.port = port;
11
+ this.config = config;
12
+ this.app = express();
13
+ this.simulator = null;
14
+ this.server = null;
15
+ this.setupMiddlewares();
16
+ }
17
+
18
+ setupMiddlewares() {
19
+ this.app.use(express.json());
20
+
21
+ if (logger.currentLogLevel === 'verboso') {
22
+ this.app.use((req, res, next) => {
23
+ const start = Date.now();
24
+ res.on('finish', () => {
25
+ const duration = Date.now() - start;
26
+ logger.verboso(`ECS: ${req.method} ${req.path} - ${duration}ms`);
27
+ });
28
+ next();
29
+ });
30
+ }
31
+ }
32
+
33
+ async initialize() {
34
+ this.setupRoutes();
35
+ logger.debug('ECS Server inicializado');
36
+ }
37
+
38
+ setupRoutes() {
39
+ // Health check
40
+ this.app.get('/health', (req, res) => {
41
+ res.json({
42
+ status: 'healthy',
43
+ service: 'ecs-simulator',
44
+ version: '1.0.0'
45
+ });
46
+ });
47
+
48
+ // Cluster operations
49
+ this.app.post('/clusters', (req, res) => {
50
+ const { clusterName } = req.body;
51
+ const result = this.simulator.createCluster(clusterName);
52
+ if (result.error) {
53
+ res.status(result.status).json(result.error);
54
+ } else {
55
+ res.json(result.cluster);
56
+ }
57
+ });
58
+
59
+ this.app.get('/clusters', (req, res) => {
60
+ const clusters = this.simulator.listClusters();
61
+ res.json({ clusterArns: clusters.map(c => `arn:aws:ecs:local:000000000000:cluster/${c}`) });
62
+ });
63
+
64
+ this.app.get('/clusters/:clusterName', (req, res) => {
65
+ try {
66
+ const result = this.simulator.describeCluster(req.params.clusterName);
67
+ res.json(result);
68
+ } catch (error) {
69
+ res.status(404).json({ error: error.message });
70
+ }
71
+ });
72
+
73
+ this.app.delete('/clusters/:clusterName', (req, res) => {
74
+ const result = this.simulator.deleteCluster(req.params.clusterName);
75
+ if (result.error) {
76
+ res.status(result.status).json(result.error);
77
+ } else {
78
+ res.json({ message: 'Cluster deleted' });
79
+ }
80
+ });
81
+
82
+ // Task Definition operations
83
+ this.app.post('/task-definitions', (req, res) => {
84
+ const result = this.simulator.registerTaskDefinition(req.body);
85
+ if (result.error) {
86
+ res.status(result.status).json(result.error);
87
+ } else {
88
+ res.json(result.taskDefinition);
89
+ }
90
+ });
91
+
92
+ // Service operations
93
+ this.app.post('/services', (req, res) => {
94
+ const result = this.simulator.createService(req.body);
95
+ if (result.error) {
96
+ res.status(result.status).json(result.error);
97
+ } else {
98
+ res.json(result.service);
99
+ }
100
+ });
101
+
102
+ this.app.put('/services/:serviceName', (req, res) => {
103
+ const result = this.simulator.updateService({
104
+ ...req.body,
105
+ service: req.params.serviceName
106
+ });
107
+ if (result.error) {
108
+ res.status(result.status).json(result.error);
109
+ } else {
110
+ res.json(result.service);
111
+ }
112
+ });
113
+
114
+ // Task operations
115
+ this.app.post('/tasks', (req, res) => {
116
+ this.simulator.runTask(req.body).then(result => {
117
+ if (result.error) {
118
+ res.status(result.status).json(result.error);
119
+ } else {
120
+ res.json(result.task);
121
+ }
122
+ });
123
+ });
124
+
125
+ this.app.get('/tasks', (req, res) => {
126
+ const result = this.simulator.listTasks(req.query);
127
+ res.json(result);
128
+ });
129
+
130
+ this.app.post('/tasks/describe', (req, res) => {
131
+ const result = this.simulator.describeTasks(req.body);
132
+ res.json(result);
133
+ });
134
+
135
+ this.app.post('/tasks/:taskArn/stop', (req, res) => {
136
+ this.simulator.stopTask(req.params.taskArn).then(result => {
137
+ if (result.error) {
138
+ res.status(result.status).json(result.error);
139
+ } else {
140
+ res.json(result.task);
141
+ }
142
+ });
143
+ });
144
+
145
+ // Admin endpoints
146
+ this.setupAdminRoutes();
147
+ }
148
+
149
+ setupAdminRoutes() {
150
+ this.app.get('/__admin/clusters', (req, res) => {
151
+ res.json({
152
+ clusters: this.simulator.getClustersCount(),
153
+ services: this.simulator.getServicesCount(),
154
+ tasks: this.simulator.getTasksCount(),
155
+ runningContainers: this.simulator.getRunningContainers()
156
+ });
157
+ });
158
+
159
+ this.app.get('/__admin/clusters/:clusterName/details', (req, res) => {
160
+ const cluster = this.simulator.clusters.get(req.params.clusterName);
161
+ if (cluster) {
162
+ res.json(cluster);
163
+ } else {
164
+ res.status(404).json({ error: 'Cluster not found' });
165
+ }
166
+ });
167
+
168
+ this.app.get('/__admin/containers', (req, res) => {
169
+ const containers = [];
170
+ for (const [id, process] of this.simulator.containerProcesses) {
171
+ containers.push({
172
+ containerId: id,
173
+ taskArn: process.taskArn,
174
+ container: process.container,
175
+ running: process.running,
176
+ startTime: process.startTime
177
+ });
178
+ }
179
+ res.json(containers);
180
+ });
181
+
182
+ this.app.get('/__admin/ports', (req, res) => {
183
+ res.json({
184
+ availablePorts: Array.from(this.simulator.availablePorts),
185
+ usedPorts: this.getUsedPorts()
186
+ });
187
+ });
188
+ }
189
+
190
+ getUsedPorts() {
191
+ const usedPorts = [];
192
+ for (const [_, process] of this.simulator.containerProcesses) {
193
+ for (const mapping of process.container.portMappings) {
194
+ if (mapping.hostPort) {
195
+ usedPorts.push(mapping.hostPort);
196
+ }
197
+ }
198
+ }
199
+ return usedPorts;
200
+ }
201
+
202
+ start() {
203
+ return new Promise((resolve) => {
204
+ this.server = this.app.listen(this.port, () => {
205
+ logger.info(`🐳 ECS/Fargate rodando em http://localhost:${this.port}`);
206
+ resolve();
207
+ });
208
+ });
209
+ }
210
+
211
+ stop() {
212
+ return new Promise((resolve) => {
213
+ if (this.server) {
214
+ this.server.close(() => resolve());
215
+ } else {
216
+ resolve();
217
+ }
218
+ });
219
+ }
220
+
221
+ getStatus() {
222
+ return {
223
+ running: !!this.server,
224
+ port: this.port,
225
+ endpoint: `http://localhost:${this.port}`,
226
+ clustersCount: this.simulator?.getClustersCount() || 0,
227
+ servicesCount: this.simulator?.getServicesCount() || 0,
228
+ tasksCount: this.simulator?.getTasksCount() || 0,
229
+ runningContainers: this.simulator?.getRunningContainers() || 0
230
+ };
231
+ }
232
+ }
233
+
234
+ module.exports = ECSServer;
@@ -0,0 +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
+
845
+ module.exports = ECSSimulator;
@@ -85,8 +85,15 @@ class LambdaSimulator {
85
85
  const runMiddlewares = async (index) => {
86
86
  if (index >= middlewares.length) {
87
87
  // Executa handler
88
+
88
89
  result = await this.executeHandler(matchedRoute.handler, event);
89
90
  handled = true;
91
+
92
+ console.log(`✅ Resposta: ${result.statusCode}`);
93
+ res
94
+ .status(result.statusCode || 200)
95
+ .set(result.headers || {})
96
+ .send(result.body ? JSON.parse(result.body) : null);
90
97
  return;
91
98
  }
92
99
 
@@ -115,7 +122,7 @@ class LambdaSimulator {
115
122
  if (!handled && result) {
116
123
  return this.formatResponse(result);
117
124
  }
118
-
125
+
119
126
  return null;
120
127
  }
121
128
 
@@ -6,7 +6,8 @@
6
6
  "lambda": true,
7
7
  "sns": false,
8
8
  "eventbridge": false,
9
- "apigateway": true
9
+ "apigateway": true,
10
+ "ecs": false
10
11
  },
11
12
  "ports": {
12
13
  "dynamodb": 8000,
@@ -39,12 +40,8 @@
39
40
  "tables": [
40
41
  {
41
42
  "TableName": "users-table",
42
- "KeySchema": [
43
- { "AttributeName": "userId", "KeyType": "HASH" }
44
- ],
45
- "AttributeDefinitions": [
46
- { "AttributeName": "userId", "AttributeType": "S" }
47
- ],
43
+ "KeySchema": [{ "AttributeName": "userId", "KeyType": "HASH" }],
44
+ "AttributeDefinitions": [{ "AttributeName": "userId", "AttributeType": "S" }],
48
45
  "ProvisionedThroughput": {
49
46
  "ReadCapacityUnits": 5,
50
47
  "WriteCapacityUnits": 5
@@ -161,5 +158,46 @@
161
158
  }
162
159
  ]
163
160
  }
161
+ ],
162
+ "ecs": {
163
+ "clusters": ["production", "staging", "development"],
164
+ "defaultTaskCpu": 512,
165
+ "defaultTaskMemory": 1024,
166
+ "containerRuntime": "simulate"
167
+ },
168
+ "taskDefinitions": [
169
+ {
170
+ "family": "my-app",
171
+ "containerDefinitions": [
172
+ {
173
+ "name": "web",
174
+ "image": "nginx:alpine",
175
+ "cpu": 256,
176
+ "memory": 512,
177
+ "essential": true,
178
+ "portMappings": [
179
+ {
180
+ "containerPort": 80,
181
+ "hostPort": 0,
182
+ "protocol": "tcp"
183
+ }
184
+ ],
185
+ "environment": [{ "name": "ENVIRONMENT", "value": "local" }]
186
+ }
187
+ ],
188
+ "networkMode": "awsvpc",
189
+ "cpu": "512",
190
+ "memory": "1024"
191
+ }
192
+ ],
193
+ "services-ecs": [
194
+ {
195
+ "name": "my-web-service",
196
+ "cluster": "development",
197
+ "taskDefinition": "my-app",
198
+ "desiredCount": 2,
199
+ "schedulingStrategy": "REPLICA"
200
+ }
164
201
  ]
165
- }
202
+
203
+ }