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
@@ -1,3 +1,12 @@
1
+ /**
2
+ * User service module for handling user account operations.
3
+ * Provides REST API handlers for authentication, registration, profile management,
4
+ * email verification, password recovery, and guest user lifecycle management.
5
+ *
6
+ * @module src/api/user/user.service.js
7
+ * @namespace UserService
8
+ */
9
+
1
10
  import { loggerFactory } from '../../server/logger.js';
2
11
  import { DataQuery } from '../../server/data-query.js';
3
12
  import {
@@ -15,21 +24,40 @@ import { MailerProvider } from '../../mailer/MailerProvider.js';
15
24
  import { CoreWsEmitter } from '../../ws/core/core.ws.emit.js';
16
25
  import { CoreWsMailerChannel } from '../../ws/core/channels/core.ws.mailer.js';
17
26
  import validator from 'validator';
18
- import { DataBaseProvider } from '../../db/DataBaseProvider.js';
27
+ import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
19
28
  import { FileFactory, FileCleanup } from '../file/file.service.js';
20
29
  import { UserDto } from './user.model.js';
21
30
  import { timer } from '../../client/components/core/CommonJs.js';
22
31
  import { GuestService } from './guest.service.js';
32
+ import { resolveHostKeyContext } from '../../server/conf.js';
23
33
 
24
34
  const logger = loggerFactory(import.meta);
25
35
 
26
- const UserService = {
27
- post: async (req, res, options) => {
28
- /** @type {import('./user.model.js').UserModel} */
29
- const User = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.User;
30
-
36
+ /**
37
+ * User Service for handling REST API user operations.
38
+ * Manages authentication, registration, profile CRUD, email verification,
39
+ * password recovery, session management, and guest user lifecycle.
40
+ * @namespace UserService
41
+ */
42
+ class UserService {
43
+ /**
44
+ * POST - Create or authenticate users.
45
+ * Supports authentication, guest account creation, email verification,
46
+ * and password recovery email sending.
47
+ * @async
48
+ * @function post
49
+ * @memberof UserService
50
+ * @param {Object} req - Express request object.
51
+ * @param {Object} res - Express response object.
52
+ * @param {Object} options - Request options containing host and path.
53
+ * @returns {Promise<Object>} User data with auth token, or status message.
54
+ * @throws {Error} If authentication fails, email is invalid, or email send error.
55
+ */
56
+ static post = async (req, res, options) => {
31
57
  /** @type {import('../file/file.model.js').FileModel} */
32
- const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
58
+ const File = DataBaseProviderService.getModel('File', options);
59
+ /** @type {import('./user.model.js').UserModel} */
60
+ const User = DataBaseProviderService.getModel('User', options);
33
61
 
34
62
  if (req.params.id === 'recover-verify-email') {
35
63
  const user = await User.findOne({
@@ -44,11 +72,10 @@ const UserService = {
44
72
 
45
73
  const token = jwtSign({ email: req.body.email }, options, 15);
46
74
  const payloadToken = jwtSign({ email: req.body.email }, options, 15);
47
- const id = `${options.host}${options.path}`;
75
+ const id = resolveHostKeyContext(options);
48
76
  const translate = MailerProvider.instance[id].translateTemplates.recoverEmail;
49
- const recoverUrl = `${process.env.NODE_ENV === 'development' ? 'http://' : 'https://'}${req.body.hostname}${
50
- req.body.proxyPath
51
- }recover?payload=${payloadToken}`;
77
+ const recoverUrl = `${process.env.NODE_ENV === 'development' ? 'http://' : 'https://'}${req.body.hostname}${req.body.proxyPath
78
+ }recover?payload=${payloadToken}`;
52
79
  const sendResult = await MailerProvider.send({
53
80
  id,
54
81
  sendOptions: {
@@ -81,7 +108,7 @@ const UserService = {
81
108
  if (!validator.isEmail(req.body.email)) throw { message: 'invalid email' };
82
109
 
83
110
  const token = jwtSign({ email: req.body.email }, options, 15);
84
- const id = `${options.host}${options.path}`;
111
+ const id = resolveHostKeyContext(options);
85
112
  const user = await User.findById(req.auth.user._id);
86
113
 
87
114
  if (user.emailConfirmed) throw new Error('email already confirmed');
@@ -130,10 +157,9 @@ const UserService = {
130
157
  });
131
158
  const getMinutesRemaining = () => (-1 * user.failedLoginAttempts - new Date().getTime()) / (1000 * 60);
132
159
  const accountLocketMessage = () =>
133
- `Account locked. Please try again in: ${
134
- getMinutesRemaining() < 1
135
- ? `${(getMinutesRemaining() * 60).toFixed(0)} s`
136
- : `${getMinutesRemaining().toFixed(0)} min`
160
+ `Account locked. Please try again in: ${getMinutesRemaining() < 1
161
+ ? `${(getMinutesRemaining() * 60).toFixed(0)} s`
162
+ : `${getMinutesRemaining().toFixed(0)} min`
137
163
  }.`;
138
164
 
139
165
  if (user) {
@@ -231,13 +257,27 @@ const UserService = {
231
257
  default:
232
258
  return await createUserAndSession(req, res, User, options);
233
259
  }
234
- },
235
- get: async (req, res, options) => {
236
- /** @type {import('./user.model.js').UserModel} */
237
- const User = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.User;
238
-
260
+ };
261
+
262
+ /**
263
+ * GET - Retrieve user data.
264
+ * Supports public profile lookup by username, asset retrieval,
265
+ * email lookup, password recovery flow, email verification,
266
+ * admin user listing, authentication refresh, and profile retrieval.
267
+ * @async
268
+ * @function get
269
+ * @memberof UserService
270
+ * @param {Object} req - Express request object.
271
+ * @param {Object} res - Express response object.
272
+ * @param {Object} options - Request options containing host and path.
273
+ * @returns {Promise<Object>} User data with optional session token.
274
+ * @throws {Error} If user not found, profile is private, or token invalid.
275
+ */
276
+ static get = async (req, res, options) => {
239
277
  /** @type {import('../file/file.model.js').FileModel} */
240
- const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
278
+ const File = DataBaseProviderService.getModel('File', options);
279
+ /** @type {import('./user.model.js').UserModel} */
280
+ const User = DataBaseProviderService.getModel('User', options);
241
281
 
242
282
  if (req.path.startsWith('/u/')) {
243
283
  // First lookup user by username
@@ -309,7 +349,7 @@ const UserService = {
309
349
  {
310
350
  const user = await User.findByIdAndUpdate(_id, { emailConfirmed: true }, { runValidators: true });
311
351
  }
312
- const userWsId = CoreWsMailerChannel.getUserWsId(`${options.host}${options.path}`, user._id.toString());
352
+ const userWsId = CoreWsMailerChannel.getUserWsId(resolveHostKeyContext(options), user._id.toString());
313
353
  if (userWsId && CoreWsMailerChannel.client[userWsId]) {
314
354
  CoreWsEmitter.emit(CoreWsMailerChannel.channel, CoreWsMailerChannel.client[userWsId], {
315
355
  status: 'email-confirmed',
@@ -384,10 +424,23 @@ const UserService = {
384
424
  }
385
425
  }
386
426
  }
387
- },
388
- delete: async (req, res, options) => {
427
+ };
428
+
429
+ /**
430
+ * DELETE - Remove user accounts or log out sessions.
431
+ * Supports admin bulk deletion, user self-deletion, and session logout.
432
+ * @async
433
+ * @function delete
434
+ * @memberof UserService
435
+ * @param {Object} req - Express request object.
436
+ * @param {Object} res - Express response object.
437
+ * @param {Object} options - Request options containing host and path.
438
+ * @returns {Promise<Object>} Deleted user data or logout status message.
439
+ * @throws {Error} If logout fails, user not found, or token invalid.
440
+ */
441
+ static delete = async (req, res, options) => {
389
442
  /** @type {import('./user.model.js').UserModel} */
390
- const User = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.User;
443
+ const User = DataBaseProviderService.getModel('User', options);
391
444
 
392
445
  if (req.params.id === 'logout') {
393
446
  const result = await logoutSession(User, req, res);
@@ -417,13 +470,25 @@ const UserService = {
417
470
  }
418
471
  }
419
472
  }
420
- },
421
- put: async (req, res, options) => {
422
- /** @type {import('./user.model.js').UserModel} */
423
- const User = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.User;
424
-
473
+ };
474
+
475
+ /**
476
+ * PUT - Update user data.
477
+ * Supports profile image upload, password recovery, and profile field updates.
478
+ * @async
479
+ * @function put
480
+ * @memberof UserService
481
+ * @param {Object} req - Express request object.
482
+ * @param {Object} res - Express response object.
483
+ * @param {Object} options - Request options containing host and path.
484
+ * @returns {Promise<Object>} Updated user data.
485
+ * @throws {Error} If user not found, token invalid, or invalid file.
486
+ */
487
+ static put = async (req, res, options) => {
425
488
  /** @type {import('../file/file.model.js').FileModel} */
426
- const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
489
+ const File = DataBaseProviderService.getModel('File', options);
490
+ /** @type {import('./user.model.js').UserModel} */
491
+ const User = DataBaseProviderService.getModel('User', options);
427
492
 
428
493
  // req.path | req.baseUrl
429
494
 
@@ -511,7 +576,7 @@ const UserService = {
511
576
  }
512
577
  }
513
578
  }
514
- },
515
- };
579
+ };
580
+ }
516
581
 
517
- export { UserService };
582
+ export { UserService };
@@ -356,7 +356,7 @@ class UnderpostBaremetal {
356
356
 
357
357
  // Build phase (skip if upload-only mode)
358
358
  if (options.packerMaasImageBuild) {
359
- if (shellExec('packer version').code !== 0) {
359
+ if (shellExec('packer version', { silentOnError: true }).code !== 0) {
360
360
  throw new Error('Packer is not installed. Please install Packer to proceed.');
361
361
  }
362
362
 
@@ -424,7 +424,9 @@ rm -rf ${artifacts.join(' ')}`);
424
424
  const uploadCmd = `${uploadScript} ${process.env.MAAS_ADMIN_USERNAME} "${workflow.maas.name}" "${workflow.maas.title}" "${workflow.maas.architecture}" "${workflow.maas.base_image}" "${workflow.maas.filetype}" "${tarballPath}"`;
425
425
 
426
426
  logger.info(`Uploading to MAAS using: ${uploadScript}`);
427
- const uploadResult = shellExec(uploadCmd);
427
+ // silentOnError: caller logs stdout/stderr structure on failure
428
+ // before throwing its own, more informative error.
429
+ const uploadResult = shellExec(uploadCmd, { silentOnError: true });
428
430
  if (uploadResult.code !== 0) {
429
431
  logger.error(`Upload failed with exit code: ${uploadResult.code}`);
430
432
  if (uploadResult.stdout) {
@@ -2895,9 +2897,16 @@ EOF`);
2895
2897
  for (const mountPath of mounts[mountCmd]) {
2896
2898
  const hostMountPath = `${process.env.NFS_EXPORT_PATH}/${hostname}${mountPath}`;
2897
2899
  // Check if the path is already mounted using `mountpoint` command.
2898
- const isPathMounted = !shellExec(`mountpoint ${hostMountPath}`, { silent: true, stdout: true }).match(
2899
- 'not a mountpoint',
2900
- );
2900
+ // `mountpoint` exits 1 when the path is not a mountpoint — silentOnError
2901
+ // prevents ShellExecError so we can inspect stdout/stderr for the string.
2902
+ const mountpointOut = shellExec(`mountpoint ${hostMountPath}`, {
2903
+ silent: true,
2904
+ stdout: true,
2905
+ silentOnError: true,
2906
+ });
2907
+ const isPathMounted = typeof mountpointOut === 'string' && mountpointOut.length > 0
2908
+ ? !mountpointOut.match('not a mountpoint') && !mountpointOut.match('No such file')
2909
+ : false;
2901
2910
 
2902
2911
  if (isPathMounted) {
2903
2912
  logger.warn('Nfs path already mounted', mountPath);
@@ -3041,10 +3050,10 @@ udp-port = 32766
3041
3050
  // Check both /usr/local/bin (compiled) and system paths
3042
3051
  let qemuAarch64Path = null;
3043
3052
 
3044
- if (shellExec('test -x /usr/local/bin/qemu-system-aarch64').code === 0) {
3053
+ if (shellExec('test -x /usr/local/bin/qemu-system-aarch64', { silentOnError: true }).code === 0) {
3045
3054
  qemuAarch64Path = '/usr/local/bin/qemu-system-aarch64';
3046
- } else if (shellExec('which qemu-system-aarch64').code === 0) {
3047
- qemuAarch64Path = shellExec('which qemu-system-aarch64').stdout.trim();
3055
+ } else if (shellExec('which qemu-system-aarch64', { silentOnError: true }).code === 0) {
3056
+ qemuAarch64Path = shellExec('which qemu-system-aarch64', { stdout: true }).trim();
3048
3057
  }
3049
3058
 
3050
3059
  if (!qemuAarch64Path) {
@@ -3070,10 +3079,10 @@ udp-port = 32766
3070
3079
  // Check both /usr/local/bin (compiled) and system paths
3071
3080
  let qemuX86Path = null;
3072
3081
 
3073
- if (shellExec('test -x /usr/local/bin/qemu-system-x86_64').code === 0) {
3082
+ if (shellExec('test -x /usr/local/bin/qemu-system-x86_64', { silentOnError: true }).code === 0) {
3074
3083
  qemuX86Path = '/usr/local/bin/qemu-system-x86_64';
3075
- } else if (shellExec('which qemu-system-x86_64').code === 0) {
3076
- qemuX86Path = shellExec('which qemu-system-x86_64').stdout.trim();
3084
+ } else if (shellExec('which qemu-system-x86_64', { silentOnError: true }).code === 0) {
3085
+ qemuX86Path = shellExec('which qemu-system-x86_64', { stdout: true }).trim();
3077
3086
  }
3078
3087
 
3079
3088
  if (!qemuX86Path) {
@@ -7,6 +7,8 @@
7
7
  import { getNpmRootPath } from '../server/conf.js';
8
8
  import { loggerFactory } from '../server/logger.js';
9
9
  import { shellExec } from '../server/process.js';
10
+ import { MONGODB_DEFAULT_REPLICA_COUNT } from '../db/mongo/MongooseDB.js';
11
+ import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
10
12
  import os from 'os';
11
13
  import fs from 'fs-extra';
12
14
  import Underpost from '../index.js';
@@ -22,6 +24,7 @@ const logger = loggerFactory(import.meta);
22
24
  */
23
25
  class UnderpostCluster {
24
26
  static API = {
27
+
25
28
  /**
26
29
  * @method init
27
30
  * @description Initializes and configures the Kubernetes cluster based on provided options.
@@ -41,6 +44,7 @@ class UnderpostCluster {
41
44
  * @param {boolean} [options.certManager=false] - Deploy Cert-Manager for certificate management.
42
45
  * @param {boolean} [options.listPods=false] - List Kubernetes pods.
43
46
  * @param {boolean} [options.reset=false] - Perform a comprehensive reset of Kubernetes and container environments.
47
+ * @param {boolean} [options.resetMongodb=false] - Perform a targeted reset of MongoDB components without restarting the entire cluster.
44
48
  * @param {boolean} [options.dev=false] - Run in development mode (adjusts paths).
45
49
  * @param {string} [options.nsUse=''] - Set the current kubectl namespace (creates namespace if it doesn't exist).
46
50
  * @param {string} [options.namespace='default'] - Kubernetes namespace for cluster operations.
@@ -78,6 +82,7 @@ class UnderpostCluster {
78
82
  certManager: false,
79
83
  listPods: false,
80
84
  reset: false,
85
+ resetMongodb: false,
81
86
  dev: false,
82
87
  nsUse: '',
83
88
  namespace: 'default',
@@ -120,6 +125,7 @@ class UnderpostCluster {
120
125
  const namespaceExists = shellExec(`kubectl get namespace ${options.nsUse} --ignore-not-found -o name`, {
121
126
  stdout: true,
122
127
  silent: true,
128
+ silentOnError: true,
123
129
  }).trim();
124
130
 
125
131
  if (!namespaceExists) {
@@ -145,6 +151,16 @@ class UnderpostCluster {
145
151
  });
146
152
  }
147
153
 
154
+ // Targeted MongoDB-only reset (does not restart the whole node)
155
+ if (options.resetMongodb) {
156
+ const clusterType = options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind';
157
+ return await MongoBootstrap.reset({
158
+ namespace: options.namespace,
159
+ clusterType,
160
+ underpostRoot,
161
+ });
162
+ }
163
+
148
164
  // Check if a cluster (Kind, Kubeadm, or K3s) is already initialized
149
165
  const alreadyKubeadmCluster = Underpost.kubectl.get('calico-kube-controllers')[0];
150
166
  const alreadyKindCluster = Underpost.kubectl.get('kube-apiserver-kind-control-plane')[0];
@@ -208,15 +224,40 @@ class UnderpostCluster {
208
224
  `kubectl apply -f https://cdn.jsdelivr.net/gh/rancher/local-path-provisioner@master/deploy/local-path-storage.yaml`,
209
225
  );
210
226
  } else {
211
- // Kind cluster initialization (if not using kubeadm or k3s)
227
+ // Kind cluster initialization (default for development)
212
228
  logger.info('Initializing Kind cluster...');
213
- shellExec(
214
- `cd ${underpostRoot}/manifests && kind create cluster --config kind-config${
215
- options.dev ? '-dev' : ''
216
- }.yaml`,
217
- );
229
+ const devReplicaCount = Math.max(Number(options.replicas) || MONGODB_DEFAULT_REPLICA_COUNT, 3);
230
+ shellExec(`sudo mkdir -p /data/mongodb`);
231
+ for (let index = 0; index < devReplicaCount; index++) {
232
+ shellExec(`sudo mkdir -p /data/mongodb/v${index}`);
233
+ }
234
+ const kindCreateCmd = `cd ${underpostRoot}/manifests && kind create cluster --config kind-config-dev.yaml`;
235
+ try {
236
+ shellExec(kindCreateCmd);
237
+ } catch (error) {
238
+ const kindCreateErrText = `${error?.message || ''}\n${error?.stderr || ''}`;
239
+ if (kindCreateErrText.includes('all predefined address pools have been fully subnetted')) {
240
+ logger.warn('Docker address pool exhausted while creating Kind cluster. Running cleanup and retrying once...');
241
+ Underpost.cluster.recoverKindDockerNetworks();
242
+ try {
243
+ shellExec(kindCreateCmd);
244
+ } catch (retryError) {
245
+ const retryErrText = `${retryError?.message || ''}\n${retryError?.stderr || ''}`;
246
+ if (retryErrText.includes('all predefined address pools have been fully subnetted')) {
247
+ logger.warn('Kind retry still failed from pool exhaustion. Applying Docker daemon address-pool config and retrying once more...');
248
+ Underpost.cluster.ensureDockerDefaultAddressPools();
249
+ shellExec(kindCreateCmd);
250
+ } else {
251
+ throw retryError;
252
+ }
253
+ }
254
+ } else {
255
+ throw error;
256
+ }
257
+ }
218
258
  Underpost.cluster.chown('kind'); // Pass 'kind' to chown
219
259
  }
260
+ Underpost.cluster.natSetup({ underpostRoot });
220
261
  }
221
262
 
222
263
  // --- Optional Component Deployments (Databases, Ingress, Cert-Manager) ---
@@ -317,33 +358,16 @@ EOF
317
358
  );
318
359
  }
319
360
  } else if (options.mongodb) {
320
- if (options.pullImage) Underpost.cluster.pullImage('mongo:latest', options);
321
- shellExec(
322
- `sudo kubectl create secret generic mongodb-keyfile --from-file=/home/dd/engine/engine-private/mongodb-keyfile --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
323
- );
324
- shellExec(
325
- `sudo kubectl create secret generic mongodb-secret --from-file=username=/home/dd/engine/engine-private/mongodb-username --from-file=password=/home/dd/engine/engine-private/mongodb-password --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
326
- );
327
- shellExec(`kubectl delete statefulset mongodb -n ${options.namespace} --ignore-not-found`);
328
- shellExec(`kubectl apply -f ${underpostRoot}/manifests/mongodb/storage-class.yaml -n ${options.namespace}`);
329
- shellExec(`kubectl apply -k ${underpostRoot}/manifests/mongodb -n ${options.namespace}`);
330
-
331
- const successInstance = await Underpost.test.statusMonitor('mongodb-0', 'Running', 'pods', 1000, 60 * 10);
332
-
333
- if (successInstance) {
334
- if (!options.mongoDbHost) options.mongoDbHost = 'mongodb-0.mongodb-service';
335
- const mongoConfig = {
336
- _id: 'rs0',
337
- members: options.mongoDbHost.split(',').map((host, index) => ({ _id: index, host: `${host}:27017` })),
338
- };
339
-
340
- shellExec(
341
- `sudo kubectl exec -i mongodb-0 -- mongosh --quiet --json=relaxed \
342
- --eval 'use admin' \
343
- --eval 'rs.initiate(${JSON.stringify(mongoConfig)})' \
344
- --eval 'rs.status()'`,
345
- );
346
- }
361
+ const clusterType = options.k3s ? 'k3s' : options.kubeadm ? 'kubeadm' : 'kind';
362
+ await MongoBootstrap.initReplicaSet({
363
+ namespace: options.namespace,
364
+ replicaCount: Number(options.replicas) || MONGODB_DEFAULT_REPLICA_COUNT,
365
+ mongoDbHost: options.mongoDbHost || '',
366
+ pullImage: options.pullImage,
367
+ reset: options.reset,
368
+ clusterType,
369
+ underpostRoot,
370
+ });
347
371
  }
348
372
 
349
373
  if (options.contour) {
@@ -419,7 +443,8 @@ EOF
419
443
  * This method ensures proper SELinux, Docker, Containerd, and Sysctl settings
420
444
  * are applied for a healthy Kubernetes environment. It explicitly avoids
421
445
  * iptables flushing commands to prevent conflicts with Kubernetes' own network management.
422
- * @param {string} underpostRoot - The root directory of the underpost project.
446
+ * @param {object} [options] - Configuration options for host setup.
447
+ * @param {string} [options.underpostRoot] - The root path of the underpost project, used for locating scripts if needed.
423
448
  * @memberof UnderpostCluster
424
449
  */
425
450
  config(options = { underpostRoot: '.' }) {
@@ -451,6 +476,27 @@ EOF
451
476
  // Reload systemd daemon to pick up new unit files/changes
452
477
  shellExec(`sudo systemctl daemon-reload`);
453
478
 
479
+ // Increase inotify limits
480
+ shellExec(`sudo sysctl -w fs.inotify.max_user_watches=2099999999`);
481
+ shellExec(`sudo sysctl -w fs.inotify.max_user_instances=2099999999`);
482
+ shellExec(`sudo sysctl -w fs.inotify.max_queued_events=2099999999`);
483
+
484
+ },
485
+
486
+ /**
487
+ * @method natSetup
488
+ * @description Configures NAT and iptables settings for Kubernetes networking.
489
+ * This method enables necessary sysctl settings for bridge networking and applies iptables rules
490
+ * required for Kubernetes cluster communication. It is designed to work with kubeadm and k3s clusters, ensuring that
491
+ * traffic through Linux bridges is processed by iptables, which is crucial for CNI plugins to function correctly.
492
+ * The method also applies NAT iptables rules and configures firewalld for Kubernetes, which is required for multi-machine kubeadm inter-node communication.
493
+ * Note: This method should be called after the cluster is initialized and before deploying any workloads that require network communication.
494
+ * @param {object} [options] - Configuration options for NAT setup.
495
+ * @param {string} [options.underpostRoot] - The root path of the underpost project, used to locate the nat-iptables.sh script.
496
+ * @memberof UnderpostCluster
497
+ */
498
+ natSetup(options = { underpostRoot: '.' }) {
499
+ const { underpostRoot } = options;
454
500
  // Enable bridge-nf-call-iptables for Kubernetes networking
455
501
  // This ensures traffic through Linux bridges is processed by iptables (crucial for CNI)
456
502
  for (const iptableConfPath of [
@@ -465,12 +511,6 @@ net.bridge.bridge-nf-call-arptables = 1
465
511
  net.ipv4.ip_forward = 1' | sudo tee ${iptableConfPath}`,
466
512
  { silent: true },
467
513
  );
468
-
469
- // Increase inotify limits
470
- shellExec(`sudo sysctl -w fs.inotify.max_user_watches=2099999999`);
471
- shellExec(`sudo sysctl -w fs.inotify.max_user_instances=2099999999`);
472
- shellExec(`sudo sysctl -w fs.inotify.max_queued_events=2099999999`);
473
-
474
514
  // shellExec(`sudo sysctl --system`); // Apply sysctl changes immediately
475
515
  // Apply NAT iptables rules and configure firewalld for Kubernetes.
476
516
  // nat-iptables.sh enables firewalld and opens all required ports; do NOT stop it
@@ -553,6 +593,10 @@ net.ipv4.ip_forward = 1' | sudo tee ${iptableConfPath}`,
553
593
  // Phase 1: Clean up Persistent Volumes with hostPath
554
594
  // This targets data created by Kubernetes Persistent Volumes that use hostPath.
555
595
  logger.info('Phase 1/7: Cleaning Kubernetes hostPath volumes...');
596
+ if ((options.clusterType || 'kind') === 'kind') {
597
+ logger.info(' -> Kind detected: cleaning node-local MongoDB hostPath directories...');
598
+ Underpost.cluster.cleanKindMongoHostPaths({ basePath: '/data/mongodb', replicaCount: 3 });
599
+ }
556
600
  if (options.removeVolumeHostPaths)
557
601
  try {
558
602
  const pvListJson = shellExec(`kubectl get pv -o json || echo '{"items":[]}'`, {
@@ -595,7 +639,8 @@ net.ipv4.ip_forward = 1' | sudo tee ${iptableConfPath}`,
595
639
  shellExec('sudo systemctl stop podman');
596
640
  // Lazy-unmount all kubelet pod mounts to avoid 'Device or resource busy' on rm.
597
641
  shellExec(
598
- `sudo sh -c 'findmnt --raw --noheadings -o TARGET | grep /var/lib/kubelet | sort -r | xargs -r umount -l' 2>/dev/null || true`,
642
+ `sudo sh -c 'findmnt --raw --noheadings -o TARGET | grep /var/lib/kubelet | sort -r | xargs -r umount -l'`,
643
+ { silentOnError: true },
599
644
  );
600
645
 
601
646
  // Phase 3: Execute official uninstallation commands (type-specific)
@@ -607,11 +652,11 @@ net.ipv4.ip_forward = 1' | sudo tee ${iptableConfPath}`,
607
652
  // Kill control plane processes that hold ports (6443, 10257, 10259, 2379, 2380)
608
653
  // so the next `kubeadm init` does not fail with [ERROR Port-xxxx].
609
654
  logger.info(' -> Stopping and killing control plane containers and processes...');
610
- shellExec('sudo crictl rm -a -f 2>/dev/null || true');
611
- shellExec('sudo crictl rmp -a -f 2>/dev/null || true');
612
- shellExec('sudo systemctl stop etcd 2>/dev/null || true');
655
+ shellExec('sudo crictl rm -a -f', { silentOnError: true });
656
+ shellExec('sudo crictl rmp -a -f', { silentOnError: true });
657
+ shellExec('sudo systemctl stop etcd', { silentOnError: true });
613
658
  for (const port of [6443, 10259, 10257, 2379, 2380])
614
- shellExec(`sudo fuser -k ${port}/tcp 2>/dev/null || true`);
659
+ shellExec(`sudo fuser -k ${port}/tcp`, { silentOnError: true });
615
660
  logger.info(' -> Executing kubeadm reset...');
616
661
  shellExec('sudo kubeadm reset --force');
617
662
  } else if (clusterType === 'k3s') {
@@ -620,7 +665,13 @@ net.ipv4.ip_forward = 1' | sudo tee ${iptableConfPath}`,
620
665
  } else {
621
666
  // Default: kind
622
667
  logger.info(' -> Deleting Kind clusters...');
623
- shellExec('kind get clusters | xargs -r -t -n1 kind delete cluster');
668
+ shellExec(`clusters=$(kind get clusters)
669
+ if [ -n "$clusters" ]; then
670
+ for c in $clusters; do
671
+ echo "Deleting cluster: $c"
672
+ kind delete cluster --name "$c"
673
+ done
674
+ fi`);
624
675
  }
625
676
 
626
677
  // Phase 4: File system cleanup
@@ -630,7 +681,8 @@ net.ipv4.ip_forward = 1' | sudo tee ${iptableConfPath}`,
630
681
  shellExec('sudo rm -rf /etc/cni/net.d/*');
631
682
  // Second-pass lazy umount before rm to clear any remaining busy mounts.
632
683
  shellExec(
633
- `sudo sh -c 'findmnt --raw --noheadings -o TARGET | grep /var/lib/kubelet | sort -r | xargs -r umount -l' 2>/dev/null || true`,
684
+ `sudo sh -c 'findmnt --raw --noheadings -o TARGET | grep /var/lib/kubelet | sort -r | xargs -r umount -l'`,
685
+ { silentOnError: true },
634
686
  );
635
687
  shellExec('sudo rm -rf /var/lib/kubelet/*');
636
688
  shellExec('sudo rm -rf /var/lib/etcd');
@@ -646,14 +698,14 @@ net.ipv4.ip_forward = 1' | sudo tee ${iptableConfPath}`,
646
698
  // Remove iptables rules and CNI network interfaces.
647
699
  shellExec('sudo iptables -F');
648
700
  shellExec('sudo iptables -t nat -F');
649
- shellExec('sudo ip link del cni0 2>/dev/null || true');
650
- shellExec('sudo ip link del flannel.1 2>/dev/null || true');
651
- shellExec('sudo ip link del vxlan.calico 2>/dev/null || true');
652
- shellExec('sudo ip link del tunl0 2>/dev/null || true');
701
+ shellExec('sudo ip link del cni0', { silentOnError: true });
702
+ shellExec('sudo ip link del flannel.1', { silentOnError: true });
703
+ shellExec('sudo ip link del vxlan.calico', { silentOnError: true });
704
+ shellExec('sudo ip link del tunl0', { silentOnError: true });
653
705
 
654
706
  logger.info('Phase 6/7: Clean up images');
655
- shellExec('sudo podman rmi --all --force 2>/dev/null || true');
656
- shellExec('sudo crictl rmi --prune 2>/dev/null || true');
707
+ shellExec('sudo podman rmi --all --force', { silentOnError: true });
708
+ shellExec('sudo crictl rmi --prune', { silentOnError: true });
657
709
 
658
710
  // Phase 6: Reload daemon and finalize
659
711
  logger.info('Phase 7/7: Reloading the system daemon and finalizing...');
@@ -785,8 +837,8 @@ EOF`);
785
837
 
786
838
  // Remove CRI-O
787
839
  console.log('Removing CRI-O...');
788
- shellExec(`sudo systemctl stop crio 2>/dev/null || true`);
789
- shellExec(`sudo systemctl disable crio 2>/dev/null || true`);
840
+ shellExec('sudo systemctl stop crio', { silentOnError: true });
841
+ shellExec('sudo systemctl disable crio', { silentOnError: true });
790
842
  shellExec(`sudo dnf -y remove cri-o`);
791
843
  shellExec(`sudo rm -f /etc/yum.repos.d/cri-o.repo`);
792
844
  shellExec(`sudo rm -f /etc/crictl.yaml`);
@@ -827,6 +879,95 @@ EOF`);
827
879
 
828
880
  console.log('Uninstall process completed.');
829
881
  },
882
+
883
+ /**
884
+ * @method cleanKindMongoHostPaths
885
+ * @description Best-effort cleanup of MongoDB hostPath directories inside Kind node containers.
886
+ * This prevents stale replica/auth state when hostPath data lives in node-local container filesystems.
887
+ * @param {object} [options]
888
+ * @param {string} [options.basePath='/data/mongodb'] - Node-internal base path for MongoDB data.
889
+ * @param {number} [options.replicaCount=3] - Number of replica ordinal directories (v0..vN-1).
890
+ * @memberof UnderpostCluster
891
+ */
892
+ cleanKindMongoHostPaths(options = { basePath: '/data/mongodb', replicaCount: 3 }) {
893
+ const basePath = options.basePath || '/data/mongodb';
894
+ const replicaCount = Math.max(Number(options.replicaCount) || 3, 1);
895
+ const nodesRaw = shellExec('kind get nodes', {
896
+ stdout: true,
897
+ silent: true,
898
+ silentOnError: true,
899
+ });
900
+ const nodes = nodesRaw
901
+ .split('\n')
902
+ .map((node) => node.trim())
903
+ .filter((node) => !!node);
904
+
905
+ if (nodes.length === 0) {
906
+ logger.info('No Kind nodes detected for node-local MongoDB hostPath cleanup.');
907
+ return;
908
+ }
909
+
910
+ for (const node of nodes) {
911
+ logger.info(
912
+ `Cleaning Kind node-local MongoDB paths '${basePath}/v0..v${replicaCount - 1}' on node '${node}'...`,
913
+ );
914
+ const prepareReplicaDirsCmd = Array.from(
915
+ { length: replicaCount },
916
+ (_, index) => `mkdir -p ${basePath}/v${index}; rm -rf ${basePath}/v${index}/*;`,
917
+ ).join(' ');
918
+ const verifyReplicaDirsCmd = Array.from(
919
+ { length: replicaCount },
920
+ (_, index) => `test -d ${basePath}/v${index};`,
921
+ ).join(' ');
922
+ shellExec(
923
+ `sudo docker exec ${node} sh -lc 'mkdir -p ${basePath}; ${prepareReplicaDirsCmd}'`,
924
+ { silentOnError: true },
925
+ );
926
+ shellExec(`sudo docker exec ${node} sh -lc '${verifyReplicaDirsCmd}'`);
927
+ }
928
+ },
929
+
930
+ /**
931
+ * @method recoverKindDockerNetworks
932
+ * @description Best-effort cleanup of stale Kind Docker resources when Docker bridge
933
+ * address pools are exhausted and new networks cannot be allocated.
934
+ * @memberof UnderpostCluster
935
+ */
936
+ recoverKindDockerNetworks() {
937
+ logger.warn('Attempting Docker network recovery for Kind (address pool exhaustion detected)...');
938
+ shellExec(`sudo docker ps -aq --filter label=io.x-k8s.kind.cluster | xargs -r sudo docker rm -f`, {
939
+ silentOnError: true,
940
+ });
941
+ shellExec(`sudo docker network ls -q --filter label=io.x-k8s.kind.cluster | xargs -r sudo docker network rm`, {
942
+ silentOnError: true,
943
+ });
944
+ shellExec(`sudo docker network rm kind`, { silentOnError: true });
945
+ shellExec(`sudo docker network prune -f`, { silentOnError: true });
946
+ },
947
+
948
+ /**
949
+ * @method ensureDockerDefaultAddressPools
950
+ * @description Writes a sane Docker default-address-pools config to reduce
951
+ * Kind network allocation failures on hosts with exhausted predefined pools.
952
+ * @memberof UnderpostCluster
953
+ */
954
+ ensureDockerDefaultAddressPools() {
955
+ logger.warn('Applying Docker default-address-pools workaround for Kind network creation...');
956
+ shellExec(`cat <<'EOF' | sudo tee /etc/docker/daemon.json
957
+ {
958
+ "default-address-pools": [
959
+ {"base": "172.17.0.0/16", "size": 24},
960
+ {"base": "172.18.0.0/16", "size": 24},
961
+ {"base": "172.19.0.0/16", "size": 24},
962
+ {"base": "172.20.0.0/14", "size": 24},
963
+ {"base": "172.24.0.0/14", "size": 24}
964
+ ]
965
+ }
966
+ EOF`);
967
+ shellExec('sudo systemctl restart docker');
968
+ shellExec('sudo docker network prune -f', { silentOnError: true });
969
+ },
970
+
830
971
  };
831
972
  }
832
973
  export default UnderpostCluster;