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.
- package/.github/workflows/npmpkg.ci.yml +1 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -0
- package/.vscode/settings.json +10 -5
- package/CHANGELOG.md +122 -1
- package/CLI-HELP.md +22 -7
- package/README.md +37 -8
- package/bin/build.js +26 -9
- package/bin/deploy.js +20 -21
- package/bin/file.js +31 -13
- package/bin/index.js +2 -1
- package/bin/vs.js +1 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -4
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
- package/manifests/kind-config-dev.yaml +8 -0
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/package.json +27 -12
- package/scripts/k3s-node-setup.sh +28 -9
- package/src/api/core/core.router.js +19 -14
- package/src/api/core/core.service.js +5 -5
- package/src/api/default/default.router.js +22 -18
- package/src/api/default/default.service.js +5 -5
- package/src/api/document/document.router.js +28 -23
- package/src/api/document/document.service.js +100 -23
- package/src/api/file/file.router.js +19 -13
- package/src/api/file/file.service.js +9 -7
- package/src/api/test/test.router.js +17 -12
- package/src/api/types.js +24 -0
- package/src/api/user/guest.service.js +5 -4
- package/src/api/user/user.router.js +297 -288
- package/src/api/user/user.service.js +100 -35
- package/src/cli/baremetal.js +20 -11
- package/src/cli/cluster.js +196 -55
- package/src/cli/db.js +59 -60
- package/src/cli/deploy.js +273 -159
- package/src/cli/fs.js +3 -1
- package/src/cli/index.js +16 -9
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +217 -135
- package/src/cli/release.js +289 -131
- package/src/cli/repository.js +58 -7
- package/src/cli/run.js +152 -25
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +4 -0
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Worker.js +162 -363
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +120 -20
- package/src/db/mongo/MongoBootstrap.js +587 -0
- package/src/db/mongo/MongooseDB.js +126 -22
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/wp/Wp.js +8 -5
- package/src/server/auth.js +2 -2
- package/src/server/client-build-docs.js +1 -1
- package/src/server/client-build.js +94 -129
- package/src/server/conf.js +20 -65
- package/src/server/process.js +180 -19
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +12 -4
- package/src/ws/IoInterface.js +16 -16
- package/src/ws/core/channels/core.ws.chat.js +11 -11
- package/src/ws/core/channels/core.ws.mailer.js +29 -29
- package/src/ws/core/channels/core.ws.stream.js +19 -19
- package/src/ws/core/core.ws.connection.js +8 -8
- package/src/ws/core/core.ws.server.js +6 -5
- package/src/ws/default/channels/default.ws.main.js +10 -10
- package/src/ws/default/default.ws.connection.js +4 -4
- package/src/ws/default/default.ws.server.js +4 -3
- package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
- package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
- /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
- /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
- /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 {
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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()
|
|
135
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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 =
|
|
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 };
|
package/src/cli/baremetal.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
2899
|
-
|
|
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').
|
|
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').
|
|
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) {
|
package/src/cli/cluster.js
CHANGED
|
@@ -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 (
|
|
227
|
+
// Kind cluster initialization (default for development)
|
|
212
228
|
logger.info('Initializing Kind cluster...');
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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 {
|
|
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'
|
|
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
|
|
611
|
-
shellExec('sudo crictl rmp -a -f
|
|
612
|
-
shellExec('sudo systemctl stop etcd
|
|
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
|
|
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(
|
|
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'
|
|
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
|
|
650
|
-
shellExec('sudo ip link del flannel.1
|
|
651
|
-
shellExec('sudo ip link del vxlan.calico
|
|
652
|
-
shellExec('sudo ip link del tunl0
|
|
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
|
|
656
|
-
shellExec('sudo crictl rmi --prune
|
|
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(
|
|
789
|
-
shellExec(
|
|
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;
|