underpost 3.2.8 → 3.2.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.
Files changed (92) hide show
  1. package/.github/workflows/npmpkg.ci.yml +1 -0
  2. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  3. package/.github/workflows/release.cd.yml +1 -0
  4. package/.vscode/settings.json +10 -5
  5. package/CHANGELOG.md +223 -2
  6. package/CLI-HELP.md +36 -7
  7. package/README.md +38 -9
  8. package/bin/build.js +27 -11
  9. package/bin/deploy.js +20 -21
  10. package/bin/file.js +32 -13
  11. package/bin/index.js +2 -1
  12. package/bin/vs.js +1 -1
  13. package/bump.config.js +26 -0
  14. package/conf.js +20 -4
  15. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  16. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
  19. package/manifests/kind-config-dev.yaml +8 -0
  20. package/manifests/mongodb/pv-pvc.yaml +44 -8
  21. package/manifests/mongodb/statefulset.yaml +55 -68
  22. package/package.json +40 -25
  23. package/scripts/k3s-node-setup.sh +30 -11
  24. package/scripts/nat-iptables.sh +103 -18
  25. package/src/api/core/core.router.js +19 -14
  26. package/src/api/core/core.service.js +5 -5
  27. package/src/api/default/default.router.js +22 -18
  28. package/src/api/default/default.service.js +5 -5
  29. package/src/api/document/document.router.js +28 -23
  30. package/src/api/document/document.service.js +100 -23
  31. package/src/api/file/file.router.js +19 -13
  32. package/src/api/file/file.service.js +9 -7
  33. package/src/api/test/test.router.js +17 -12
  34. package/src/api/types.js +24 -0
  35. package/src/api/user/guest.service.js +5 -4
  36. package/src/api/user/user.router.js +297 -288
  37. package/src/api/user/user.service.js +100 -35
  38. package/src/cli/baremetal.js +20 -11
  39. package/src/cli/cluster.js +243 -55
  40. package/src/cli/db.js +106 -62
  41. package/src/cli/deploy.js +297 -154
  42. package/src/cli/fs.js +19 -3
  43. package/src/cli/index.js +37 -9
  44. package/src/cli/ipfs.js +4 -6
  45. package/src/cli/kubectl.js +4 -1
  46. package/src/cli/lxd.js +217 -135
  47. package/src/cli/release.js +289 -131
  48. package/src/cli/repository.js +91 -34
  49. package/src/cli/run.js +297 -56
  50. package/src/cli/test.js +9 -3
  51. package/src/client/Default.index.js +9 -3
  52. package/src/client/components/core/Auth.js +19 -5
  53. package/src/client/components/core/Docs.js +6 -34
  54. package/src/client/components/core/FileExplorer.js +6 -6
  55. package/src/client/components/core/Modal.js +65 -2
  56. package/src/client/components/core/PanelForm.js +56 -52
  57. package/src/client/components/core/Recover.js +4 -4
  58. package/src/client/components/core/Worker.js +170 -350
  59. package/src/client/services/default/default.management.js +20 -25
  60. package/src/client/services/user/guest.service.js +10 -3
  61. package/src/client/sw/core.sw.js +174 -112
  62. package/src/db/DataBaseProvider.js +120 -20
  63. package/src/db/mongo/MongoBootstrap.js +587 -0
  64. package/src/db/mongo/MongooseDB.js +126 -22
  65. package/src/index.js +1 -1
  66. package/src/runtime/express/Express.js +2 -2
  67. package/src/runtime/wp/Wp.js +8 -5
  68. package/src/server/auth.js +2 -2
  69. package/src/server/client-build-docs.js +1 -1
  70. package/src/server/client-build.js +94 -129
  71. package/src/server/conf.js +20 -65
  72. package/src/server/data-query.js +32 -20
  73. package/src/server/dns.js +22 -0
  74. package/src/server/process.js +180 -19
  75. package/src/server/runtime.js +1 -1
  76. package/src/server/start.js +26 -7
  77. package/src/server/valkey.js +9 -2
  78. package/src/ws/IoInterface.js +16 -16
  79. package/src/ws/core/channels/core.ws.chat.js +11 -11
  80. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  81. package/src/ws/core/channels/core.ws.stream.js +19 -19
  82. package/src/ws/core/core.ws.connection.js +8 -8
  83. package/src/ws/core/core.ws.server.js +6 -5
  84. package/src/ws/default/channels/default.ws.main.js +10 -10
  85. package/src/ws/default/default.ws.connection.js +4 -4
  86. package/src/ws/default/default.ws.server.js +4 -3
  87. package/typedoc.json +10 -1
  88. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  89. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  90. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  91. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  92. /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/cli/lxd.js CHANGED
@@ -2,6 +2,28 @@
2
2
  * LXD module for managing LXD virtual machines as K3s nodes.
3
3
  * @module src/cli/lxd.js
4
4
  * @namespace UnderpostLxd
5
+ *
6
+ * ### Proxy Device Safety
7
+ *
8
+ * Proxy devices (created by `--expose`) attach LXD proxy devices to VMs. If you
9
+ * stop + delete a VM without removing proxy devices first, LXD may crash or
10
+ * leave stale NAT rules in iptables. Both `_safeDeleteVm()` and reset() now
11
+ * enumerate and remove proxy devices before stopping/deleting VMs.
12
+ *
13
+ * ### Idempotency
14
+ *
15
+ * Every destructive operation (deleteVm, reset) is safe to re-run. If a VM is
16
+ * already gone, proxy device removal is silently skipped. If the LXD snap is
17
+ * already removed, reset continues gracefully.
18
+ *
19
+ * ### Lifecycle
20
+ *
21
+ * - `--reset` is the only complete teardown path: cleans ALL VMs, profiles,
22
+ * networks, and finally the LXD snap itself.
23
+ * - `--delete-vm` is a single-VM teardown that removes proxy devices first.
24
+ * - `--init-vm` handles OS + K3s setup. Engine replication is a separate step
25
+ * via `--bootstrap-engine`.
26
+ * - `--bootstrap-engine` replicates /home/dd/engine into the VM after init.
5
27
  */
6
28
 
7
29
  import { getNpmRootPath } from '../server/conf.js';
@@ -13,40 +35,33 @@ import Underpost from '../index.js';
13
35
 
14
36
  const logger = loggerFactory(import.meta);
15
37
 
16
- /**
17
- * @class UnderpostLxd
18
- * @description Provides a set of static methods to interact with LXD,
19
- * encapsulating common LXD operations for VM management and network testing.
20
- * @memberof UnderpostLxd
21
- */
22
38
  class UnderpostLxd {
23
39
  static API = {
24
40
  /**
25
41
  * @method callback
26
- * @description Main entry point for LXD VM operations. Each VM is a K3s node (control or worker).
42
+ * @description Main entry point for all LXD CLI operations.
27
43
  * @param {object} options
28
44
  * @param {boolean} [options.init=false] - Initialize LXD via preseed.
29
- * @param {boolean} [options.reset=false] - Remove LXD snap and purge data.
45
+ * @param {boolean} [options.reset=false] - Complete safe reset: cleans all VMs
46
+ * (proxy devices removed first), profiles, networks, then removes LXD snap.
30
47
  * @param {boolean} [options.dev=false] - Use local paths instead of npm global.
31
48
  * @param {boolean} [options.install=false] - Install LXD snap.
32
49
  * @param {boolean} [options.createVirtualNetwork=false] - Create lxdbr0 bridge network.
33
- * @param {string} [options.ipv4Address='10.250.250.1/24'] - IPv4 address/CIDR for the lxdbr0 bridge network.
50
+ * @param {string} [options.ipv4Address='10.250.250.1/24'] - IPv4 address/CIDR for lxdbr0.
34
51
  * @param {boolean} [options.createAdminProfile=false] - Create admin-profile for VMs.
35
- * @param {boolean} [options.control=false] - Initialize VM as K3s control plane node.
36
- * @param {boolean} [options.worker=false] - Initialize VM as K3s worker node.
37
- * @param {string} [options.initVm=''] - VM name to initialize as a K3s node.
38
- * @param {string} [options.deleteVm=''] - VM name to stop and delete.
39
- * @param {string} [options.createVm=''] - VM name to create (copies launch command to clipboard).
52
+ * @param {boolean} [options.control=false] - Initialize VM as K3s control plane.
53
+ * @param {boolean} [options.worker=false] - Initialize VM as K3s worker.
54
+ * @param {string} [options.initVm=''] - VM name to initialize as K3s node.
55
+ * @param {string} [options.deleteVm=''] - VM name to safely stop and delete
56
+ * (removes proxy devices first).
57
+ * @param {string} [options.createVm=''] - VM name to create (copies command to clipboard).
40
58
  * @param {string} [options.infoVm=''] - VM name to inspect.
41
- * @param {string} [options.rootSize=''] - Root disk size in GiB for new VMs (e.g. '32').
42
- * @param {string} [options.joinNode=''] - Join format: 'workerName,controlName' (standalone join). When used with --init-vm --worker, just the control node name.
59
+ * @param {string} [options.rootSize=''] - Root disk size in GiB for new VMs.
60
+ * @param {string} [options.joinNode=''] - Join format: 'workerName,controlName'.
43
61
  * @param {string} [options.expose=''] - Expose VM ports to host: 'vmName:port1,port2'.
44
62
  * @param {string} [options.deleteExpose=''] - Remove exposed ports: 'vmName:port1,port2'.
45
- * @param {string} [options.test=''] - VM name to run connectivity and health checks on.
46
- * @param {string} [options.workflowId=''] - Workflow ID for runWorkflow.
47
- * @param {string} [options.vmId=''] - VM name for workflow execution.
48
- * @param {string} [options.deployId=''] - Deployment ID for workflow context.
49
- * @param {string} [options.namespace=''] - Kubernetes namespace context.
63
+ * @param {string} [options.test=''] - VM name for connectivity and health checks.
64
+ * @param {string} [options.bootstrapEngine=''] - VM name to replicate /home/dd/engine into.
50
65
  * @memberof UnderpostLxd
51
66
  */
52
67
  async callback(
@@ -69,22 +84,58 @@ class UnderpostLxd {
69
84
  expose: '',
70
85
  deleteExpose: '',
71
86
  test: '',
72
- workflowId: '',
73
- vmId: '',
74
- deployId: '',
75
- namespace: '',
87
+ bootstrapEngine: '',
76
88
  },
77
89
  ) {
78
90
  const npmRoot = getNpmRootPath();
79
91
  const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
80
92
 
93
+ // =====================================================================
94
+ // RESET: Complete, safe teardown of all LXD state
95
+ // =====================================================================
81
96
  if (options.reset === true) {
82
- shellExec(`sudo systemctl stop snap.lxd.daemon`);
83
- shellExec(`sudo snap remove lxd --purge`);
97
+ logger.info('=== SAFE LXD RESET ===');
98
+ logger.info('Phase 1/5: Enumerating all VMs and removing proxy devices...');
99
+ const vmList = UnderpostLxd._listVms();
100
+ for (const vmName of vmList) {
101
+ UnderpostLxd._removeProxyDevices(vmName);
102
+ }
103
+
104
+ logger.info('Phase 2/5: Stopping all VMs gracefully...');
105
+ for (const vmName of vmList) {
106
+ logger.info(` Stopping VM: ${vmName}`);
107
+ shellExec(`lxc stop ${vmName} --timeout 30 2>/dev/null || true`, { silent: true, silentOnError: true });
108
+ }
109
+
110
+ logger.info('Phase 3/5: Deleting all VMs...');
111
+ for (const vmName of vmList) {
112
+ logger.info(` Deleting VM: ${vmName}`);
113
+ shellExec(`lxc delete ${vmName} --force 2>/dev/null || true`, { silent: true, silentOnError: true });
114
+ }
115
+
116
+ logger.info('Phase 4/5: Removing admin-profile and network...');
117
+ shellExec(`lxc profile delete admin-profile 2>/dev/null || true`, { silent: true, silentOnError: true });
118
+ shellExec(`lxc network delete lxdbr0 2>/dev/null || true`, { silent: true, silentOnError: true });
119
+
120
+ logger.info('Phase 5/5: Stopping LXD snap daemon and purging snap...');
121
+ shellExec(`sudo systemctl stop snap.lxd.daemon 2>/dev/null || true`, { silent: true, silentOnError: true });
122
+ shellExec(`sudo snap remove lxd --purge 2>/dev/null || true`, { silent: true, silentOnError: true });
123
+
124
+ logger.info('=== LXD RESET COMPLETE ===');
125
+ logger.info('All VMs, proxy devices, profiles, networks, and the LXD snap have been removed.');
126
+ return;
84
127
  }
85
128
 
86
- if (options.install === true) shellExec(`sudo snap install lxd`);
129
+ // =====================================================================
130
+ // INSTALL
131
+ // =====================================================================
132
+ if (options.install === true) {
133
+ shellExec(`sudo snap install lxd`);
134
+ }
87
135
 
136
+ // =====================================================================
137
+ // INIT (LXD preseed)
138
+ // =====================================================================
88
139
  if (options.init === true) {
89
140
  shellExec(`sudo systemctl start snap.lxd.daemon`);
90
141
  shellExec(`sudo systemctl status snap.lxd.daemon`);
@@ -95,6 +146,9 @@ class UnderpostLxd {
95
146
  shellExec(`lxc cluster list`);
96
147
  }
97
148
 
149
+ // =====================================================================
150
+ // CREATE VIRTUAL NETWORK
151
+ // =====================================================================
98
152
  if (options.createVirtualNetwork === true) {
99
153
  const ipv4Address = options.ipv4Address ? options.ipv4Address : '10.250.250.1/24';
100
154
  shellExec(`lxc network create lxdbr0 \
@@ -104,6 +158,9 @@ ipv4.dhcp=true \
104
158
  ipv6.address=none`);
105
159
  }
106
160
 
161
+ // =====================================================================
162
+ // CREATE ADMIN PROFILE
163
+ // =====================================================================
107
164
  if (options.createAdminProfile === true) {
108
165
  const existingProfiles = await new Promise((resolve) => {
109
166
  shellExec(`lxc profile show admin-profile`, {
@@ -120,25 +177,28 @@ ipv6.address=none`);
120
177
  }
121
178
  }
122
179
 
180
+ // =====================================================================
181
+ // DELETE VM (safe: removes proxy devices first)
182
+ // =====================================================================
123
183
  if (options.deleteVm) {
124
184
  const vmName = options.deleteVm;
125
- logger.info(`Stopping VM: ${vmName}`);
126
- shellExec(`lxc stop ${vmName}`);
127
- logger.info(`Deleting VM: ${vmName}`);
128
- shellExec(`lxc delete ${vmName}`);
129
- logger.info(`VM ${vmName} deleted.`);
185
+ UnderpostLxd._safeDeleteVm(vmName);
130
186
  }
131
187
 
188
+ // =====================================================================
189
+ // CREATE VM (copy launch command to clipboard)
190
+ // =====================================================================
132
191
  if (options.createVm) {
133
192
  pbcopy(
134
- `lxc launch images:rockylinux/9 ${
135
- options.createVm
136
- } --vm --target lxd-node1 -c limits.cpu=2 -c limits.memory=4GB --profile admin-profile -d root,size=${
137
- options.rootSize ? options.rootSize + 'GiB' : '32GiB'
193
+ `lxc launch images:rockylinux/9 ${options.createVm
194
+ } --vm --target lxd-node1 -c limits.cpu=2 -c limits.memory=4GB --profile admin-profile -d root,size=${options.rootSize ? options.rootSize + 'GiB' : '32GiB'
138
195
  }`,
139
196
  );
140
197
  }
141
198
 
199
+ // =====================================================================
200
+ // INIT VM (OS setup + K3s role, no engine push)
201
+ // =====================================================================
142
202
  if (options.initVm) {
143
203
  const vmName = options.initVm;
144
204
  const lxdSetupPath = `${underpostRoot}/scripts/lxd-vm-setup.sh`;
@@ -147,10 +207,8 @@ ipv6.address=none`);
147
207
  // Step 1: OS base setup (disk, packages, kernel modules)
148
208
  shellExec(`cat ${lxdSetupPath} | lxc exec ${vmName} -- bash`);
149
209
 
150
- // Step 2: Push engine source from host to VM
151
- await Underpost.lxd.runWorkflow({ workflowId: 'engine', vmName, dev: options.dev });
152
-
153
- // Step 3: K3s role setup (installs Node, npm deps, then k3s via node bin --dev)
210
+ // Step 2: K3s role setup (installs Node, npm deps, then k3s via underpost CLI)
211
+ // Engine source replication is a separate step via --bootstrap-engine.
154
212
  if (options.worker === true) {
155
213
  if (options.joinNode) {
156
214
  const controlNode = options.joinNode.includes(',') ? options.joinNode.split(',').pop() : options.joinNode;
@@ -174,16 +232,52 @@ ipv6.address=none`);
174
232
  }
175
233
  }
176
234
 
177
- if (options.workflowId) {
178
- await Underpost.lxd.runWorkflow({
179
- workflowId: options.workflowId,
180
- vmName: options.vmId,
181
- deployId: options.deployId,
182
- dev: options.dev,
183
- });
235
+ // =====================================================================
236
+ // BOOTSTRAP ENGINE: Replicate /home/dd/engine into a VM
237
+ // =====================================================================
238
+ if (options.bootstrapEngine) {
239
+ const vmName = options.bootstrapEngine;
240
+ logger.info(`Bootstrapping engine source into VM: ${vmName}...`);
241
+
242
+ const includesFile = `/tmp/lxd-push-${vmName}-${Date.now()}.txt`;
243
+ const srcPath = `/home/dd/engine`;
244
+ const files = await new Promise((resolve) =>
245
+ walk({ path: srcPath, ignoreFiles: ['.gitignore'], includeEmpty: false, follow: false }, (_, result) =>
246
+ resolve(result),
247
+ ),
248
+ );
249
+ fs.writeFileSync(includesFile, files.join('\n'));
250
+ shellExec(`lxc exec ${vmName} -- bash -c 'rm -rf /home/dd/engine && mkdir -p /home/dd/engine'`);
251
+ shellExec(`tar -C ${srcPath} -cf - --files-from=${includesFile} | lxc exec ${vmName} -- tar -C /home/dd/engine -xf -`);
252
+ fs.removeSync(includesFile);
253
+
254
+ // Also push engine-private if it exists
255
+ const privateSrcPath = `/home/dd/engine/engine-private`;
256
+ if (fs.existsSync(privateSrcPath)) {
257
+ const privateFiles = await new Promise((resolve) =>
258
+ walk(
259
+ {
260
+ path: privateSrcPath,
261
+ ignoreFiles: ['/home/dd/engine/.gitignore', '.gitignore'],
262
+ includeEmpty: false,
263
+ follow: false,
264
+ },
265
+ (_, result) => resolve(result),
266
+ ),
267
+ );
268
+ const privateIncludes = `/tmp/lxd-push-${vmName}-private-${Date.now()}.txt`;
269
+ fs.writeFileSync(privateIncludes, privateFiles.join('\n'));
270
+ shellExec(`lxc exec ${vmName} -- bash -c 'rm -rf /home/dd/engine/engine-private && mkdir -p /home/dd/engine/engine-private'`);
271
+ shellExec(`tar -C ${privateSrcPath} -cf - --files-from=${privateIncludes} | lxc exec ${vmName} -- tar -C /home/dd/engine/engine-private -xf -`);
272
+ fs.removeSync(privateIncludes);
273
+ }
274
+
275
+ logger.info(`Engine source bootstrapped into ${vmName}:/home/dd/engine`);
184
276
  }
185
277
 
186
- // Standalone join: --join-node workerName,controlName (without --init-vm)
278
+ // =====================================================================
279
+ // STANDALONE JOIN
280
+ // =====================================================================
187
281
  if (options.joinNode && !options.initVm) {
188
282
  const [workerNode, controlNode] = options.joinNode.split(',');
189
283
  const k3sToken = shellExec(
@@ -201,6 +295,9 @@ ipv6.address=none`);
201
295
  logger.info(`Worker ${workerNode} joined successfully.`);
202
296
  }
203
297
 
298
+ // =====================================================================
299
+ // INFO VM
300
+ // =====================================================================
204
301
  if (options.infoVm) {
205
302
  shellExec(`lxc config show ${options.infoVm}`);
206
303
  shellExec(`lxc info --show-log ${options.infoVm}`);
@@ -208,6 +305,9 @@ ipv6.address=none`);
208
305
  shellExec(`lxc list ${options.infoVm}`);
209
306
  }
210
307
 
308
+ // =====================================================================
309
+ // EXPOSE (proxy host ports to VM)
310
+ // =====================================================================
211
311
  if (options.expose) {
212
312
  const [vmName, ports] = options.expose.split(':');
213
313
  const protocols = ['tcp'];
@@ -232,6 +332,9 @@ ipv6.address=none`);
232
332
  }
233
333
  }
234
334
 
335
+ // =====================================================================
336
+ // DELETE EXPOSE
337
+ // =====================================================================
235
338
  if (options.deleteExpose) {
236
339
  const [vmName, ports] = options.deleteExpose.split(':');
237
340
  const protocols = ['tcp'];
@@ -242,6 +345,9 @@ ipv6.address=none`);
242
345
  }
243
346
  }
244
347
 
348
+ // =====================================================================
349
+ // TEST (connectivity and health checks)
350
+ // =====================================================================
245
351
  if (options.test) {
246
352
  const vmName = options.test;
247
353
  const vmIp = shellExec(
@@ -264,93 +370,69 @@ ipv6.address=none`);
264
370
  shellExec(`lxc exec ${vmName} -- bash -c 'sudo k3s kubectl get nodes'`);
265
371
  }
266
372
  },
373
+ };
267
374
 
268
- /**
269
- * @method pushDirectory
270
- * @description Pushes a host directory into a VM using ignore-walk (respecting .gitignore)
271
- * and a tar pipe. Skips gitignored paths (e.g. node_modules, .git, build artifacts).
272
- * @param {object} params
273
- * @param {string} params.srcPath - Absolute path of the source directory on the host.
274
- * @param {string} params.vmName - Target LXD VM name.
275
- * @param {string} params.destPath - Absolute path of the destination directory inside the VM.
276
- * @param {string[]} [params.ignoreFiles=['.gitignore']] - Ignore-file names to respect during walk.
277
- * @returns {Promise<void>}
278
- * @memberof UnderpostLxd
279
- */
280
- async pushDirectory({ srcPath, vmName, destPath, ignoreFiles }) {
281
- const includesFile = `/tmp/lxd-push-${vmName}-${Date.now()}.txt`;
282
- if (!ignoreFiles) ignoreFiles = ['.gitignore'];
283
- // Collect non-ignored files via ignore-walk
284
- const files = await new Promise((resolve) =>
285
- walk(
286
- {
287
- path: srcPath,
288
- ignoreFiles,
289
- includeEmpty: false,
290
- follow: false,
291
- },
292
- (_, result) => resolve(result),
293
- ),
294
- );
295
-
296
- // Write relative paths (one per line) to a temp includes file
297
- fs.writeFileSync(includesFile, files.join('\n'));
298
- logger.info(`lxd pushDirectory: ${files.length} files collected`, { srcPath, vmName, destPath, includesFile });
299
-
300
- // Reset destination directory inside the VM
301
- shellExec(`lxc exec ${vmName} -- bash -c 'rm -rf ${destPath} && mkdir -p ${destPath}'`);
302
-
303
- // Stream tar archive from host into VM
304
- shellExec(
305
- `tar -C ${srcPath} -cf - --files-from=${includesFile} | lxc exec ${vmName} -- tar -C ${destPath} -xf -`,
306
- );
307
-
308
- // Clean up temp includes file
309
- fs.removeSync(includesFile);
310
- },
375
+ // =====================================================================
376
+ // PRIVATE HELPERS
377
+ // =====================================================================
311
378
 
312
- /**
313
- * @method runWorkflow
314
- * @description Executes predefined workflows on LXD VMs.
315
- * @param {object} params
316
- * @param {string} params.workflowId - Workflow ID to execute.
317
- * @param {string} params.vmName - Target VM name.
318
- * @param {string} [params.deployId] - Deployment identifier.
319
- * @param {boolean} [params.dev=false] - Use local paths.
320
- * @memberof UnderpostLxd
321
- */
322
- async runWorkflow({ workflowId, vmName, deployId, dev }) {
323
- switch (workflowId) {
324
- case 'engine': {
325
- await Underpost.lxd.pushDirectory({
326
- srcPath: `/home/dd/engine`,
327
- vmName,
328
- destPath: `/home/dd/engine`,
329
- });
330
- await Underpost.lxd.pushDirectory({
331
- srcPath: `/home/dd/engine/engine-private`,
332
- vmName,
333
- destPath: `/home/dd/engine/engine-private`,
334
- ignoreFiles: ['/home/dd/engine/.gitignore', '.gitignore'],
335
- });
336
- break;
337
- }
338
- case 'engine-recursive-push': {
339
- const enginePath = '/home/dd/engine';
340
- shellExec(`lxc exec ${vmName} -- bash -c 'rm -rf ${enginePath}'`);
341
- shellExec(`lxc exec ${vmName} -- bash -c 'mkdir -p /home/dd'`);
342
- shellExec(`lxc file push ${enginePath} ${vmName}/home/dd --recursive`);
343
- break;
344
- }
345
- case 'dev-reset': {
346
- shellExec(
347
- `lxc exec ${vmName} -- bash -lc 'cd /home/dd/engine && node bin cluster --dev --reset --k3s && node bin cluster --dev --k3s'`,
348
- );
349
- break;
350
- }
351
- }
352
- },
353
- };
379
+ /**
380
+ * Lists all LXD VM (virtual-machine) instance names.
381
+ * @returns {string[]} Array of VM names.
382
+ * @private
383
+ */
384
+ static _listVms() {
385
+ const raw = shellExec(
386
+ `lxc list --format json | jq -r '.[] | select(.type=="virtual-machine") | .name // empty' 2>/dev/null || true`,
387
+ { stdout: true, silent: true, silentOnError: true },
388
+ ).trim();
389
+ if (!raw) return [];
390
+ return raw.split('\n').filter((n) => n.length > 0);
391
+ }
392
+
393
+ /**
394
+ * Enumerates and removes all proxy devices attached to a VM.
395
+ * Proxy devices are named with the pattern <vmName>-<protocol>-port-<port>.
396
+ * Fails silently if the VM or device is already gone (idempotent).
397
+ * @param {string} vmName - The VM name to clean proxy devices from.
398
+ * @private
399
+ */
400
+ static _removeProxyDevices(vmName) {
401
+ logger.info(` Removing proxy devices from ${vmName}...`);
402
+ const devicesRaw = shellExec(
403
+ `lxc config device list ${vmName} 2>/dev/null | grep -E "^${vmName}-tcp-port-" || true`,
404
+ { stdout: true, silent: true, silentOnError: true },
405
+ ).trim();
406
+ if (!devicesRaw) {
407
+ logger.info(` No proxy devices found on ${vmName}.`);
408
+ return;
409
+ }
410
+ for (const deviceName of devicesRaw.split('\n')) {
411
+ const name = deviceName.trim();
412
+ if (!name) continue;
413
+ logger.info(` Removing device: ${name}`);
414
+ shellExec(`lxc config device remove ${vmName} ${name} 2>/dev/null || true`, {
415
+ silent: true,
416
+ silentOnError: true,
417
+ });
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Safely deletes a single VM: removes proxy devices first, then stops and deletes.
423
+ * Idempotent: safe to re-run if VM is already gone.
424
+ * @param {string} vmName - The VM name to delete.
425
+ * @private
426
+ */
427
+ static _safeDeleteVm(vmName) {
428
+ logger.info(`Safely deleting VM: ${vmName}`);
429
+ UnderpostLxd._removeProxyDevices(vmName);
430
+ logger.info(` Stopping VM: ${vmName}`);
431
+ shellExec(`lxc stop ${vmName} --timeout 30 2>/dev/null || true`, { silent: true, silentOnError: true });
432
+ logger.info(` Deleting VM: ${vmName}`);
433
+ shellExec(`lxc delete ${vmName} --force 2>/dev/null || true`, { silent: true, silentOnError: true });
434
+ logger.info(`VM ${vmName} safely deleted.`);
435
+ }
354
436
  }
355
437
 
356
- export default UnderpostLxd;
438
+ export default UnderpostLxd;