underpost 3.2.8 → 3.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.github/workflows/npmpkg.ci.yml +1 -0
  2. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  3. package/.github/workflows/release.cd.yml +1 -0
  4. package/.vscode/settings.json +10 -5
  5. package/CHANGELOG.md +223 -2
  6. package/CLI-HELP.md +36 -7
  7. package/README.md +38 -9
  8. package/bin/build.js +27 -11
  9. package/bin/deploy.js +20 -21
  10. package/bin/file.js +32 -13
  11. package/bin/index.js +2 -1
  12. package/bin/vs.js +1 -1
  13. package/bump.config.js +26 -0
  14. package/conf.js +20 -4
  15. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  16. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  17. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  18. package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
  19. package/manifests/kind-config-dev.yaml +8 -0
  20. package/manifests/mongodb/pv-pvc.yaml +44 -8
  21. package/manifests/mongodb/statefulset.yaml +55 -68
  22. package/package.json +40 -25
  23. package/scripts/k3s-node-setup.sh +30 -11
  24. package/scripts/nat-iptables.sh +103 -18
  25. package/src/api/core/core.router.js +19 -14
  26. package/src/api/core/core.service.js +5 -5
  27. package/src/api/default/default.router.js +22 -18
  28. package/src/api/default/default.service.js +5 -5
  29. package/src/api/document/document.router.js +28 -23
  30. package/src/api/document/document.service.js +100 -23
  31. package/src/api/file/file.router.js +19 -13
  32. package/src/api/file/file.service.js +9 -7
  33. package/src/api/test/test.router.js +17 -12
  34. package/src/api/types.js +24 -0
  35. package/src/api/user/guest.service.js +5 -4
  36. package/src/api/user/user.router.js +297 -288
  37. package/src/api/user/user.service.js +100 -35
  38. package/src/cli/baremetal.js +20 -11
  39. package/src/cli/cluster.js +243 -55
  40. package/src/cli/db.js +106 -62
  41. package/src/cli/deploy.js +297 -154
  42. package/src/cli/fs.js +19 -3
  43. package/src/cli/index.js +37 -9
  44. package/src/cli/ipfs.js +4 -6
  45. package/src/cli/kubectl.js +4 -1
  46. package/src/cli/lxd.js +217 -135
  47. package/src/cli/release.js +289 -131
  48. package/src/cli/repository.js +91 -34
  49. package/src/cli/run.js +297 -56
  50. package/src/cli/test.js +9 -3
  51. package/src/client/Default.index.js +9 -3
  52. package/src/client/components/core/Auth.js +19 -5
  53. package/src/client/components/core/Docs.js +6 -34
  54. package/src/client/components/core/FileExplorer.js +6 -6
  55. package/src/client/components/core/Modal.js +65 -2
  56. package/src/client/components/core/PanelForm.js +56 -52
  57. package/src/client/components/core/Recover.js +4 -4
  58. package/src/client/components/core/Worker.js +170 -350
  59. package/src/client/services/default/default.management.js +20 -25
  60. package/src/client/services/user/guest.service.js +10 -3
  61. package/src/client/sw/core.sw.js +174 -112
  62. package/src/db/DataBaseProvider.js +120 -20
  63. package/src/db/mongo/MongoBootstrap.js +587 -0
  64. package/src/db/mongo/MongooseDB.js +126 -22
  65. package/src/index.js +1 -1
  66. package/src/runtime/express/Express.js +2 -2
  67. package/src/runtime/wp/Wp.js +8 -5
  68. package/src/server/auth.js +2 -2
  69. package/src/server/client-build-docs.js +1 -1
  70. package/src/server/client-build.js +94 -129
  71. package/src/server/conf.js +20 -65
  72. package/src/server/data-query.js +32 -20
  73. package/src/server/dns.js +22 -0
  74. package/src/server/process.js +180 -19
  75. package/src/server/runtime.js +1 -1
  76. package/src/server/start.js +26 -7
  77. package/src/server/valkey.js +9 -2
  78. package/src/ws/IoInterface.js +16 -16
  79. package/src/ws/core/channels/core.ws.chat.js +11 -11
  80. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  81. package/src/ws/core/channels/core.ws.stream.js +19 -19
  82. package/src/ws/core/core.ws.connection.js +8 -8
  83. package/src/ws/core/core.ws.server.js +6 -5
  84. package/src/ws/default/channels/default.ws.main.js +10 -10
  85. package/src/ws/default/default.ws.connection.js +4 -4
  86. package/src/ws/default/default.ws.server.js +4 -3
  87. package/typedoc.json +10 -1
  88. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  89. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  90. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  91. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  92. /package/src/client/ssr/{pages → views}/Test.js +0 -0
@@ -0,0 +1,587 @@
1
+ /**
2
+ * MongoDB Replica Set Bootstrap Module
3
+ * @module src/db/mongo/MongoBootstrap
4
+ * @namespace MongoBootstrap
5
+ * @description Centralized logic for bootstrapping a MongoDB replica set inside Kubernetes.
6
+ * Provides class-based static methods for initializing MongoDB clusters across all
7
+ * cluster types (Kind, Kubeadm, K3s), managing replica set configuration, and
8
+ * detecting the primary pod.
9
+ */
10
+
11
+ import fs from 'fs-extra';
12
+ import { loggerFactory } from '../../server/logger.js';
13
+ import { shellExec } from '../../server/process.js';
14
+ import {
15
+ MONGODB_DEFAULT_REPLICA_COUNT,
16
+ MONGODB_DEFAULT_REPLICA_SET,
17
+ MONGODB_SERVICE_NAME,
18
+ MONGODB_STATEFULSET_NAME,
19
+ resolveMongoReplicaHosts,
20
+ } from './MongooseDB.js';
21
+
22
+ const logger = loggerFactory(import.meta);
23
+
24
+ /**
25
+ * @typedef {Object} MongoBootstrapOptions
26
+ * @property {string} [namespace='default'] - Kubernetes namespace.
27
+ * @property {number} [replicaCount=3] - Number of replica set members.
28
+ * @property {string} [mongoDbHost=''] - Explicit host list override (comma-separated or empty for StatefulSet defaults).
29
+ * @property {boolean} [pullImage=false] - Whether to pull the mongo image before deploy.
30
+ * @property {boolean} [reset=false] - Whether to clean all persistent data before init.
31
+ * @property {string} [clusterType='kind'] - One of 'kind', 'kubeadm', 'k3s'.
32
+ * @property {string} underpostRoot - Path to the underpost root (manifests location).
33
+ */
34
+
35
+ /**
36
+ * @class MongoBootstrap
37
+ * @memberof MongoBootstrap
38
+ * @description Manages the lifecycle of a MongoDB replica set in Kubernetes.
39
+ * Provides static methods for initializing, configuring, and querying the
40
+ * replica set status. Handles secrets, storage, pod readiness, and mongosh
41
+ * orchestration in an idempotent manner.
42
+ */
43
+ class MongoBootstrap {
44
+ /**
45
+ * Reads a credential file and returns its trimmed contents.
46
+ * @param {string} filePath - Absolute path to the credential file.
47
+ * @returns {string} Trimmed credential value (empty string on error).
48
+ */
49
+ static readCredential(filePath) {
50
+ try {
51
+ return fs.readFileSync(filePath, 'utf8').replace(/\r?\n/g, '').trim();
52
+ } catch {
53
+ logger.warn(`Cannot read credential file: ${filePath}`);
54
+ return '';
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Builds a mongosh script that handles all replica set bootstrapping states:
60
+ * pristine volumes, pre-existing auth, reconfiguration, and idempotent no-ops.
61
+ * @param {object} param0
62
+ * @param {string} param0.replicaSetName - Replica set name (e.g. 'rs0').
63
+ * @param {number} param0.replicaCount - Desired replica count.
64
+ * @param {string} param0.statefulSetName - StatefulSet name (e.g. 'mongodb').
65
+ * @param {string} param0.serviceName - Headless service name.
66
+ * @param {string[]} param0.desiredHosts - Desired host:port entries for members.
67
+ * @param {string} param0.rootUser - Admin username.
68
+ * @param {string} param0.rootPassword - Admin password.
69
+ * @returns {string} A single mongosh-evaluable JavaScript string.
70
+ */
71
+ static buildMongoshInitScript({
72
+ replicaSetName,
73
+ replicaCount,
74
+ statefulSetName,
75
+ serviceName,
76
+ desiredHosts,
77
+ rootUser,
78
+ rootPassword,
79
+ }) {
80
+ const mePort = '27017';
81
+ const defaultShortHosts = Array.from(
82
+ { length: replicaCount },
83
+ (_, i) => `${statefulSetName}-${i}.${serviceName}:${mePort}`,
84
+ );
85
+ const hosts = desiredHosts.length > 0 ? desiredHosts : defaultShortHosts.slice(0, replicaCount);
86
+ const desiredConfig = {
87
+ _id: replicaSetName,
88
+ members: hosts.map((host, index) => ({ _id: index, host })),
89
+ };
90
+
91
+ return [
92
+ `const mePort = "27017";`,
93
+ `const desiredConfig = ${JSON.stringify(desiredConfig)};`,
94
+ `const rootUser = ${JSON.stringify(rootUser)};`,
95
+ `const rootPassword = ${JSON.stringify(rootPassword)};`,
96
+
97
+ // Wait for a writable primary, polling up to 30s
98
+ `const waitPrimary = () => {`,
99
+ ` for (let i = 0; i < 30; i++) {`,
100
+ ` const h = db.hello ? db.hello() : db.isMaster();`,
101
+ ` if (h.isWritablePrimary || h.ismaster) return;`,
102
+ ` sleep(1000);`,
103
+ ` }`,
104
+ ` throw new Error("Timed out waiting for writable primary");`,
105
+ `};`,
106
+
107
+ // Ensure the root user exists (idempotent)
108
+ `const ensureRootUser = () => {`,
109
+ ` if (!rootUser || !rootPassword) return;`,
110
+ ` const adminDb = db.getSiblingDB("admin");`,
111
+ ` try {`,
112
+ ` adminDb.createUser({ user: rootUser, pwd: rootPassword, roles: [{ role: "root", db: "admin" }] });`,
113
+ ` print("SUCCESS_USER_BOOTSTRAPPED");`,
114
+ ` } catch(e) {`,
115
+ ` const s = String(e);`,
116
+ ` if (s.includes("already exists") || s.includes("DuplicateKey")) { print("SUCCESS_USER_EXISTS"); }`,
117
+ ` else if (s.includes("requires authentication") || s.includes("Unauthorized") || s.includes("not authorized")) { print("SUCCESS_USER_GUARDED"); }`,
118
+ ` else throw e;`,
119
+ ` }`,
120
+ `};`,
121
+
122
+ // Authenticate and apply desired replica config
123
+ `const ensureAdminAuth = () => {`,
124
+ ` if (!rootUser || !rootPassword) return true;`,
125
+ ` try {`,
126
+ ` const status = db.runCommand({ connectionStatus: 1 });`,
127
+ ` const users = status && status.authInfo && status.authInfo.authenticatedUsers ? status.authInfo.authenticatedUsers : [];`,
128
+ ` if (users.length > 0) return true;`,
129
+ ` } catch (e) {}`,
130
+ ` const ok = db.getSiblingDB("admin").auth(rootUser, rootPassword);`,
131
+ ` if (ok !== 1 && ok !== true) {`,
132
+ ` print("SUCCESS_USER_BOOTSTRAPPED_NO_RECONFIG");`,
133
+ ` return false;`,
134
+ ` }`,
135
+ ` return true;`,
136
+ `};`,
137
+
138
+ `const reconfigure = () => {`,
139
+ ` if (!ensureAdminAuth()) return false;`,
140
+ ` const cur = rs.conf();`,
141
+ ` const curHosts = cur.members.map(m => m.host).sort().join(",");`,
142
+ ` const nextHosts = desiredConfig.members.map(m => m.host).sort().join(",");`,
143
+ ` if (curHosts !== nextHosts || cur._id !== desiredConfig._id) {`,
144
+ ` rs.reconfig({...desiredConfig, version: (cur.version || 1) + 1}, { force: true });`,
145
+ ` print("SUCCESS_RECONFIGURED");`,
146
+ ` } else {`,
147
+ ` print("SUCCESS_ALREADY_MATCHES");`,
148
+ ` }`,
149
+ ` return true;`,
150
+ `};`,
151
+
152
+ // Determine if replica set is already initialized
153
+ `let initialized = false;`,
154
+ `try {`,
155
+ ` const s = rs.status();`,
156
+ ` if (s && s.ok === 1) initialized = true;`,
157
+ `} catch(e) {`,
158
+ ` const msg = String(e);`,
159
+ ` if (msg.includes("requires authentication") || msg.includes("Unauthorized") || msg.includes("not authorized")) {`,
160
+ ` initialized = true;`,
161
+ ` } else if (!msg.includes("NotYetInitialized") && !msg.includes("no replset config") && !msg.includes("maps to this node")) {`,
162
+ ` throw e;`,
163
+ ` }`,
164
+ `}`,
165
+
166
+ // Not initialized: bootstrap via localhost
167
+ `if (!initialized) {`,
168
+ ` try {`,
169
+ ` rs.initiate({ _id: desiredConfig._id, members: [{ _id: 0, host: "localhost:" + mePort }] });`,
170
+ ` } catch(e) {`,
171
+ ` const msg = String(e);`,
172
+ ` if (!msg.includes("already initialized") && !msg.includes("AlreadyInitialized")) throw e;`,
173
+ ` }`,
174
+ `}`,
175
+
176
+ // Wait for primary, create user, then reconfig to full host list
177
+ `waitPrimary();`,
178
+ `ensureRootUser();`,
179
+ `reconfigure();`,
180
+ `quit(0);`,
181
+ ].join('\n');
182
+ }
183
+
184
+ /**
185
+ * Checks that Kind cluster nodes have the required /data/mongodb mount.
186
+ * @param {string[]} kindNodes - List of Kind node names.
187
+ * @returns {string[]} List of node names missing the mount (empty if all ok).
188
+ */
189
+ static findNodesMissingMongoMount(kindNodes) {
190
+ return kindNodes.filter((node) => {
191
+ const inspect = shellExec(
192
+ `sudo docker inspect ${node} --format '{{range .Mounts}}{{if eq .Destination "/data/mongodb"}}yes{{end}}{{end}}'`,
193
+ { stdout: true, silent: true, silentOnError: true },
194
+ );
195
+ return !inspect.trim().includes('yes');
196
+ });
197
+ }
198
+
199
+ /**
200
+ * Cleans hostPath directories inside Kind node containers.
201
+ * @param {number} [replicaCount=3] - Number of replica ordinal directories.
202
+ */
203
+ static cleanKindMongoHostPaths(replicaCount = 3) {
204
+ const raw = shellExec('kind get nodes', { stdout: true, silent: true, silentOnError: true });
205
+ const nodes = raw.split('\n').map((n) => n.trim()).filter(Boolean);
206
+ if (nodes.length === 0) {
207
+ logger.info('No Kind nodes detected for hostPath cleanup.');
208
+ return;
209
+ }
210
+ const basePath = '/data/mongodb';
211
+ for (const node of nodes) {
212
+ const prepareCmd = Array.from({ length: replicaCount }, (_, i) =>
213
+ `mkdir -p ${basePath}/v${i}; rm -rf ${basePath}/v${i}/*;`,
214
+ ).join(' ');
215
+ shellExec(`sudo docker exec ${node} sh -lc 'mkdir -p ${basePath}; ${prepareCmd}'`, { silentOnError: true });
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Reads MongoDB root credentials from engine-private.
221
+ * @param {string} enginePrivateRoot - Path to the engine-private directory.
222
+ * @returns {{ username: string, password: string }}
223
+ */
224
+ static readMongoCredentials(enginePrivateRoot) {
225
+ return {
226
+ username: MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-username`),
227
+ password: MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-password`),
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Creates or updates Kubernetes secrets required by the MongoDB statefulset.
233
+ * @param {string} namespace - Target namespace.
234
+ * @param {string} enginePrivateRoot - Path to engine-private directory.
235
+ */
236
+ static ensureMongoSecrets(namespace, enginePrivateRoot) {
237
+ const keyfile = MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-keyfile`);
238
+ const { username, password } = MongoBootstrap.readMongoCredentials(enginePrivateRoot);
239
+
240
+ shellExec(
241
+ `sudo kubectl create secret generic mongodb-keyfile` +
242
+ ` --from-literal=mongodb-keyfile="${keyfile.replace(/'/g, "'\\''")}"` +
243
+ ` --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
244
+ );
245
+ shellExec(
246
+ `sudo kubectl create secret generic mongodb-secret` +
247
+ ` --from-literal=username="${username}" --from-literal=password="${password}"` +
248
+ ` --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
249
+ );
250
+ }
251
+
252
+ /**
253
+ * Waits for all MongoDB statefulset pods to reach Running state.
254
+ * @param {string} namespace - Target namespace.
255
+ * @param {number} replicaCount - Expected number of pods.
256
+ * @returns {Promise<number>} Number of pods that failed to become ready (0 = all good).
257
+ */
258
+ static async waitForPods(namespace, replicaCount) {
259
+ const { default: Underpost } = await import('../../index.js');
260
+ const results = await Promise.all(
261
+ Array.from({ length: replicaCount }, async (_, i) => {
262
+ const podName = `${MONGODB_STATEFULSET_NAME}-${i}`;
263
+ const ready = await Underpost.test.statusMonitor(podName, 'Running', 'pods', 1000, 60 * 10);
264
+ return { index: i, ready };
265
+ }),
266
+ );
267
+ const failed = results.filter((r) => !r.ready).map((r) => `${MONGODB_STATEFULSET_NAME}-${r.index}`);
268
+ if (failed.length > 0) {
269
+ logger.error('MongoDB pods did not become ready', { failed });
270
+ }
271
+ return failed.length;
272
+ }
273
+
274
+ /**
275
+ * Full MongoDB replica set initialization.
276
+ *
277
+ * Handles secret creation, PVC/hostPath cleanup, statefulset rollout, pod readiness
278
+ * wait, and idempotent replica set bootstrapping via mongosh.
279
+ *
280
+ * @param {MongoBootstrapOptions} options - Bootstrap configuration.
281
+ * @returns {Promise<void>}
282
+ */
283
+ static async initReplicaSet(options) {
284
+ const {
285
+ namespace = 'default',
286
+ replicaCount = MONGODB_DEFAULT_REPLICA_COUNT,
287
+ mongoDbHost = '',
288
+ pullImage = false,
289
+ reset = false,
290
+ clusterType = 'kind',
291
+ underpostRoot,
292
+ } = options;
293
+
294
+ const enginePrivateRoot = `${process.cwd()}/engine-private`;
295
+ const effectiveReplicaCount = Math.max(Number(replicaCount) || MONGODB_DEFAULT_REPLICA_COUNT, 3);
296
+ const mongoRootUsername = MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-username`);
297
+ const mongoRootPassword = MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-password`);
298
+ const mongoReplicaHosts = resolveMongoReplicaHosts({
299
+ hostList: mongoDbHost,
300
+ replicaCount: effectiveReplicaCount,
301
+ });
302
+ const useExplicitHosts = !!mongoDbHost.trim();
303
+
304
+ // Kind-specific mount checks
305
+ const isKind = clusterType === 'kind' || !clusterType;
306
+ if (isKind) {
307
+ const kindNodesRaw = shellExec('kind get nodes', { stdout: true, silent: true, silentOnError: true });
308
+ const kindNodes = kindNodesRaw.split('\n').map((n) => n.trim()).filter(Boolean);
309
+ if (kindNodes.length > 0) {
310
+ const missingMounts = MongoBootstrap.findNodesMissingMongoMount(kindNodes);
311
+ if (missingMounts.length > 0) {
312
+ throw new Error(
313
+ `Kind cluster is missing required mount '/data/mongodb' on nodes: ${missingMounts.join(', ')}. ` +
314
+ `Run with --reset or manually add the mount to kind-config.yaml.`,
315
+ );
316
+ }
317
+ }
318
+ }
319
+
320
+ // Pull image if requested (cluster-type aware)
321
+ if (pullImage) {
322
+ if (isKind) {
323
+ const tarPath = `/tmp/kind-image-mongo-latest.tar`;
324
+ shellExec('docker pull mongo:latest');
325
+ shellExec(`docker save mongo:latest -o ${tarPath}`);
326
+ const nodes = shellExec('kind get nodes', { stdout: true, silent: true }).trim().split('\n').filter(Boolean);
327
+ for (const node of nodes) {
328
+ shellExec(`cat ${tarPath} | docker exec -i ${node} ctr --namespace=k8s.io images import -`);
329
+ }
330
+ shellExec(`rm -f ${tarPath}`);
331
+ } else {
332
+ const criSock = shellExec('test -S /var/run/crio/crio.sock && echo crio || echo containerd', {
333
+ stdout: true, silent: true,
334
+ }).trim() === 'crio'
335
+ ? 'unix:///var/run/crio/crio.sock'
336
+ : 'unix:///run/containerd/containerd.sock';
337
+ shellExec(`sudo env PATH="$PATH:/usr/local/bin:/usr/bin" crictl --runtime-endpoint ${criSock} pull mongo:latest`);
338
+ }
339
+ }
340
+
341
+ // Secrets
342
+ MongoBootstrap.ensureMongoSecrets(namespace, enginePrivateRoot);
343
+
344
+ // Tear down existing statefulset
345
+ shellExec(`kubectl delete statefulset ${MONGODB_STATEFULSET_NAME} -n ${namespace} --ignore-not-found`);
346
+ shellExec(`kubectl wait --for=delete pod -l app=mongodb -n ${namespace} --timeout=180s`, { silentOnError: true });
347
+
348
+ // Clean data if reset or kind
349
+ if (reset || isKind) {
350
+ shellExec(`kubectl delete pvc -l app=mongodb -n ${namespace} --ignore-not-found`);
351
+ shellExec(`kubectl delete pvc mongodb-pvc -n ${namespace} --ignore-not-found`);
352
+ shellExec(`kubectl delete pv -l app=mongodb --ignore-not-found`);
353
+ shellExec(`kubectl delete pv mongodb-pv --ignore-not-found`);
354
+
355
+ if (isKind) {
356
+ shellExec('sudo mkdir -p /data/mongodb && sudo rm -rf /data/mongodb/*');
357
+ for (let i = 0; i < effectiveReplicaCount; i++) {
358
+ shellExec(`sudo mkdir -p /data/mongodb/v${i}`);
359
+ }
360
+ MongoBootstrap.cleanKindMongoHostPaths(effectiveReplicaCount);
361
+ }
362
+ }
363
+
364
+ // Apply manifests
365
+ shellExec(`kubectl apply -f ${underpostRoot}/manifests/mongodb/storage-class.yaml -n ${namespace}`);
366
+ shellExec(`kubectl apply -k ${underpostRoot}/manifests/mongodb -n ${namespace}`);
367
+ shellExec(`kubectl scale statefulset/${MONGODB_STATEFULSET_NAME} --replicas=${effectiveReplicaCount} -n ${namespace}`);
368
+
369
+ // Wait for all pods
370
+ const failedCount = await MongoBootstrap.waitForPods(namespace, effectiveReplicaCount);
371
+ if (failedCount > 0) {
372
+ throw new Error(
373
+ `MongoDB replica pods did not reach Running state in time. ` +
374
+ `Ensure podManagementPolicy is set to OrderedReady in statefulset.yaml.`,
375
+ );
376
+ }
377
+
378
+ // Build the bootstrap script
379
+ const defaultHosts = Array.from(
380
+ { length: effectiveReplicaCount },
381
+ (_, i) => `${MONGODB_STATEFULSET_NAME}-${i}.${MONGODB_SERVICE_NAME}:27017`,
382
+ );
383
+ const desiredHosts = useExplicitHosts ? mongoReplicaHosts : defaultHosts;
384
+
385
+ const initScript = MongoBootstrap.buildMongoshInitScript({
386
+ replicaSetName: MONGODB_DEFAULT_REPLICA_SET,
387
+ replicaCount: effectiveReplicaCount,
388
+ statefulSetName: MONGODB_STATEFULSET_NAME,
389
+ serviceName: MONGODB_SERVICE_NAME,
390
+ desiredHosts,
391
+ rootUser: mongoRootUsername,
392
+ rootPassword: mongoRootPassword,
393
+ });
394
+ const inlineInitScript = initScript.replace(/\r?\n/g, ' ');
395
+
396
+ // Execute init with retry
397
+ const execMongoCmd = (auth = false) => {
398
+ const pod0 = `${MONGODB_STATEFULSET_NAME}-0`;
399
+ if (auth && mongoRootUsername && mongoRootPassword) {
400
+ return shellExec(
401
+ `sudo kubectl exec -i ${pod0} -n ${namespace} -- bash -lc ` +
402
+ `'mongosh --quiet --host localhost --authenticationDatabase admin ` +
403
+ `-u ${JSON.stringify(mongoRootUsername)} -p ${JSON.stringify(mongoRootPassword)} ` +
404
+ `--eval ${JSON.stringify(inlineInitScript)}'`,
405
+ { silentOnError: true },
406
+ );
407
+ }
408
+ return shellExec(
409
+ `sudo kubectl exec -i ${pod0} -n ${namespace} -- bash -lc ` +
410
+ `'mongosh --quiet --host localhost --eval ${JSON.stringify(inlineInitScript)}'`,
411
+ { silentOnError: true },
412
+ );
413
+ };
414
+
415
+ let success = false;
416
+ const maxAttempts = 5;
417
+
418
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
419
+ const noAuthResult = execMongoCmd(false);
420
+ if (noAuthResult.code === 0) {
421
+ if (mongoRootUsername && mongoRootPassword) {
422
+ const authResult = execMongoCmd(true);
423
+ if (authResult.code === 0) {
424
+ success = true;
425
+ break;
426
+ }
427
+ logger.warn('No-auth bootstrap succeeded but auth verify failed, retrying...', { attempt });
428
+ } else {
429
+ success = true;
430
+ break;
431
+ }
432
+ } else {
433
+ const authResult = execMongoCmd(true);
434
+ if (authResult.code === 0) {
435
+ success = true;
436
+ break;
437
+ }
438
+ logger.warn('Both bootstrap paths failed, retrying...', { attempt });
439
+ }
440
+
441
+ if (attempt < maxAttempts) {
442
+ await new Promise((r) => setTimeout(r, 3000));
443
+ }
444
+ }
445
+
446
+ if (!success) {
447
+ throw new Error(
448
+ 'MongoDB replica set initialization failed after max retries. ' +
449
+ 'Check pod logs for mongodb-0 to diagnose.',
450
+ );
451
+ }
452
+
453
+ logger.info('MongoDB replica set initialized successfully.');
454
+ }
455
+
456
+ /**
457
+ * Performs a targeted, hard cleanup of only MongoDB-related Kubernetes resources
458
+ * and artifacts (StatefulSet, PVCs/PVs, Secrets, ConfigMaps, caches, YAML manifests, and
459
+ * hostPath data) without restarting the whole node or touching unrelated cluster resources.
460
+ * @param {object} [options] - Configuration options for the MongoDB reset.
461
+ * @param {string} [options.namespace='default'] - Kubernetes namespace.
462
+ * @param {string} [options.clusterType='kind'] - The type of cluster: 'kind', 'kubeadm', or 'k3s'.
463
+ * @param {string} [options.underpostRoot] - The root path of the underpost project (manifests location).
464
+ * @memberof MongoBootstrap
465
+ */
466
+ static async reset(options = { namespace: 'default', clusterType: 'kind', underpostRoot: '.' }) {
467
+ const { namespace = 'default', clusterType = 'kind', underpostRoot } = options;
468
+ const isKind = clusterType === 'kind' || !clusterType;
469
+ logger.info(`Starting MongoDB-only reset in namespace '${namespace}' (cluster type: ${clusterType})...`);
470
+
471
+ try {
472
+ // Phase 1: Delete MongoDB StatefulSet and Deployment (both current and legacy mongodb-4.4)
473
+ logger.info('Phase 1/6: Deleting MongoDB workloads...');
474
+ shellExec(`kubectl delete statefulset mongodb -n ${namespace} --ignore-not-found --wait=false`);
475
+ shellExec(`kubectl delete deployment mongodb-deployment -n ${namespace} --ignore-not-found --wait=false`);
476
+ // Delete mongo-express if present
477
+ shellExec(`kubectl delete deployment mongo-express -n ${namespace} --ignore-not-found --wait=false`);
478
+ shellExec(`kubectl delete service mongo-express-service -n ${namespace} --ignore-not-found`);
479
+
480
+ // Phase 2: Delete MongoDB headless service (will be recreated on redeploy)
481
+ logger.info('Phase 2/6: Deleting MongoDB Services and ConfigMaps...');
482
+ shellExec(`kubectl delete service mongodb-service -n ${namespace} --ignore-not-found`);
483
+
484
+ // Phase 3: Delete MongoDB Secrets
485
+ logger.info('Phase 3/6: Deleting MongoDB Secrets...');
486
+ shellExec(`kubectl delete secret mongodb-secret -n ${namespace} --ignore-not-found`);
487
+ shellExec(`kubectl delete secret mongodb-keyfile -n ${namespace} --ignore-not-found`);
488
+
489
+ // Phase 4: Delete MongoDB PVCs and PVs (both current and legacy mongodb-4.4)
490
+ logger.info('Phase 4/6: Deleting MongoDB PersistentVolumeClaims and PersistentVolumes...');
491
+ // Delete PVCs from volumeClaimTemplates
492
+ for (let i = 0; i < 10; i++) {
493
+ shellExec(`kubectl delete pvc mongodb-storage-mongodb-${i} -n ${namespace} --ignore-not-found`);
494
+ }
495
+ shellExec(`kubectl delete pvc mongodb-pvc -n ${namespace} --ignore-not-found`);
496
+ shellExec(`kubectl delete pv mongodb-pv-0 --ignore-not-found`);
497
+ shellExec(`kubectl delete pv mongodb-pv-1 --ignore-not-found`);
498
+ shellExec(`kubectl delete pv mongodb-pv-2 --ignore-not-found`);
499
+ shellExec(`kubectl delete pv mongodb-pv --ignore-not-found`);
500
+ // Also catch any remaining PVs with the app=mongodb label
501
+ shellExec(`kubectl delete pv -l app=mongodb --ignore-not-found`);
502
+
503
+ // Delete MongoDB StorageClass
504
+ shellExec(`kubectl delete storageclass mongodb-storage-class --ignore-not-found`);
505
+
506
+ // Phase 5: Clean up hostPath data on host and inside Kind node containers
507
+ logger.info('Phase 5/6: Cleaning up MongoDB hostPath data...');
508
+ // Host-level cleanup
509
+ shellExec(`sudo rm -rf /data/mongodb`);
510
+ shellExec(`sudo mkdir -p /data/mongodb`);
511
+ for (let i = 0; i < 3; i++) {
512
+ shellExec(`sudo mkdir -p /data/mongodb/v${i}`);
513
+ }
514
+
515
+ // Kind node-internal cleanup
516
+ if (isKind) {
517
+ MongoBootstrap.cleanKindMongoHostPaths(3);
518
+ }
519
+
520
+ // Phase 6: Wait for pod deletion to complete
521
+ logger.info('Phase 6/6: Waiting for MongoDB pods to terminate...');
522
+ shellExec(`kubectl wait --for=delete pod -l app=mongodb -n ${namespace} --timeout=120s`, { silentOnError: true });
523
+
524
+ logger.info('MongoDB reset completed successfully. Ready for fresh MongoDB deployment.');
525
+ } catch (error) {
526
+ logger.error(`Error during MongoDB reset: ${error.message}`);
527
+ console.error(error);
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Gets the primary MongoDB pod name from replica set status.
533
+ * @param {object} [options] - Query options.
534
+ * @param {string} [options.namespace='default'] - Kubernetes namespace.
535
+ * @param {string} [options.podName='mongodb-0'] - Any MongoDB pod to query.
536
+ * @param {string} [options.username] - MongoDB admin username.
537
+ * @param {string} [options.password] - MongoDB admin password.
538
+ * @param {string} [options.authDatabase='admin'] - Auth database.
539
+ * @returns {string|null} Primary pod name, or null if not found.
540
+ */
541
+ static getPrimaryPodName(options = {}) {
542
+ const { namespace = 'default', podName = 'mongodb-0', username, password, authDatabase = 'admin' } = options;
543
+
544
+ const readTrimmedFile = (filePath) => {
545
+ try {
546
+ if (fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf8').trim();
547
+ } catch { /* ignore */ }
548
+ return '';
549
+ };
550
+
551
+ const mongoUser = username || process.env.MONGODB_USERNAME || process.env.DB_USER ||
552
+ readTrimmedFile('./engine-private/mongodb-username');
553
+ const mongoPass = password || process.env.MONGODB_PASSWORD || process.env.DB_PASSWORD ||
554
+ readTrimmedFile('./engine-private/mongodb-password');
555
+
556
+ const evalExpr = 'rs.status().members.filter(m=>m.stateStr=="PRIMARY").map(m=>m.name)';
557
+
558
+ let output = shellExec(
559
+ `sudo kubectl exec -n ${namespace} -i ${podName} -- mongosh --quiet --eval '${evalExpr}'`,
560
+ { stdout: true, silent: true, silentOnError: true },
561
+ );
562
+
563
+ if ((!output || output.trim() === '') && mongoUser && mongoPass) {
564
+ output = shellExec(
565
+ `sudo kubectl exec -n ${namespace} -i ${podName} -- mongosh --quiet ` +
566
+ `--authenticationDatabase ${JSON.stringify(authDatabase)} ` +
567
+ `-u ${JSON.stringify(mongoUser)} -p ${JSON.stringify(mongoPass)} --eval '${evalExpr}'`,
568
+ { stdout: true, silent: true, silentOnError: true },
569
+ );
570
+ }
571
+
572
+ if (!output || output.trim() === '') {
573
+ logger.warn('No MongoDB primary pod found.');
574
+ return null;
575
+ }
576
+
577
+ const match = output.match(/['"]([^'"]+)['"]/);
578
+ if (match && match[1]) {
579
+ const primary = match[1].split(':')[0].split('.')[0];
580
+ logger.info('Found MongoDB primary pod', { primary });
581
+ return primary;
582
+ }
583
+ return null;
584
+ }
585
+ }
586
+
587
+ export { MongoBootstrap };