@underpostnet/underpost 2.99.8 → 3.0.1

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 (48) hide show
  1. package/.env.production +1 -0
  2. package/.github/workflows/gitlab.ci.yml +20 -0
  3. package/.github/workflows/publish.ci.yml +18 -34
  4. package/.vscode/extensions.json +8 -50
  5. package/.vscode/settings.json +0 -77
  6. package/CHANGELOG.md +116 -1
  7. package/{cli.md → CLI-HELP.md} +48 -41
  8. package/README.md +3 -3
  9. package/bin/build.js +1 -15
  10. package/bin/deploy.js +4 -133
  11. package/bin/file.js +10 -8
  12. package/bin/zed.js +63 -2
  13. package/jsdoc.json +1 -2
  14. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  15. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  16. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  17. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  18. package/manifests/deployment/fastapi/initial_data.sh +4 -52
  19. package/manifests/ipfs/configmap.yaml +57 -0
  20. package/manifests/ipfs/headless-service.yaml +35 -0
  21. package/manifests/ipfs/kustomization.yaml +8 -0
  22. package/manifests/ipfs/statefulset.yaml +149 -0
  23. package/manifests/ipfs/storage-class.yaml +9 -0
  24. package/package.json +9 -5
  25. package/scripts/k3s-node-setup.sh +89 -0
  26. package/scripts/lxd-vm-setup.sh +23 -0
  27. package/scripts/rocky-setup.sh +1 -13
  28. package/src/api/user/user.router.js +0 -47
  29. package/src/cli/baremetal.js +7 -9
  30. package/src/cli/cluster.js +72 -121
  31. package/src/cli/deploy.js +8 -5
  32. package/src/cli/index.js +31 -30
  33. package/src/cli/ipfs.js +184 -0
  34. package/src/cli/lxd.js +192 -237
  35. package/src/cli/repository.js +4 -1
  36. package/src/cli/run.js +3 -2
  37. package/src/client/components/core/Docs.js +92 -6
  38. package/src/client/components/core/VanillaJs.js +36 -25
  39. package/src/client/services/user/user.management.js +0 -5
  40. package/src/client/services/user/user.service.js +1 -1
  41. package/src/index.js +12 -1
  42. package/src/runtime/express/Express.js +3 -2
  43. package/src/server/client-build-docs.js +178 -41
  44. package/src/server/conf.js +1 -1
  45. package/src/server/logger.js +22 -10
  46. package/.vscode/zed.keymap.json +0 -39
  47. package/.vscode/zed.settings.json +0 -20
  48. package/manifests/lxd/underpost-setup.sh +0 -163
package/src/cli/lxd.js CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
- * LXD module for managing LXD virtual machines and networks.
2
+ * LXD module for managing LXD virtual machines as K3s nodes.
3
3
  * @module src/cli/lxd.js
4
4
  * @namespace UnderpostLxd
5
5
  */
6
6
 
7
7
  import { getNpmRootPath } from '../server/conf.js';
8
8
  import { pbcopy, shellExec } from '../server/process.js';
9
+ import walk from 'ignore-walk';
9
10
  import fs from 'fs-extra';
10
11
  import { loggerFactory } from '../server/logger.js';
11
12
  import Underpost from '../index.js';
@@ -16,34 +17,36 @@ const logger = loggerFactory(import.meta);
16
17
  * @class UnderpostLxd
17
18
  * @description Provides a set of static methods to interact with LXD,
18
19
  * encapsulating common LXD operations for VM management and network testing.
20
+ * @memberof UnderpostLxd
19
21
  */
20
22
  class UnderpostLxd {
21
23
  static API = {
22
24
  /**
23
25
  * @method callback
24
- * @description Main entry point for LXD operations based on provided options.
25
- * @param {object} options - Configuration options for LXD operations.
26
- * @param {boolean} [options.init=false] - Initialize LXD.
27
- * @param {boolean} [options.reset=false] - Reset LXD installation.
28
- * @param {boolean} [options.dev=false] - Run in development mode (adjusts paths).
26
+ * @description Main entry point for LXD VM operations. Each VM is a K3s node (control or worker).
27
+ * @param {object} options
28
+ * @param {boolean} [options.init=false] - Initialize LXD via preseed.
29
+ * @param {boolean} [options.reset=false] - Remove LXD snap and purge data.
30
+ * @param {boolean} [options.dev=false] - Use local paths instead of npm global.
29
31
  * @param {boolean} [options.install=false] - Install LXD snap.
30
- * @param {boolean} [options.createVirtualNetwork=false] - Create default LXD bridge network (lxdbr0).
32
+ * @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.
31
34
  * @param {boolean} [options.createAdminProfile=false] - Create admin-profile for VMs.
32
- * @param {boolean} [options.control=false] - Flag for control plane VM initialization.
33
- * @param {boolean} [options.worker=false] - Flag for worker node VM initialization.
34
- * @param {boolean} [options.k3s=false] - Flag to indicate K3s cluster type for VM initialization.
35
- * @param {string} [options.initVm=''] - Initialize a specific VM.
36
- * @param {string} [options.createVm=''] - Create a new VM with the given name.
37
- * @param {string} [options.infoVm=''] - Display information about a specific VM.
38
- * @param {string} [options.rootSize=''] - Root disk size for new VMs (e.g., '32GiB').
39
- * @param {string} [options.joinNode=''] - Join a worker node to a control plane (format: 'workerName,controlName').
40
- * @param {string} [options.expose=''] - Expose ports from a VM to the host (format: 'vmName:port1,port2').
41
- * @param {string} [options.deleteExpose=''] - Delete exposed ports from a VM (format: 'vmName:port1,port2').
42
- * @param {string} [options.test=''] - Test health, status and network connectivity for a VM.
43
- * @param {string} [options.workflowId=''] - Workflow identifier for workflow execution.
44
- * @param {string} [options.vmId=''] - VM identifier for workflow execution.
45
- * @param {string} [options.deployId=''] - Deployment identifier for workflow execution.
46
- * @param {string} [options.namespace=''] - Namespace for workflow execution.
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).
40
+ * @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.
43
+ * @param {string} [options.expose=''] - Expose VM ports to host: 'vmName:port1,port2'.
44
+ * @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.
47
50
  * @memberof UnderpostLxd
48
51
  */
49
52
  async callback(
@@ -53,11 +56,12 @@ class UnderpostLxd {
53
56
  dev: false,
54
57
  install: false,
55
58
  createVirtualNetwork: false,
59
+ ipv4Address: '10.250.250.1/24',
56
60
  createAdminProfile: false,
57
61
  control: false,
58
62
  worker: false,
59
- k3s: false,
60
63
  initVm: '',
64
+ deleteVm: '',
61
65
  createVm: '',
62
66
  infoVm: '',
63
67
  rootSize: '',
@@ -73,77 +77,101 @@ class UnderpostLxd {
73
77
  ) {
74
78
  const npmRoot = getNpmRootPath();
75
79
  const underpostRoot = options?.dev === true ? '.' : `${npmRoot}/underpost`;
80
+
76
81
  if (options.reset === true) {
77
82
  shellExec(`sudo systemctl stop snap.lxd.daemon`);
78
83
  shellExec(`sudo snap remove lxd --purge`);
79
84
  }
85
+
80
86
  if (options.install === true) shellExec(`sudo snap install lxd`);
87
+
81
88
  if (options.init === true) {
82
89
  shellExec(`sudo systemctl start snap.lxd.daemon`);
83
90
  shellExec(`sudo systemctl status snap.lxd.daemon`);
84
- const lxdPressedContent = fs
91
+ const lxdPreseedContent = fs
85
92
  .readFileSync(`${underpostRoot}/manifests/lxd/lxd-preseed.yaml`, 'utf8')
86
93
  .replaceAll(`127.0.0.1`, Underpost.dns.getLocalIPv4Address());
87
- shellExec(`echo "${lxdPressedContent}" | lxd init --preseed`);
94
+ shellExec(`echo "${lxdPreseedContent}" | lxd init --preseed`);
88
95
  shellExec(`lxc cluster list`);
89
96
  }
97
+
90
98
  if (options.createVirtualNetwork === true) {
99
+ const ipv4Address = options.ipv4Address ? options.ipv4Address : '10.250.250.1/24';
91
100
  shellExec(`lxc network create lxdbr0 \
92
- ipv4.address=10.250.250.1/24 \
101
+ ipv4.address=${ipv4Address} \
93
102
  ipv4.nat=true \
94
103
  ipv4.dhcp=true \
95
104
  ipv6.address=none`);
96
105
  }
106
+
97
107
  if (options.createAdminProfile === true) {
98
108
  const existingProfiles = await new Promise((resolve) => {
99
109
  shellExec(`lxc profile show admin-profile`, {
100
110
  silent: true,
101
- callback: (...args) => {
102
- return resolve(JSON.stringify(args));
103
- },
111
+ callback: (...args) => resolve(JSON.stringify(args)),
104
112
  });
105
113
  });
106
114
  if (existingProfiles.toLowerCase().match('error')) {
107
- logger.warn('Profile does not exist. Using following command to create admin-profile:');
115
+ logger.warn('Profile does not exist. Use the following command to create it:');
108
116
  pbcopy(`lxc profile create admin-profile`);
109
117
  } else {
110
118
  shellExec(`cat ${underpostRoot}/manifests/lxd/lxd-admin-profile.yaml | lxc profile edit admin-profile`);
111
119
  shellExec(`lxc profile show admin-profile`);
112
120
  }
113
121
  }
114
- if (options.createVm && typeof options.createVm === 'string') {
122
+
123
+ if (options.deleteVm) {
124
+ 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.`);
130
+ }
131
+
132
+ if (options.createVm) {
115
133
  pbcopy(
116
134
  `lxc launch images:rockylinux/9 ${
117
135
  options.createVm
118
136
  } --vm --target lxd-node1 -c limits.cpu=2 -c limits.memory=4GB --profile admin-profile -d root,size=${
119
- options.rootSize && typeof options.rootSize === 'string' ? options.rootSize + 'GiB' : '32GiB'
137
+ options.rootSize ? options.rootSize + 'GiB' : '32GiB'
120
138
  }`,
121
139
  );
122
140
  }
123
- if (options.initVm && typeof options.initVm === 'string') {
124
- let flag = '';
125
- if (options.control === true) {
126
- if (options.k3s === true) {
127
- flag = ' -s -- --k3s';
128
- } else {
129
- // Default to kubeadm if not K3s
130
- flag = ' -s -- --kubeadm';
131
- }
132
- await Underpost.lxd.runWorkflow({
133
- workflowId: 'engine',
134
- vmName: options.initVm,
135
- });
136
- } else if (options.worker == true) {
137
- if (options.k3s === true) {
138
- flag = ' -s -- --worker --k3s';
141
+
142
+ if (options.initVm) {
143
+ const vmName = options.initVm;
144
+ const lxdSetupPath = `${underpostRoot}/scripts/lxd-vm-setup.sh`;
145
+ const k3sSetupPath = `${underpostRoot}/scripts/k3s-node-setup.sh`;
146
+
147
+ // Step 1: OS base setup (disk, packages, kernel modules)
148
+ shellExec(`cat ${lxdSetupPath} | lxc exec ${vmName} -- bash`);
149
+
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)
154
+ if (options.worker === true) {
155
+ if (options.joinNode) {
156
+ const controlNode = options.joinNode.includes(',') ? options.joinNode.split(',').pop() : options.joinNode;
157
+ const k3sToken = shellExec(
158
+ `lxc exec ${controlNode} -- bash -c 'sudo cat /var/lib/rancher/k3s/server/node-token'`,
159
+ { stdout: true },
160
+ ).trim();
161
+ const controlPlaneIp = shellExec(
162
+ `lxc list ${controlNode} --format json | jq -r '.[0].state.network.enp5s0.addresses[] | select(.family=="inet") | .address'`,
163
+ { stdout: true },
164
+ ).trim();
165
+ logger.info(`Initializing worker ${vmName} and joining control plane ${controlNode} (${controlPlaneIp})`);
166
+ shellExec(
167
+ `cat ${k3sSetupPath} | lxc exec ${vmName} -- bash -s -- --worker --control-ip=${controlPlaneIp} --token=${k3sToken}`,
168
+ );
139
169
  } else {
140
- // Default to kubeadm worker
141
- flag = ' -s -- --worker';
170
+ shellExec(`cat ${k3sSetupPath} | lxc exec ${vmName} -- bash -s -- --worker`);
142
171
  }
172
+ } else {
173
+ shellExec(`cat ${k3sSetupPath} | lxc exec ${vmName} -- bash -s -- --control`);
143
174
  }
144
- console.log(`Executing underpost-setup.sh on VM: ${options.initVm}`);
145
- shellExec(`cat ${underpostRoot}/manifests/lxd/underpost-setup.sh | lxc exec ${options.initVm} -- bash${flag}`);
146
- console.log(`underpost-setup.sh execution completed on VM: ${options.initVm}`);
147
175
  }
148
176
 
149
177
  if (options.workflowId) {
@@ -155,241 +183,168 @@ ipv6.address=none`);
155
183
  });
156
184
  }
157
185
 
158
- if (options.joinNode && typeof options.joinNode === 'string') {
186
+ // Standalone join: --join-node workerName,controlName (without --init-vm)
187
+ if (options.joinNode && !options.initVm) {
159
188
  const [workerNode, controlNode] = options.joinNode.split(',');
160
- // Determine if it's a Kubeadm or K3s join
161
- const isK3sJoin = options.k3s === true;
162
-
163
- if (isK3sJoin) {
164
- console.log(`Attempting to join K3s worker node ${workerNode} to control plane ${controlNode}`);
165
- // Get K3s token from control plane
166
- const k3sToken = shellExec(
167
- `lxc exec ${controlNode} -- bash -c 'sudo cat /var/lib/rancher/k3s/server/node-token'`,
168
- { stdout: true },
169
- ).trim();
170
- // Get control plane IP
171
- const controlPlaneIp = shellExec(
172
- `lxc list ${controlNode} --format json | jq -r '.[0].state.network.enp5s0.addresses[] | select(.family=="inet") | .address'`,
173
- { stdout: true },
174
- ).trim();
175
-
176
- if (!k3sToken || !controlPlaneIp) {
177
- console.error(`Failed to get K3s token or control plane IP. Cannot join worker.`);
178
- return;
179
- }
180
- const k3sJoinCommand = `K3S_URL=https://${controlPlaneIp}:6443 K3S_TOKEN=${k3sToken} curl -sfL https://get.k3s.io | sh -`;
181
- shellExec(`lxc exec ${workerNode} -- bash -c '${k3sJoinCommand}'`);
182
- console.log(`K3s worker node ${workerNode} join command executed.`);
183
- } else {
184
- // Kubeadm join
185
- console.log(`Attempting to join Kubeadm worker node ${workerNode} to control plane ${controlNode}`);
186
- const token = shellExec(
187
- `echo "$(lxc exec ${controlNode} -- bash -c 'sudo kubeadm token create --print-join-command')"`,
188
- { stdout: true },
189
- );
190
- shellExec(`lxc exec ${workerNode} -- bash -c '${token}'`);
191
- console.log(`Kubeadm worker node ${workerNode} join command executed.`);
192
- }
189
+ const k3sToken = shellExec(
190
+ `lxc exec ${controlNode} -- bash -c 'sudo cat /var/lib/rancher/k3s/server/node-token'`,
191
+ { stdout: true },
192
+ ).trim();
193
+ const controlPlaneIp = shellExec(
194
+ `lxc list ${controlNode} --format json | jq -r '.[0].state.network.enp5s0.addresses[] | select(.family=="inet") | .address'`,
195
+ { stdout: true },
196
+ ).trim();
197
+ logger.info(`Joining K3s worker ${workerNode} to control plane ${controlNode} (${controlPlaneIp})`);
198
+ shellExec(
199
+ `lxc exec ${workerNode} -- bash -c 'K3S_URL=https://${controlPlaneIp}:6443 K3S_TOKEN=${k3sToken} curl -sfL https://get.k3s.io | sh -s - agent'`,
200
+ );
201
+ logger.info(`Worker ${workerNode} joined successfully.`);
193
202
  }
194
- if (options.infoVm && typeof options.infoVm === 'string') {
203
+
204
+ if (options.infoVm) {
195
205
  shellExec(`lxc config show ${options.infoVm}`);
196
206
  shellExec(`lxc info --show-log ${options.infoVm}`);
197
207
  shellExec(`lxc info ${options.infoVm}`);
198
208
  shellExec(`lxc list ${options.infoVm}`);
199
209
  }
200
- if (options.expose && typeof options.expose === 'string') {
210
+
211
+ if (options.expose) {
201
212
  const [vmName, ports] = options.expose.split(':');
202
- console.log({ vmName, ports });
203
- const protocols = ['tcp']; // udp
213
+ const protocols = ['tcp'];
204
214
  const hostIp = Underpost.dns.getLocalIPv4Address();
205
215
  const vmIp = shellExec(
206
216
  `lxc list ${vmName} --format json | jq -r '.[0].state.network.enp5s0.addresses[] | select(.family=="inet") | .address'`,
207
217
  { stdout: true },
208
218
  ).trim();
209
219
  if (!vmIp) {
210
- console.error(`Could not get VM IP for ${vmName}. Cannot expose ports.`);
220
+ logger.error(`Could not get VM IP for ${vmName}. Cannot expose ports.`);
211
221
  return;
212
222
  }
213
223
  for (const port of ports.split(',')) {
214
224
  for (const protocol of protocols) {
215
225
  const deviceName = `${vmName}-${protocol}-port-${port}`;
216
- shellExec(`lxc config device remove ${vmName} ${deviceName}`); // Use to prevent error if device doesn't exist
226
+ shellExec(`lxc config device remove ${vmName} ${deviceName}`);
217
227
  shellExec(
218
228
  `lxc config device add ${vmName} ${deviceName} proxy listen=${protocol}:${hostIp}:${port} connect=${protocol}:${vmIp}:${port} nat=true`,
219
229
  );
220
- console.log(`Manually exposed ${protocol}:${hostIp}:${port} -> ${vmIp}:${port} for ${vmName}`);
230
+ logger.info(`Exposed ${protocol}:${hostIp}:${port} -> ${vmIp}:${port} on ${vmName}`);
221
231
  }
222
232
  }
223
233
  }
224
- if (options.deleteExpose && typeof options.deleteExpose === 'string') {
225
- const [controlNode, ports] = options.deleteExpose.split(':');
226
- console.log({ controlNode, ports });
227
- const protocols = ['tcp']; // udp
234
+
235
+ if (options.deleteExpose) {
236
+ const [vmName, ports] = options.deleteExpose.split(':');
237
+ const protocols = ['tcp'];
228
238
  for (const port of ports.split(',')) {
229
239
  for (const protocol of protocols) {
230
- shellExec(`lxc config device remove ${controlNode} ${controlNode}-${protocol}-port-${port}`);
240
+ shellExec(`lxc config device remove ${vmName} ${vmName}-${protocol}-port-${port}`);
231
241
  }
232
242
  }
233
243
  }
234
244
 
235
- if (options.test && typeof options.test === 'string') {
245
+ if (options.test) {
236
246
  const vmName = options.test;
237
- console.log(`Starting comprehensive test for VM: ${vmName}`);
238
-
239
- // 1. Monitor for IPv4 address
240
- let vmIp = '';
241
- let retries = 0;
242
- const maxRetries = 10;
243
- const delayMs = 5000; // 5 seconds
244
-
245
- while (!vmIp && retries < maxRetries) {
246
- try {
247
- console.log(`Attempting to get IPv4 address for ${vmName} (Attempt ${retries + 1}/${maxRetries})...`);
248
- vmIp = shellExec(
249
- `lxc list ${vmName} --format json | jq -r '.[0].state.network.enp5s0.addresses[] | select(.family=="inet") | .address'`,
250
- { stdout: true },
251
- ).trim();
252
- if (vmIp) {
253
- console.log(`IPv4 address found for ${vmName}: ${vmIp}`);
254
- } else {
255
- console.log(`IPv4 address not yet available for ${vmName}. Retrying in ${delayMs / 1000} seconds...`);
256
- await new Promise((resolve) => setTimeout(resolve, delayMs));
257
- }
258
- } catch (error) {
259
- console.error(`Error getting IPv4 address: ${error.message}`);
260
- console.log(`Retrying in ${delayMs / 1000} seconds...`);
261
- await new Promise((resolve) => setTimeout(resolve, delayMs));
262
- }
263
- retries++;
264
- }
265
-
266
- if (!vmIp) {
267
- console.error(`Failed to get IPv4 address for ${vmName} after ${maxRetries} attempts. Aborting tests.`);
268
- return;
269
- }
270
-
271
- // 2. Iteratively check connection to google.com
272
- let connectedToGoogle = false;
273
- retries = 0;
274
- while (!connectedToGoogle && retries < maxRetries) {
275
- try {
276
- console.log(`Checking connectivity to google.com from ${vmName} (Attempt ${retries + 1}/${maxRetries})...`);
277
- const curlOutput = shellExec(
278
- `lxc exec ${vmName} -- bash -c 'curl -s -o /dev/null -w "%{http_code}" http://google.com'`,
279
- { stdout: true },
280
- );
281
- if (curlOutput.startsWith('2') || curlOutput.startsWith('3')) {
282
- console.log(`Successfully connected to google.com from ${vmName}.`);
283
- connectedToGoogle = true;
284
- } else {
285
- console.log(`Connectivity to google.com not yet verified. Retrying in ${delayMs / 1000} seconds...`);
286
- await new Promise((resolve) => setTimeout(resolve, delayMs));
287
- }
288
- } catch (error) {
289
- console.error(`Error checking connectivity to google.com: ${error.message}`);
290
- console.log(`Retrying in ${delayMs / 1000} seconds...`);
291
- await new Promise((resolve) => setTimeout(resolve, delayMs));
292
- }
293
- retries++;
294
- }
295
-
296
- if (!connectedToGoogle) {
297
- console.error(
298
- `Failed to connect to google.com from ${vmName} after ${maxRetries} attempts. Aborting further tests.`,
299
- );
300
- return;
301
- }
302
-
303
- // 3. Check other connectivity, network, and VM health parameters
304
- console.log(`\n--- Comprehensive Health Report for ${vmName} ---`);
305
-
306
- // VM Status
307
- console.log('\n--- VM Status ---');
308
- try {
309
- const vmStatus = shellExec(`lxc list ${vmName} --format json`, { stdout: true, silent: true });
310
- console.log(JSON.stringify(JSON.parse(vmStatus), null, 2));
311
- } catch (error) {
312
- console.error(`Error getting VM status: ${error.message}`);
313
- }
314
-
315
- // CPU Usage
316
- console.log('\n--- CPU Usage ---');
317
- try {
318
- const cpuUsage = shellExec(`lxc exec ${vmName} -- bash -c 'top -bn1 | grep "Cpu(s)"'`, { stdout: true });
319
- console.log(cpuUsage.trim());
320
- } catch (error) {
321
- console.error(`Error getting CPU usage: ${error.message}`);
322
- }
247
+ const vmIp = shellExec(
248
+ `lxc list ${vmName} --format json | jq -r '.[0].state.network.enp5s0.addresses[] | select(.family=="inet") | .address'`,
249
+ { stdout: true },
250
+ ).trim();
251
+ logger.info(`VM ${vmName} IPv4: ${vmIp || 'none'}`);
252
+ const httpStatus = shellExec(
253
+ `lxc exec ${vmName} -- curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://google.com`,
254
+ { stdout: true },
255
+ ).trim();
256
+ logger.info(`VM ${vmName} HTTP connectivity: ${httpStatus}`);
257
+ logger.info(`Health report for VM: ${vmName}`);
258
+ shellExec(`lxc list ${vmName} --format json`);
259
+ shellExec(`lxc exec ${vmName} -- bash -c 'top -bn1 | grep "Cpu(s)"'`);
260
+ shellExec(`lxc exec ${vmName} -- bash -c 'free -m'`);
261
+ shellExec(`lxc exec ${vmName} -- bash -c 'df -h /'`);
262
+ shellExec(`lxc exec ${vmName} -- bash -c 'ip a'`);
263
+ shellExec(`lxc exec ${vmName} -- bash -c 'cat /etc/resolv.conf'`);
264
+ shellExec(`lxc exec ${vmName} -- bash -c 'sudo k3s kubectl get nodes'`);
265
+ }
266
+ },
323
267
 
324
- // Memory Usage
325
- console.log('\n--- Memory Usage ---');
326
- try {
327
- const memoryUsage = shellExec(`lxc exec ${vmName} -- bash -c 'free -m'`, { stdout: true });
328
- console.log(memoryUsage.trim());
329
- } catch (error) {
330
- console.error(`Error getting memory usage: ${error.message}`);
331
- }
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
+ );
332
295
 
333
- // Disk Usage
334
- console.log('\n--- Disk Usage (Root Partition) ---');
335
- try {
336
- const diskUsage = shellExec(`lxc exec ${vmName} -- bash -c 'df -h /'`, { stdout: true });
337
- console.log(diskUsage.trim());
338
- } catch (error) {
339
- console.error(`Error getting disk usage: ${error.message}`);
340
- }
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 });
341
299
 
342
- // Network Interface Status
343
- console.log('\n--- Network Interface Status (ip a) ---');
344
- try {
345
- const ipA = shellExec(`lxc exec ${vmName} -- bash -c 'ip a'`, { stdout: true });
346
- console.log(ipA.trim());
347
- } catch (error) {
348
- console.error(`Error getting network interface status: ${error.message}`);
349
- }
300
+ // Reset destination directory inside the VM
301
+ shellExec(`lxc exec ${vmName} -- bash -c 'rm -rf ${destPath} && mkdir -p ${destPath}'`);
350
302
 
351
- // DNS Resolution (resolv.conf)
352
- console.log('\n--- DNS Configuration (/etc/resolv.conf) ---');
353
- try {
354
- const resolvConf = shellExec(`lxc exec ${vmName} -- bash -c 'cat /etc/resolv.conf'`, { stdout: true });
355
- console.log(resolvConf.trim());
356
- } catch (error) {
357
- console.error(`Error getting DNS configuration: ${error.message}`);
358
- }
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
+ );
359
307
 
360
- console.log(`\nComprehensive test for VM: ${vmName} completed.`);
361
- }
308
+ // Clean up temp includes file
309
+ fs.removeSync(includesFile);
362
310
  },
311
+
363
312
  /**
364
313
  * @method runWorkflow
365
314
  * @description Executes predefined workflows on LXD VMs.
366
- * @param {object} params - Parameters for the workflow.
367
- * @param {string} params.workflowId - The workflow id to execute (e.g., 'init').
368
- * @param {string} params.vmName - The name of the VM to run the workflow on.
369
- * @param {string} [params.deployId] - Optional deployment identifier.
370
- * @param {boolean} [params.dev=false] - Run in development mode (adjusts paths).
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.
371
320
  * @memberof UnderpostLxd
372
321
  */
373
322
  async runWorkflow({ workflowId, vmName, deployId, dev }) {
374
323
  switch (workflowId) {
375
324
  case 'engine': {
376
- const basePath = `/home/dd`;
377
- const subDir = 'engine';
378
- shellExec(`lxc exec ${vmName} -- bash -c 'rm ${basePath} && mkdir -p ${basePath}/${subDir}'`);
379
- shellExec(`lxc file push ${basePath}/${subDir}/package.json ${vmName}${basePath}/${subDir}/package.json`);
380
- shellExec(`lxc file push ${basePath}/${subDir}/src ${vmName}${basePath}/${subDir} --recursive`);
381
- shellExec(`lxc file push ${basePath}/${subDir}/${subDir}-private ${vmName}${basePath}/${subDir} --recursive`);
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
+ });
382
336
  break;
383
337
  }
384
- case 'setup-underpost-engine': {
385
- const basePath = `/home/dd/engine`;
386
- shellExec(`lxc exec ${vmName} -- bash -lc 'nvm use $(node --version) && cd ${basePath} && npm install'`);
387
- shellExec(`lxc exec ${vmName} -- bash -lc 'underpost run secret'`);
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`);
388
343
  break;
389
344
  }
390
- case 'k3s-setup': {
345
+ case 'dev-reset': {
391
346
  shellExec(
392
- `lxc exec ${vmName} -- bash -lc 'cd /home/dd/engine && node bin cluster --dev --reset && node bin cluster --dev --k3s'`,
347
+ `lxc exec ${vmName} -- bash -lc 'cd /home/dd/engine && node bin cluster --dev --reset --k3s && node bin cluster --dev --k3s'`,
393
348
  );
394
349
  break;
395
350
  }
@@ -124,6 +124,7 @@ class UnderpostRepository {
124
124
 
125
125
  if (options.changelog !== undefined || options.changelogBuild) {
126
126
  const ciIntegrationPrefix = 'ci(package-pwa-microservices-';
127
+ const ciIntegrationVersionPrefix = 'New release v:';
127
128
  const releaseMatch = 'New release v:';
128
129
 
129
130
  // Helper: parse [<tag>] commits into grouped sections
@@ -261,7 +262,9 @@ class UnderpostRepository {
261
262
  let commits;
262
263
  if (!hasExplicitCount) {
263
264
  // No explicit count: find commits up to the last CI integration boundary
264
- const ciIndex = allCommits.findIndex((c) => c.message.startsWith(ciIntegrationPrefix));
265
+ const ciIndex = allCommits.findIndex(
266
+ (c) => c.message.startsWith(ciIntegrationPrefix) && !c.message.match(ciIntegrationVersionPrefix),
267
+ );
265
268
  commits = ciIndex >= 0 ? allCommits.slice(0, ciIndex) : allCommits;
266
269
  } else {
267
270
  commits = allCommits;
package/src/cli/run.js CHANGED
@@ -1148,7 +1148,7 @@ EOF
1148
1148
  const baseClusterCommand = options.dev ? ' --dev' : '';
1149
1149
  const clusterType = options.k3s ? 'k3s' : 'kubeadm';
1150
1150
  shellCd(`/home/dd/engine`);
1151
- shellExec(`${baseCommand} cluster${baseClusterCommand} --reset`);
1151
+ shellExec(`${baseCommand} cluster${baseClusterCommand} --reset --${clusterType}`);
1152
1152
  await timer(5000);
1153
1153
  shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType}`);
1154
1154
  await timer(5000);
@@ -1779,8 +1779,9 @@ EOF
1779
1779
  * @memberof UnderpostRun
1780
1780
  */
1781
1781
  'gpu-env': (path, options = DEFAULT_OPTION) => {
1782
+ const clusterType = 'kubeadm';
1782
1783
  shellExec(
1783
- `node bin cluster --dev --reset && node bin cluster --dev --dedicated-gpu --kubeadm && kubectl get pods --all-namespaces -o wide -w`,
1784
+ `node bin cluster --dev --reset --${clusterType} && node bin cluster --dev --dedicated-gpu --${clusterType} && kubectl get pods --all-namespaces -o wide -w`,
1784
1785
  );
1785
1786
  },
1786
1787
  /**