underpost 3.2.9 → 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 (81) 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 +122 -1
  6. package/CLI-HELP.md +22 -7
  7. package/README.md +37 -8
  8. package/bin/build.js +26 -9
  9. package/bin/deploy.js +20 -21
  10. package/bin/file.js +31 -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 +1 -1
  16. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  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 +27 -12
  23. package/scripts/k3s-node-setup.sh +28 -9
  24. package/src/api/core/core.router.js +19 -14
  25. package/src/api/core/core.service.js +5 -5
  26. package/src/api/default/default.router.js +22 -18
  27. package/src/api/default/default.service.js +5 -5
  28. package/src/api/document/document.router.js +28 -23
  29. package/src/api/document/document.service.js +100 -23
  30. package/src/api/file/file.router.js +19 -13
  31. package/src/api/file/file.service.js +9 -7
  32. package/src/api/test/test.router.js +17 -12
  33. package/src/api/types.js +24 -0
  34. package/src/api/user/guest.service.js +5 -4
  35. package/src/api/user/user.router.js +297 -288
  36. package/src/api/user/user.service.js +100 -35
  37. package/src/cli/baremetal.js +20 -11
  38. package/src/cli/cluster.js +196 -55
  39. package/src/cli/db.js +59 -60
  40. package/src/cli/deploy.js +273 -159
  41. package/src/cli/fs.js +3 -1
  42. package/src/cli/index.js +16 -9
  43. package/src/cli/ipfs.js +4 -6
  44. package/src/cli/kubectl.js +4 -1
  45. package/src/cli/lxd.js +217 -135
  46. package/src/cli/release.js +289 -131
  47. package/src/cli/repository.js +58 -7
  48. package/src/cli/run.js +152 -25
  49. package/src/cli/test.js +9 -3
  50. package/src/client/Default.index.js +9 -3
  51. package/src/client/components/core/Auth.js +4 -0
  52. package/src/client/components/core/PanelForm.js +56 -52
  53. package/src/client/components/core/Worker.js +162 -363
  54. package/src/client/sw/core.sw.js +174 -112
  55. package/src/db/DataBaseProvider.js +120 -20
  56. package/src/db/mongo/MongoBootstrap.js +587 -0
  57. package/src/db/mongo/MongooseDB.js +126 -22
  58. package/src/index.js +1 -1
  59. package/src/runtime/express/Express.js +2 -2
  60. package/src/runtime/wp/Wp.js +8 -5
  61. package/src/server/auth.js +2 -2
  62. package/src/server/client-build-docs.js +1 -1
  63. package/src/server/client-build.js +94 -129
  64. package/src/server/conf.js +20 -65
  65. package/src/server/process.js +180 -19
  66. package/src/server/runtime.js +1 -1
  67. package/src/server/start.js +12 -4
  68. package/src/ws/IoInterface.js +16 -16
  69. package/src/ws/core/channels/core.ws.chat.js +11 -11
  70. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  71. package/src/ws/core/channels/core.ws.stream.js +19 -19
  72. package/src/ws/core/core.ws.connection.js +8 -8
  73. package/src/ws/core/core.ws.server.js +6 -5
  74. package/src/ws/default/channels/default.ws.main.js +10 -10
  75. package/src/ws/default/default.ws.connection.js +4 -4
  76. package/src/ws/default/default.ws.server.js +4 -3
  77. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  78. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  79. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  80. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  81. /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/cli/index.js CHANGED
@@ -242,6 +242,7 @@ program
242
242
  .command('cluster')
243
243
  .argument('[pod-name]', 'Optional: Filters information by a specific pod name.')
244
244
  .option('--reset', `Deletes all clusters and prunes all related data and caches.`)
245
+ .option('--reset-mongodb', `Performs a hard cleanup of only MongoDB-related resources (StatefulSet, PVCs/PVs, Secrets, ConfigMaps, caches) without restarting the whole node.`)
245
246
  .option('--mariadb', 'Initializes the cluster with a MariaDB statefulset.')
246
247
  .option('--mysql', 'Initializes the cluster with a MySQL statefulset.')
247
248
  .option('--mongodb', 'Initializes the cluster with a MongoDB statefulset.')
@@ -346,6 +347,10 @@ program
346
347
  '--pull-bundle',
347
348
  'Explicitly pull the pre-built client bundle from Cloudinary inside the container. Use together with --skip-full-build.',
348
349
  )
350
+ .option(
351
+ '--image-pull-policy <policy>',
352
+ 'Override container imagePullPolicy in the generated deployment manifest (Always, IfNotPresent, Never). Defaults to Never for localhost/ images and IfNotPresent otherwise.',
353
+ )
349
354
  .description('Manages application deployments, defaulting to deploying development pods.')
350
355
  .action(Underpost.deploy.callback);
351
356
 
@@ -670,8 +675,8 @@ program
670
675
  .option(
671
676
  '--host-aliases <host-aliases>',
672
677
  'Adds entries to the Pod /etc/hosts via hostAliases. ' +
673
- 'Format: semicolon-separated entries of "ip=hostname1,hostname2" ' +
674
- '(e.g., "127.0.0.1=foo.local,bar.local;10.1.2.3=foo.remote,bar.remote").',
678
+ 'Format: semicolon-separated entries of "ip=hostname1,hostname2" ' +
679
+ '(e.g., "127.0.0.1=foo.local,bar.local;10.1.2.3=foo.remote,bar.remote").',
675
680
  )
676
681
  .option('--copy', 'Copies the runner output to the clipboard (supported by: generate-pass, template-deploy-local).')
677
682
  .option(
@@ -688,7 +693,7 @@ program
688
693
  program
689
694
  .command('lxd')
690
695
  .option('--init', 'Initializes LXD on the current machine via preseed.')
691
- .option('--reset', 'Removes the LXD snap and purges all data.')
696
+ .option('--reset', 'SAFE complete reset: cleans all VMs (proxy devices first), profiles, networks, then removes LXD snap.')
692
697
  .option('--install', 'Installs the LXD snap.')
693
698
  .option('--dev', 'Use local paths instead of the global npm installation.')
694
699
  .option('--create-virtual-network', 'Creates the lxdbr0 bridge network.')
@@ -697,7 +702,7 @@ program
697
702
  .option('--control', 'Initialize the target VM as a K3s control plane node.')
698
703
  .option('--worker', 'Initialize the target VM as a K3s worker node.')
699
704
  .option('--create-vm <vm-name>', 'Copy the LXC launch command for a new K3s VM to the clipboard.')
700
- .option('--delete-vm <vm-name>', 'Stop and delete the specified VM.')
705
+ .option('--delete-vm <vm-name>', 'SAFELY stop and delete VM (removes proxy devices first, then stops, then deletes). Safe to re-run.')
701
706
  .option('--init-vm <vm-name>', 'Run k3s-node-setup.sh on the specified VM (use with --control or --worker).')
702
707
  .option('--info-vm <vm-name>', 'Display full configuration and status for the specified VM.')
703
708
  .option('--test <vm-name>', 'Run connectivity and health checks on the specified VM.')
@@ -705,13 +710,11 @@ program
705
710
  .option(
706
711
  '--join-node <nodes>',
707
712
  'Join a K3s worker to a control plane. Standalone format: "workerName,controlName". ' +
708
- 'When used with --init-vm --worker, provide just the control node name for auto-join.',
713
+ 'When used with --init-vm --worker, provide just the control node name for auto-join.',
709
714
  )
710
715
  .option('--expose <vm-name:ports>', 'Proxy host ports to a VM (e.g., "k3s-control:80,443").')
711
716
  .option('--delete-expose <vm-name:ports>', 'Remove proxied ports from a VM (e.g., "k3s-control:80,443").')
712
- .option('--workflow-id <workflow-id>', 'Workflow ID to execute via runWorkflow.')
713
- .option('--vm-id <vm-name>', 'Target VM name for workflow execution.')
714
- .option('--deploy-id <deploy-id>', 'Deployment ID context for workflow execution.')
717
+ .option('--bootstrap-engine <vm-name>', 'Replicate /home/dd/engine source into the VM after init completes.')
715
718
  .option('--namespace <namespace>', 'Kubernetes namespace context (defaults to "default").')
716
719
  .description('Manages LXD virtual machines as K3s nodes (control plane or workers).')
717
720
  .action(Underpost.lxd.callback);
@@ -814,7 +817,7 @@ program
814
817
  .option(
815
818
  '--ci-push <deploy-id>',
816
819
  'Local equivalent of engine-*.ci.yml: builds dd-{deploy-id} and pushes to the engine-{deploy-id} repository. ' +
817
- 'Accepts the suffix (e.g., "cyberia"), "dd-cyberia", or "engine-cyberia".',
820
+ 'Accepts the suffix (e.g., "cyberia"), "dd-cyberia", or "engine-cyberia".',
818
821
  )
819
822
  .option(
820
823
  '--message <message>',
@@ -824,6 +827,10 @@ program
824
827
  '--pwa-build',
825
828
  'Runs the pwa-microservices-template update flow: always re-clones, syncs engine sources, installs, builds, and pushes.',
826
829
  )
830
+ .option(
831
+ '--dry-run',
832
+ 'For --build: previews version-bump changes (per-file substitution counts) without writing files or running downstream commands.',
833
+ )
827
834
  .description('Release orchestrator for building new versions and deploying releases of the Underpost CLI.')
828
835
  .action(async (version, options) => {
829
836
  if (options.build) return Underpost.release.build(version, options);
package/src/cli/ipfs.js CHANGED
@@ -131,12 +131,10 @@ class UnderpostIPFS {
131
131
  // Apply UDP buffer sysctl on every Kind node so QUIC (used by IPFS) can reach the
132
132
  // recommended 7.5 MB buffer size. Kind nodes are containers and do NOT inherit the
133
133
  // host sysctl values, so this must be set via docker exec on each node directly.
134
- if (!options.kubeadm && !options.k3s) {
135
- logger.info('Applying UDP buffer sysctl on Kind nodes');
136
- shellExec(
137
- `for node in $(kind get nodes); do docker exec $node sysctl -w net.core.rmem_max=7500000 net.core.wmem_max=7500000; done`,
138
- );
139
- }
134
+ shellExec(
135
+ `sudo sysctl -w net.core.rmem_max=7500000
136
+ sudo sysctl -w net.core.wmem_max=7500000`,
137
+ );
140
138
 
141
139
  shellExec(`kubectl apply -f ${underpostRoot}/manifests/ipfs/storage-class.yaml`);
142
140
  shellExec(`kubectl apply -k ${underpostRoot}/manifests/ipfs -n ${options.namespace}`);
@@ -47,9 +47,12 @@ class UnderpostKubectl {
47
47
  * @memberof UnderpostKubectl
48
48
  */
49
49
  get(deployId, kindType = 'pods', namespace = '') {
50
+ // Existence-check style: a missing kubectl context, a non-existent
51
+ // namespace, or no pods matching the filter must return an empty
52
+ // list (not throw). silentOnError keeps the legacy contract.
50
53
  const raw = shellExec(
51
54
  `sudo kubectl get ${kindType}${namespace ? ` -n ${namespace}` : ` --all-namespaces`} -o wide`,
52
- { stdout: true, disableLog: true, silent: true },
55
+ { stdout: true, disableLog: true, silent: true, silentOnError: true },
53
56
  );
54
57
 
55
58
  const heads = raw
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;