@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 +2 -1
- package/package.json +2 -2
- package/src/config/default-config.js +2 -2
- package/src/config/env-loader.js +3 -1
- package/src/server.js +3 -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/lambda/simulator.js +8 -1
- package/src/template/config-template.json +46 -8
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.
|
|
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-
|
|
112
|
+
"buildDate": "2026-03-29T15:37:11.579Z",
|
|
113
113
|
"published": true
|
|
114
114
|
}
|
package/src/config/env-loader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|