@gugananuvem/aws-local-simulator 1.0.8 → 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 +2 -1
- package/bin/aws-local-simulator.js +62 -1
- package/package.json +10 -7
- package/src/config/config-loader.js +113 -0
- package/src/config/default-config.js +65 -0
- package/src/config/env-loader.js +69 -0
- package/src/index.js +131 -1
- package/src/index.mjs +124 -0
- package/src/server.js +222 -0
- package/src/services/apigateway/index.js +67 -0
- package/src/services/apigateway/server.js +435 -0
- package/src/services/apigateway/simulator.js +1252 -0
- package/src/services/cognito/index.js +66 -0
- package/src/services/cognito/server.js +229 -0
- package/src/services/cognito/simulator.js +848 -0
- package/src/services/dynamodb/index.js +71 -0
- package/src/services/dynamodb/server.js +122 -0
- package/src/services/dynamodb/simulator.js +614 -0
- package/src/services/ecs/index.js +66 -0
- package/src/services/ecs/server.js +234 -0
- package/src/services/ecs/simulator.js +845 -0
- package/src/services/eventbridge/index.js +85 -0
- package/src/services/index.js +19 -0
- package/src/services/lambda/handler-loader.js +173 -0
- package/src/services/lambda/index.js +73 -0
- package/src/services/lambda/route-registry.js +275 -0
- package/src/services/lambda/server.js +153 -0
- package/src/services/lambda/simulator.js +285 -0
- package/src/services/s3/index.js +70 -0
- package/src/services/s3/server.js +239 -0
- package/src/services/s3/simulator.js +740 -0
- package/src/services/sns/index.js +76 -0
- package/src/services/sqs/index.js +96 -0
- package/src/services/sqs/server.js +274 -0
- package/src/services/sqs/simulator.js +660 -0
- package/src/template/aws-config-template.js +88 -0
- package/src/template/aws-config-template.mjs +91 -0
- package/src/template/config-template.json +203 -0
- package/src/utils/aws-config.js +92 -0
- package/src/utils/local-store.js +68 -0
- package/src/utils/logger.js +60 -0
|
@@ -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;
|