@fenwave/agent 1.1.0
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/.claude/settings.local.json +11 -0
- package/Dockerfile +12 -0
- package/LICENSE +29 -0
- package/README.md +434 -0
- package/auth.js +276 -0
- package/cli-commands.js +1185 -0
- package/containerManager.js +385 -0
- package/convert-to-esm.sh +62 -0
- package/docker-actions/apps.js +3256 -0
- package/docker-actions/config-transformer.js +380 -0
- package/docker-actions/containers.js +346 -0
- package/docker-actions/general.js +171 -0
- package/docker-actions/images.js +1128 -0
- package/docker-actions/logs.js +188 -0
- package/docker-actions/metrics.js +270 -0
- package/docker-actions/registry.js +1100 -0
- package/docker-actions/terminal.js +247 -0
- package/docker-actions/volumes.js +696 -0
- package/helper-functions.js +193 -0
- package/index.html +60 -0
- package/index.js +988 -0
- package/package.json +49 -0
- package/setup/setupWizard.js +499 -0
- package/store/agentSessionStore.js +51 -0
- package/store/agentStore.js +113 -0
- package/store/configStore.js +174 -0
- package/store/deviceCredentialStore.js +107 -0
- package/store/npmTokenStore.js +65 -0
- package/store/registryStore.js +329 -0
- package/store/setupState.js +147 -0
- package/utils/deviceInfo.js +98 -0
- package/utils/ecrAuth.js +225 -0
- package/utils/encryption.js +112 -0
- package/utils/envSetup.js +54 -0
- package/utils/errorHandler.js +327 -0
- package/utils/prerequisites.js +323 -0
- package/utils/prompts.js +318 -0
- package/websocket-server.js +364 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Transformer
|
|
3
|
+
*
|
|
4
|
+
* Transforms platform-agnostic deployment configuration into platform-specific formats.
|
|
5
|
+
* Supports Docker Compose and Helm transformations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Transforms deployment config to Docker Compose format
|
|
10
|
+
* Splits environment variables by scope (container, host, secret)
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} deploymentConfig - Universal deployment configuration
|
|
13
|
+
* @param {Array} deploymentConfig.environment - Environment variables with scopes
|
|
14
|
+
* @param {Object} deploymentConfig.resources - Resource limits (memory, cpu)
|
|
15
|
+
* @param {Object} deploymentConfig.scaling - Scaling config (replicas)
|
|
16
|
+
* @param {Object} deploymentConfig.volumes - Volume configurations
|
|
17
|
+
* @returns {Object} Docker Compose configuration with separated env vars
|
|
18
|
+
*/
|
|
19
|
+
export function toDockerComposeConfig(deploymentConfig) {
|
|
20
|
+
if (!deploymentConfig) {
|
|
21
|
+
return {
|
|
22
|
+
containerEnv: {},
|
|
23
|
+
hostEnv: {},
|
|
24
|
+
secretRefs: {},
|
|
25
|
+
resources: null,
|
|
26
|
+
replicas: 1,
|
|
27
|
+
volumes: null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = {
|
|
32
|
+
containerEnv: {},
|
|
33
|
+
hostEnv: {},
|
|
34
|
+
secretRefs: {},
|
|
35
|
+
resources: null,
|
|
36
|
+
replicas: 1,
|
|
37
|
+
volumes: null,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Process environment variables by scope
|
|
41
|
+
if (
|
|
42
|
+
deploymentConfig.environment &&
|
|
43
|
+
Array.isArray(deploymentConfig.environment)
|
|
44
|
+
) {
|
|
45
|
+
for (const envVar of deploymentConfig.environment) {
|
|
46
|
+
const { name, value, scope } = envVar;
|
|
47
|
+
|
|
48
|
+
// Skip invalid entries
|
|
49
|
+
if (!name || !value) continue;
|
|
50
|
+
|
|
51
|
+
switch (scope) {
|
|
52
|
+
case "container":
|
|
53
|
+
result.containerEnv[name] = value;
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
case "host":
|
|
57
|
+
result.hostEnv[name] = value;
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
case "secret":
|
|
61
|
+
// Store secret references (e.g., "${SECRET_NAME}")
|
|
62
|
+
// These will be resolved from host environment at runtime
|
|
63
|
+
result.secretRefs[name] = value;
|
|
64
|
+
// Also add to container env so docker-compose can interpolate
|
|
65
|
+
result.containerEnv[name] = value;
|
|
66
|
+
break;
|
|
67
|
+
|
|
68
|
+
default:
|
|
69
|
+
// Default to container scope
|
|
70
|
+
result.containerEnv[name] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Process resource limits
|
|
76
|
+
if (deploymentConfig.resources) {
|
|
77
|
+
result.resources = {
|
|
78
|
+
memory: deploymentConfig.resources.memory || null,
|
|
79
|
+
cpu: deploymentConfig.resources.cpu || null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Process scaling
|
|
84
|
+
if (deploymentConfig.scaling && deploymentConfig.scaling.replicas) {
|
|
85
|
+
result.replicas = deploymentConfig.scaling.replicas;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Process volumes
|
|
89
|
+
if (deploymentConfig.volumes) {
|
|
90
|
+
result.volumes = deploymentConfig.volumes;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Generates a bash script to export host environment variables
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} hostEnv - Key-value pairs of host environment variables
|
|
100
|
+
* @returns {string} Bash script content
|
|
101
|
+
*/
|
|
102
|
+
export function generateHostEnvScript(hostEnv) {
|
|
103
|
+
if (!hostEnv || Object.keys(hostEnv).length === 0) {
|
|
104
|
+
return "#!/bin/bash\n# No host environment variables configured\n";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lines = ["#!/bin/bash", "# Host environment variables", ""];
|
|
108
|
+
|
|
109
|
+
for (const [name, value] of Object.entries(hostEnv)) {
|
|
110
|
+
// Escape special characters in values
|
|
111
|
+
const escapedValue = value.replace(/"/g, '\\"').replace(/\$/g, "\\$");
|
|
112
|
+
lines.push(`export ${name}="${escapedValue}"`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
lines.push("");
|
|
116
|
+
return lines.join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Generates Docker Compose deploy section for resources and replicas
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} resources - Resource limits {memory, cpu}
|
|
123
|
+
* @param {number} replicas - Number of replicas
|
|
124
|
+
* @returns {Object|null} Docker Compose deploy configuration
|
|
125
|
+
*/
|
|
126
|
+
export function generateComposeDeploy(resources, replicas) {
|
|
127
|
+
const deploy = {};
|
|
128
|
+
|
|
129
|
+
// Add replicas
|
|
130
|
+
if (replicas && replicas > 1) {
|
|
131
|
+
deploy.replicas = replicas;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add resource limits
|
|
135
|
+
if (resources && (resources.memory || resources.cpu)) {
|
|
136
|
+
deploy.resources = {
|
|
137
|
+
limits: {},
|
|
138
|
+
reservations: {},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (resources.memory) {
|
|
142
|
+
deploy.resources.limits.memory = resources.memory;
|
|
143
|
+
// Set reservations to 50% of limits
|
|
144
|
+
deploy.resources.reservations.memory = resources.memory;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (resources.cpu) {
|
|
148
|
+
deploy.resources.limits.cpus = resources.cpu;
|
|
149
|
+
// Set reservations to 50% of limits
|
|
150
|
+
const cpuValue = parseFloat(resources.cpu);
|
|
151
|
+
if (!isNaN(cpuValue)) {
|
|
152
|
+
deploy.resources.reservations.cpus = (cpuValue * 0.5).toString();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return Object.keys(deploy).length > 0 ? deploy : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generates .env file content for Docker Compose
|
|
162
|
+
* Includes both container and secret reference variables
|
|
163
|
+
*
|
|
164
|
+
* @param {Object} containerEnv - Container environment variables
|
|
165
|
+
* @param {Object} secretRefs - Secret reference variables
|
|
166
|
+
* @returns {string} .env file content
|
|
167
|
+
*/
|
|
168
|
+
export function generateEnvFile(containerEnv, secretRefs) {
|
|
169
|
+
const lines = ["# Environment variables for Docker Compose", ""];
|
|
170
|
+
|
|
171
|
+
// Add container environment variables
|
|
172
|
+
if (containerEnv && Object.keys(containerEnv).length > 0) {
|
|
173
|
+
lines.push("# Container environment variables");
|
|
174
|
+
for (const [name, value] of Object.entries(containerEnv)) {
|
|
175
|
+
// Skip secret refs (they're added separately)
|
|
176
|
+
if (secretRefs && secretRefs[name]) continue;
|
|
177
|
+
lines.push(`${name}=${value}`);
|
|
178
|
+
}
|
|
179
|
+
lines.push("");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add secret references with comments
|
|
183
|
+
if (secretRefs && Object.keys(secretRefs).length > 0) {
|
|
184
|
+
lines.push("# Secret references (resolved from host environment)");
|
|
185
|
+
for (const [name, value] of Object.entries(secretRefs)) {
|
|
186
|
+
lines.push(`${name}=${value}`);
|
|
187
|
+
}
|
|
188
|
+
lines.push("");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Validates deployment configuration
|
|
196
|
+
*
|
|
197
|
+
* @param {Object} deploymentConfig - Universal deployment configuration
|
|
198
|
+
* @returns {Object} Validation result {valid: boolean, errors: string[]}
|
|
199
|
+
*/
|
|
200
|
+
export function validateDeploymentConfig(deploymentConfig) {
|
|
201
|
+
const errors = [];
|
|
202
|
+
|
|
203
|
+
if (!deploymentConfig) {
|
|
204
|
+
return { valid: true, errors: [] }; // Config is optional
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Validate environment variables
|
|
208
|
+
if (deploymentConfig.environment) {
|
|
209
|
+
if (!Array.isArray(deploymentConfig.environment)) {
|
|
210
|
+
errors.push("environment must be an array");
|
|
211
|
+
} else {
|
|
212
|
+
deploymentConfig.environment.forEach((envVar, index) => {
|
|
213
|
+
if (!envVar.name) {
|
|
214
|
+
errors.push(`environment[${index}]: name is required`);
|
|
215
|
+
}
|
|
216
|
+
if (!envVar.value) {
|
|
217
|
+
errors.push(`environment[${index}]: value is required`);
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
!envVar.scope ||
|
|
221
|
+
!["container", "host", "secret"].includes(envVar.scope)
|
|
222
|
+
) {
|
|
223
|
+
errors.push(
|
|
224
|
+
`environment[${index}]: scope must be 'container', 'host', or 'secret'`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Validate resources
|
|
232
|
+
if (deploymentConfig.resources) {
|
|
233
|
+
const { memory, cpu } = deploymentConfig.resources;
|
|
234
|
+
|
|
235
|
+
if (memory && typeof memory !== "string") {
|
|
236
|
+
errors.push('resources.memory must be a string (e.g., "512Mi", "1Gi")');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (cpu && typeof cpu !== "string") {
|
|
240
|
+
errors.push('resources.cpu must be a string (e.g., "500m", "1")');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Validate scaling
|
|
245
|
+
if (deploymentConfig.scaling) {
|
|
246
|
+
const { replicas } = deploymentConfig.scaling;
|
|
247
|
+
|
|
248
|
+
if (replicas !== undefined) {
|
|
249
|
+
if (typeof replicas !== "number" || replicas < 1) {
|
|
250
|
+
errors.push("scaling.replicas must be a number >= 1");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Validate volumes
|
|
256
|
+
if (deploymentConfig.volumes) {
|
|
257
|
+
if (typeof deploymentConfig.volumes !== "object") {
|
|
258
|
+
errors.push("volumes must be an object");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
valid: errors.length === 0,
|
|
264
|
+
errors,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Transforms deployment config to Helm values format
|
|
270
|
+
* (For future Kubernetes deployments)
|
|
271
|
+
*
|
|
272
|
+
* @param {Object} deploymentConfig - Universal deployment configuration
|
|
273
|
+
* @returns {Object} Helm values object
|
|
274
|
+
*/
|
|
275
|
+
export function toHelmValues(deploymentConfig) {
|
|
276
|
+
if (!deploymentConfig) {
|
|
277
|
+
return {};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const helmValues = {};
|
|
281
|
+
|
|
282
|
+
// Process environment variables
|
|
283
|
+
if (
|
|
284
|
+
deploymentConfig.environment &&
|
|
285
|
+
Array.isArray(deploymentConfig.environment)
|
|
286
|
+
) {
|
|
287
|
+
helmValues.env = [];
|
|
288
|
+
|
|
289
|
+
for (const envVar of deploymentConfig.environment) {
|
|
290
|
+
const { name, value, scope } = envVar;
|
|
291
|
+
|
|
292
|
+
if (!name || !value) continue;
|
|
293
|
+
|
|
294
|
+
switch (scope) {
|
|
295
|
+
case "container":
|
|
296
|
+
// Direct environment variable
|
|
297
|
+
helmValues.env.push({
|
|
298
|
+
name,
|
|
299
|
+
value,
|
|
300
|
+
});
|
|
301
|
+
break;
|
|
302
|
+
|
|
303
|
+
case "secret":
|
|
304
|
+
// Secret reference - extract secret name and key
|
|
305
|
+
// Expected format: ${SECRET_NAME} or ${SECRET_NAME:KEY}
|
|
306
|
+
const secretMatch = value.match(/\$\{([^:}]+)(?::([^}]+))?\}/);
|
|
307
|
+
if (secretMatch) {
|
|
308
|
+
const secretName = secretMatch[1];
|
|
309
|
+
const secretKey = secretMatch[2] || name.toLowerCase();
|
|
310
|
+
|
|
311
|
+
helmValues.env.push({
|
|
312
|
+
name,
|
|
313
|
+
valueFrom: {
|
|
314
|
+
secretKeyRef: {
|
|
315
|
+
name: secretName,
|
|
316
|
+
key: secretKey,
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
} else {
|
|
321
|
+
// Invalid secret reference, treat as regular value
|
|
322
|
+
helmValues.env.push({ name, value });
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
case "host":
|
|
327
|
+
// Host env vars don't apply to Kubernetes - skip or warn
|
|
328
|
+
console.warn(
|
|
329
|
+
`Host environment variable ${name} will not be included in Helm values`
|
|
330
|
+
);
|
|
331
|
+
break;
|
|
332
|
+
|
|
333
|
+
default:
|
|
334
|
+
helmValues.env.push({ name, value });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Process resource limits
|
|
340
|
+
if (deploymentConfig.resources) {
|
|
341
|
+
helmValues.resources = {
|
|
342
|
+
limits: {},
|
|
343
|
+
requests: {},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
if (deploymentConfig.resources.memory) {
|
|
347
|
+
helmValues.resources.limits.memory = deploymentConfig.resources.memory;
|
|
348
|
+
helmValues.resources.requests.memory = deploymentConfig.resources.memory;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (deploymentConfig.resources.cpu) {
|
|
352
|
+
helmValues.resources.limits.cpu = deploymentConfig.resources.cpu;
|
|
353
|
+
// Request 50% of CPU limit
|
|
354
|
+
const cpuValue = parseFloat(deploymentConfig.resources.cpu);
|
|
355
|
+
if (!isNaN(cpuValue)) {
|
|
356
|
+
helmValues.resources.requests.cpu = (cpuValue * 0.5).toString();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Process scaling
|
|
362
|
+
if (deploymentConfig.scaling && deploymentConfig.scaling.replicas) {
|
|
363
|
+
helmValues.replicaCount = deploymentConfig.scaling.replicas;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Process volumes
|
|
367
|
+
if (deploymentConfig.volumes) {
|
|
368
|
+
helmValues.persistence = {};
|
|
369
|
+
|
|
370
|
+
for (const [name, size] of Object.entries(deploymentConfig.volumes)) {
|
|
371
|
+
helmValues.persistence[name] = {
|
|
372
|
+
enabled: true,
|
|
373
|
+
size,
|
|
374
|
+
accessMode: "ReadWriteOnce",
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return helmValues;
|
|
380
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import Docker from "dockerode";
|
|
2
|
+
import { formatUptime } from "../helper-functions.js";
|
|
3
|
+
import os from "os";
|
|
4
|
+
|
|
5
|
+
// Create Docker client
|
|
6
|
+
let docker;
|
|
7
|
+
try {
|
|
8
|
+
docker = new Docker();
|
|
9
|
+
} catch (error) {
|
|
10
|
+
console.error(`Failed to connect to Docker daemon: ${error.message}`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Format container for response
|
|
15
|
+
const formatContainer = async (container) => {
|
|
16
|
+
try {
|
|
17
|
+
const containerInfo = await container.inspect();
|
|
18
|
+
const stats = await container.stats({ stream: false });
|
|
19
|
+
|
|
20
|
+
// Calculate CPU usage percentage using a simpler approach
|
|
21
|
+
let cpuPercent = 0;
|
|
22
|
+
try {
|
|
23
|
+
const cpuDelta =
|
|
24
|
+
stats.cpu_stats.cpu_usage.total_usage -
|
|
25
|
+
stats.precpu_stats.cpu_usage.total_usage;
|
|
26
|
+
const systemCpuDelta =
|
|
27
|
+
stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
|
|
28
|
+
const cpuCores = stats.cpu_stats.online_cpus || os.cpus().length;
|
|
29
|
+
|
|
30
|
+
if (systemCpuDelta > 0 && cpuDelta > 0) {
|
|
31
|
+
cpuPercent = (cpuDelta / systemCpuDelta) * cpuCores * 100;
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error("Error calculating CPU percentage:", err.message);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Calculate memory usage percentage
|
|
38
|
+
let memoryPercent = 0;
|
|
39
|
+
try {
|
|
40
|
+
const memoryUsage = stats.memory_stats.usage || 0;
|
|
41
|
+
const memoryLimit = stats.memory_stats.limit || 1;
|
|
42
|
+
memoryPercent = (memoryUsage / memoryLimit) * 100;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error("Error calculating memory percentage:", err.message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Format ports
|
|
48
|
+
const ports = [];
|
|
49
|
+
if (containerInfo.NetworkSettings && containerInfo.NetworkSettings.Ports) {
|
|
50
|
+
Object.entries(containerInfo.NetworkSettings.Ports).forEach(
|
|
51
|
+
([containerPort, hostBindings]) => {
|
|
52
|
+
if (hostBindings) {
|
|
53
|
+
hostBindings.forEach((binding) => {
|
|
54
|
+
ports.push(`${binding.HostPort}:${containerPort}`);
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
ports.push(containerPort);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Calculate uptime
|
|
64
|
+
const startTime = new Date(containerInfo.State.StartedAt).getTime();
|
|
65
|
+
const uptime = containerInfo.State.Running
|
|
66
|
+
? formatUptime(Date.now() - startTime)
|
|
67
|
+
: "-";
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
id: container.id,
|
|
71
|
+
name: containerInfo.Name.replace(/^\//, ""), // Remove leading slash
|
|
72
|
+
image: containerInfo.Config.Image,
|
|
73
|
+
status: containerInfo.State.Running ? "running" : "stopped",
|
|
74
|
+
uptime,
|
|
75
|
+
ports,
|
|
76
|
+
cpu: isNaN(cpuPercent) ? 0 : Math.round(cpuPercent * 100) / 100,
|
|
77
|
+
memory: isNaN(memoryPercent) ? 0 : Math.round(memoryPercent * 100) / 100,
|
|
78
|
+
created: new Date(containerInfo.Created).toLocaleDateString(),
|
|
79
|
+
};
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("Error formatting container:", error.message);
|
|
82
|
+
return {
|
|
83
|
+
id: container.id,
|
|
84
|
+
name: "Unknown",
|
|
85
|
+
image: "Unknown",
|
|
86
|
+
status: "error",
|
|
87
|
+
uptime: "-",
|
|
88
|
+
ports: [],
|
|
89
|
+
cpu: 0,
|
|
90
|
+
memory: 0,
|
|
91
|
+
created: "Unknown",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
async function handleContainerAction(ws, action, payload) {
|
|
97
|
+
switch (action) {
|
|
98
|
+
case "fetchContainers":
|
|
99
|
+
return await handleFetchContainers(ws, payload);
|
|
100
|
+
case "startContainer":
|
|
101
|
+
return await handleStartContainer(ws, payload);
|
|
102
|
+
case "stopContainer":
|
|
103
|
+
return await handleStopContainer(ws, payload);
|
|
104
|
+
case "restartContainer":
|
|
105
|
+
return await handleRestartContainer(ws, payload);
|
|
106
|
+
case "deleteContainer":
|
|
107
|
+
return await handleDeleteContainer(ws, payload);
|
|
108
|
+
case "createContainer":
|
|
109
|
+
return await handleCreateContainer(ws, payload);
|
|
110
|
+
case "inspectContainer":
|
|
111
|
+
return await handleInspectContainer(ws, payload);
|
|
112
|
+
default:
|
|
113
|
+
throw new Error(`Unknown container action: ${action}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function handleFetchContainers(ws, payload = {}) {
|
|
118
|
+
try {
|
|
119
|
+
const containers = await docker.listContainers({ all: true });
|
|
120
|
+
const containerPromises = containers.map((container) =>
|
|
121
|
+
formatContainer(docker.getContainer(container.Id))
|
|
122
|
+
);
|
|
123
|
+
const formattedContainers = await Promise.all(containerPromises);
|
|
124
|
+
|
|
125
|
+
ws.send(
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
type: "containers",
|
|
128
|
+
containers: formattedContainers,
|
|
129
|
+
requestId: payload.requestId,
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error("Error fetching containers:", error);
|
|
134
|
+
ws.send(
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
type: "error",
|
|
137
|
+
error: "Failed to fetch containers: " + error.message,
|
|
138
|
+
requestId: payload.requestId,
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function handleStartContainer(ws, payload) {
|
|
145
|
+
try {
|
|
146
|
+
const container = docker.getContainer(payload.id);
|
|
147
|
+
await container.start();
|
|
148
|
+
const formattedContainer = await formatContainer(container);
|
|
149
|
+
|
|
150
|
+
ws.send(
|
|
151
|
+
JSON.stringify({
|
|
152
|
+
type: "containerStarted",
|
|
153
|
+
container: formattedContainer,
|
|
154
|
+
requestId: payload.requestId,
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error("Error starting container:", error);
|
|
159
|
+
ws.send(
|
|
160
|
+
JSON.stringify({
|
|
161
|
+
type: "error",
|
|
162
|
+
error: "Failed to start container: " + error.message,
|
|
163
|
+
requestId: payload.requestId,
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function handleStopContainer(ws, payload) {
|
|
170
|
+
try {
|
|
171
|
+
const container = docker.getContainer(payload.id);
|
|
172
|
+
await container.stop();
|
|
173
|
+
const formattedContainer = await formatContainer(container);
|
|
174
|
+
|
|
175
|
+
ws.send(
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
type: "containerStopped",
|
|
178
|
+
container: formattedContainer,
|
|
179
|
+
requestId: payload.requestId,
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error("Error stopping container:", error);
|
|
184
|
+
ws.send(
|
|
185
|
+
JSON.stringify({
|
|
186
|
+
type: "error",
|
|
187
|
+
error: "Failed to stop container: " + error.message,
|
|
188
|
+
requestId: payload.requestId,
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function handleRestartContainer(ws, payload) {
|
|
195
|
+
try {
|
|
196
|
+
const container = docker.getContainer(payload.id);
|
|
197
|
+
await container.restart();
|
|
198
|
+
const formattedContainer = await formatContainer(container);
|
|
199
|
+
|
|
200
|
+
ws.send(
|
|
201
|
+
JSON.stringify({
|
|
202
|
+
type: "containerRestarted",
|
|
203
|
+
container: formattedContainer,
|
|
204
|
+
requestId: payload.requestId,
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error("Error restarting container:", error);
|
|
209
|
+
ws.send(
|
|
210
|
+
JSON.stringify({
|
|
211
|
+
type: "error",
|
|
212
|
+
error: "Failed to restart container: " + error.message,
|
|
213
|
+
requestId: payload.requestId,
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function handleDeleteContainer(ws, payload) {
|
|
220
|
+
try {
|
|
221
|
+
const container = docker.getContainer(payload.id);
|
|
222
|
+
await container.remove({ force: true });
|
|
223
|
+
|
|
224
|
+
ws.send(
|
|
225
|
+
JSON.stringify({
|
|
226
|
+
type: "containerDeleted",
|
|
227
|
+
id: payload.id,
|
|
228
|
+
success: true,
|
|
229
|
+
requestId: payload.requestId,
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error("Error deleting container:", error);
|
|
234
|
+
ws.send(
|
|
235
|
+
JSON.stringify({
|
|
236
|
+
type: "error",
|
|
237
|
+
error: "Failed to delete container: " + error.message,
|
|
238
|
+
requestId: payload.requestId,
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function handleCreateContainer(ws, payload) {
|
|
245
|
+
try {
|
|
246
|
+
const options = payload.options;
|
|
247
|
+
|
|
248
|
+
// Format port bindings
|
|
249
|
+
const portBindings = {};
|
|
250
|
+
if (options.ports) {
|
|
251
|
+
const portMappings = options.ports.split(",").map((p) => p.trim());
|
|
252
|
+
portMappings.forEach((mapping) => {
|
|
253
|
+
const [hostPort, containerPort] = mapping.split(":");
|
|
254
|
+
portBindings[`${containerPort}/tcp`] = [{ HostPort: hostPort }];
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Format volumes
|
|
259
|
+
const volumes = {};
|
|
260
|
+
const binds = [];
|
|
261
|
+
if (options.volumes) {
|
|
262
|
+
const volumeMappings = options.volumes.split(",").map((v) => v.trim());
|
|
263
|
+
volumeMappings.forEach((mapping) => {
|
|
264
|
+
const [hostPath, containerPath] = mapping.split(":");
|
|
265
|
+
volumes[containerPath] = {};
|
|
266
|
+
binds.push(`${hostPath}:${containerPath}`);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Format environment variables
|
|
271
|
+
const env = [];
|
|
272
|
+
if (options.environment) {
|
|
273
|
+
const envMappings = options.environment.split(",").map((e) => e.trim());
|
|
274
|
+
envMappings.forEach((mapping) => {
|
|
275
|
+
env.push(mapping);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Create container
|
|
280
|
+
const container = await docker.createContainer({
|
|
281
|
+
name: options.name,
|
|
282
|
+
Image: options.image,
|
|
283
|
+
ExposedPorts: Object.keys(portBindings).reduce((acc, port) => {
|
|
284
|
+
acc[port] = {};
|
|
285
|
+
return acc;
|
|
286
|
+
}, {}),
|
|
287
|
+
HostConfig: {
|
|
288
|
+
PortBindings: portBindings,
|
|
289
|
+
Binds: binds,
|
|
290
|
+
RestartPolicy: {
|
|
291
|
+
Name: options.restart || "no",
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
Env: env,
|
|
295
|
+
Volumes: volumes,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const formattedContainer = await formatContainer(container);
|
|
299
|
+
|
|
300
|
+
ws.send(
|
|
301
|
+
JSON.stringify({
|
|
302
|
+
type: "containerCreated",
|
|
303
|
+
container: formattedContainer,
|
|
304
|
+
requestId: payload.requestId,
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error("Error creating container:", error);
|
|
309
|
+
ws.send(
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
type: "error",
|
|
312
|
+
error: "Failed to create container: " + error.message,
|
|
313
|
+
requestId: payload.requestId,
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function handleInspectContainer(ws, payload) {
|
|
320
|
+
try {
|
|
321
|
+
const { id, requestId } = payload;
|
|
322
|
+
const container = docker.getContainer(id);
|
|
323
|
+
const inspectionData = await container.inspect();
|
|
324
|
+
|
|
325
|
+
ws.send(
|
|
326
|
+
JSON.stringify({
|
|
327
|
+
type: "containerInspected",
|
|
328
|
+
data: inspectionData,
|
|
329
|
+
requestId,
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error("Error inspecting container:", error);
|
|
334
|
+
ws.send(
|
|
335
|
+
JSON.stringify({
|
|
336
|
+
type: "error",
|
|
337
|
+
error: "Failed to inspect container: " + error.message,
|
|
338
|
+
requestId: payload.requestId,
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export default { handleContainerAction, formatContainer, docker };
|
|
345
|
+
|
|
346
|
+
export { handleContainerAction, formatContainer, docker };
|