@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.
@@ -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 };