underpost 3.2.9 → 3.2.11

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 (104) 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/extensions.json +9 -9
  5. package/.vscode/settings.json +20 -4
  6. package/CHANGELOG.md +195 -1
  7. package/CLI-HELP.md +92 -23
  8. package/README.md +38 -9
  9. package/bin/build.js +27 -7
  10. package/bin/build.template.js +187 -0
  11. package/bin/deploy.js +12 -2
  12. package/bin/index.js +2 -1
  13. package/bump.config.js +26 -0
  14. package/conf.js +20 -7
  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/lxd/lxd-admin-profile.yaml +12 -3
  21. package/manifests/mongodb/pv-pvc.yaml +44 -8
  22. package/manifests/mongodb/statefulset.yaml +55 -68
  23. package/manifests/mongodb-4.4/headless-service.yaml +10 -0
  24. package/manifests/mongodb-4.4/kustomization.yaml +3 -1
  25. package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
  26. package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
  27. package/manifests/mongodb-4.4/statefulset.yaml +79 -0
  28. package/manifests/mongodb-4.4/storage-class.yaml +9 -0
  29. package/manifests/valkey/statefulset.yaml +1 -1
  30. package/manifests/valkey/valkey-nodeport.yaml +17 -0
  31. package/package.json +27 -12
  32. package/scripts/ipxe-setup.sh +52 -49
  33. package/scripts/k3s-node-setup.sh +81 -46
  34. package/scripts/lxd-vm-setup.sh +193 -8
  35. package/scripts/maas-nat-firewalld.sh +145 -0
  36. package/src/api/core/core.router.js +19 -14
  37. package/src/api/core/core.service.js +5 -5
  38. package/src/api/default/default.router.js +22 -18
  39. package/src/api/default/default.service.js +5 -5
  40. package/src/api/document/document.router.js +28 -23
  41. package/src/api/document/document.service.js +100 -23
  42. package/src/api/file/file.router.js +19 -13
  43. package/src/api/file/file.service.js +9 -7
  44. package/src/api/test/test.router.js +17 -12
  45. package/src/api/types.js +24 -0
  46. package/src/api/user/guest.service.js +5 -4
  47. package/src/api/user/user.router.js +297 -288
  48. package/src/api/user/user.service.js +100 -35
  49. package/src/cli/baremetal.js +132 -101
  50. package/src/cli/cluster.js +700 -232
  51. package/src/cli/db.js +59 -60
  52. package/src/cli/deploy.js +216 -137
  53. package/src/cli/fs.js +13 -3
  54. package/src/cli/index.js +80 -15
  55. package/src/cli/ipfs.js +4 -6
  56. package/src/cli/kubectl.js +4 -1
  57. package/src/cli/lxd.js +1099 -223
  58. package/src/cli/monitor.js +9 -3
  59. package/src/cli/release.js +334 -140
  60. package/src/cli/repository.js +68 -23
  61. package/src/cli/run.js +191 -47
  62. package/src/cli/secrets.js +11 -2
  63. package/src/cli/test.js +9 -3
  64. package/src/client/Default.index.js +9 -3
  65. package/src/client/components/core/Auth.js +5 -0
  66. package/src/client/components/core/ClientEvents.js +76 -0
  67. package/src/client/components/core/EventBus.js +4 -0
  68. package/src/client/components/core/Modal.js +82 -41
  69. package/src/client/components/core/PanelForm.js +56 -52
  70. package/src/client/components/core/Worker.js +162 -363
  71. package/src/client/sw/core.sw.js +174 -112
  72. package/src/db/DataBaseProvider.js +115 -15
  73. package/src/db/mariadb/MariaDB.js +2 -1
  74. package/src/db/mongo/MongoBootstrap.js +657 -0
  75. package/src/db/mongo/MongooseDB.js +129 -21
  76. package/src/index.js +1 -1
  77. package/src/runtime/express/Express.js +2 -2
  78. package/src/runtime/wp/Wp.js +8 -5
  79. package/src/server/auth.js +2 -2
  80. package/src/server/client-build-docs.js +1 -1
  81. package/src/server/client-build.js +94 -129
  82. package/src/server/conf.js +81 -79
  83. package/src/server/process.js +180 -19
  84. package/src/server/proxy.js +9 -2
  85. package/src/server/runtime.js +1 -1
  86. package/src/server/start.js +16 -4
  87. package/src/server/valkey.js +2 -0
  88. package/src/ws/IoInterface.js +16 -16
  89. package/src/ws/core/channels/core.ws.chat.js +11 -11
  90. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  91. package/src/ws/core/channels/core.ws.stream.js +19 -19
  92. package/src/ws/core/core.ws.connection.js +8 -8
  93. package/src/ws/core/core.ws.server.js +6 -5
  94. package/src/ws/default/channels/default.ws.main.js +10 -10
  95. package/src/ws/default/default.ws.connection.js +4 -4
  96. package/src/ws/default/default.ws.server.js +4 -3
  97. package/bin/file.js +0 -202
  98. package/bin/vs.js +0 -74
  99. package/bin/zed.js +0 -84
  100. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  101. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  102. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  103. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  104. /package/src/client/ssr/{pages → views}/Test.js +0 -0
@@ -0,0 +1,657 @@
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} [hostList=''] - 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
206
+ .split('\n')
207
+ .map((n) => n.trim())
208
+ .filter(Boolean);
209
+ if (nodes.length === 0) {
210
+ logger.info('No Kind nodes detected for hostPath cleanup.');
211
+ return;
212
+ }
213
+ const basePath = '/data/mongodb';
214
+ for (const node of nodes) {
215
+ const prepareCmd = Array.from(
216
+ { length: replicaCount },
217
+ (_, i) => `mkdir -p ${basePath}/v${i}; rm -rf ${basePath}/v${i}/*;`,
218
+ ).join(' ');
219
+ shellExec(`sudo docker exec ${node} sh -lc 'mkdir -p ${basePath}; ${prepareCmd}'`, { silentOnError: true });
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Cleans MongoDB data subdirectories inside each Kind node via docker exec.
225
+ *
226
+ * nsenter-based remounting is unreliable here: inside the Kind node's mount namespace
227
+ * /proc/1 refers to the Kind node's own PID 1 (not the host init), so any bind-mount
228
+ * source built from /proc/1/root is circular and still resolves through the stale bind.
229
+ *
230
+ * Using docker exec is correct: it operates through the same namespace view that kubelet
231
+ * uses when binding hostPath PVs into pods, so cleaning here guarantees the pod sees
232
+ * an empty /data/db regardless of bind-mount staleness.
233
+ *
234
+ * @param {string[]} kindNodes - List of Kind node container names.
235
+ * @param {string} [basePath='/data/mongodb'] - The base path containing v0/v1/v2 subdirs.
236
+ */
237
+ static remountKindMongoVolume(kindNodes, basePath = '/data/mongodb') {
238
+ for (const node of kindNodes) {
239
+ logger.info(`Cleaning MongoDB data dirs inside Kind node '${node}'...`);
240
+ for (let i = 0; i < 3; i++) {
241
+ const dir = `${basePath}/v${i}`;
242
+ // Ensure directory exists, wipe all contents (including hidden files), set open permissions
243
+ // so the pod's initContainer chown can run without issues.
244
+ shellExec(
245
+ `sudo docker exec ${node} sh -c 'mkdir -p ${dir} && find ${dir} -mindepth 1 -delete && chmod 755 ${dir}'`,
246
+ { silentOnError: true },
247
+ );
248
+ logger.info(`Cleaned ${dir} in Kind node '${node}'`);
249
+ }
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Reads MongoDB root credentials from engine-private.
255
+ * @param {string} enginePrivateRoot - Path to the engine-private directory.
256
+ * @returns {{ username: string, password: string }}
257
+ */
258
+ static readMongoCredentials(enginePrivateRoot) {
259
+ return {
260
+ username: MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-username`),
261
+ password: MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-password`),
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Creates or updates Kubernetes secrets required by the MongoDB statefulset.
267
+ * @param {string} namespace - Target namespace.
268
+ * @param {string} enginePrivateRoot - Path to engine-private directory.
269
+ */
270
+ static ensureMongoSecrets(namespace, enginePrivateRoot) {
271
+ const keyfile = MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-keyfile`);
272
+ const { username, password } = MongoBootstrap.readMongoCredentials(enginePrivateRoot);
273
+
274
+ shellExec(
275
+ `sudo kubectl create secret generic mongodb-keyfile` +
276
+ ` --from-literal=mongodb-keyfile="${keyfile.replace(/'/g, "'\\''")}"` +
277
+ ` --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
278
+ );
279
+ shellExec(
280
+ `sudo kubectl create secret generic mongodb-secret` +
281
+ ` --from-literal=username="${username}" --from-literal=password="${password}"` +
282
+ ` --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
283
+ );
284
+ }
285
+
286
+ /**
287
+ * Waits for all MongoDB statefulset pods to reach Running state.
288
+ * @param {string} namespace - Target namespace.
289
+ * @param {number} replicaCount - Expected number of pods.
290
+ * @returns {Promise<number>} Number of pods that failed to become ready (0 = all good).
291
+ */
292
+ static async waitForPods(namespace, replicaCount) {
293
+ const { default: Underpost } = await import('../../index.js');
294
+ const results = await Promise.all(
295
+ Array.from({ length: replicaCount }, async (_, i) => {
296
+ const podName = `${MONGODB_STATEFULSET_NAME}-${i}`;
297
+ const ready = await Underpost.test.statusMonitor(podName, 'Running', 'pods', 1000, 60 * 10);
298
+ return { index: i, ready };
299
+ }),
300
+ );
301
+ const failed = results.filter((r) => !r.ready).map((r) => `${MONGODB_STATEFULSET_NAME}-${r.index}`);
302
+ if (failed.length > 0) {
303
+ logger.error('MongoDB pods did not become ready', { failed });
304
+ }
305
+ return failed.length;
306
+ }
307
+
308
+ /**
309
+ * Full MongoDB replica set initialization.
310
+ *
311
+ * Handles secret creation, PVC/hostPath cleanup, statefulset rollout, pod readiness
312
+ * wait, and idempotent replica set bootstrapping via mongosh.
313
+ *
314
+ * @param {MongoBootstrapOptions} options - Bootstrap configuration.
315
+ * @returns {Promise<void>}
316
+ */
317
+ static async initReplicaSet(options) {
318
+ const {
319
+ namespace = 'default',
320
+ replicaCount = MONGODB_DEFAULT_REPLICA_COUNT,
321
+ hostList = '',
322
+ pullImage = false,
323
+ reset = false,
324
+ clusterType = 'kind',
325
+ underpostRoot,
326
+ } = options;
327
+
328
+ const enginePrivateRoot = `${process.cwd()}/engine-private`;
329
+ const effectiveReplicaCount = Math.max(Number(replicaCount) || MONGODB_DEFAULT_REPLICA_COUNT, 3);
330
+ const mongoRootUsername = MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-username`);
331
+ const mongoRootPassword = MongoBootstrap.readCredential(`${enginePrivateRoot}/mongodb-password`);
332
+ const mongoReplicaHosts = resolveMongoReplicaHosts({
333
+ hostList,
334
+ replicaCount: effectiveReplicaCount,
335
+ });
336
+ const useExplicitHosts = !!hostList.trim();
337
+
338
+ // Kind-specific mount checks
339
+ const isKind = clusterType === 'kind' || !clusterType;
340
+ let kindNodes = [];
341
+ if (isKind) {
342
+ const kindNodesRaw = shellExec('kind get nodes', { stdout: true, silent: true, silentOnError: true });
343
+ kindNodes = kindNodesRaw
344
+ .split('\n')
345
+ .map((n) => n.trim())
346
+ .filter(Boolean);
347
+ if (kindNodes.length > 0) {
348
+ const missingMounts = MongoBootstrap.findNodesMissingMongoMount(kindNodes);
349
+ if (missingMounts.length > 0) {
350
+ throw new Error(
351
+ `Kind cluster is missing required mount '/data/mongodb' on nodes: ${missingMounts.join(', ')}. ` +
352
+ `Run with --reset or manually add the mount to kind-config.yaml.`,
353
+ );
354
+ }
355
+ }
356
+ }
357
+
358
+ // Pull image if requested (cluster-type aware)
359
+ if (pullImage) {
360
+ if (isKind) {
361
+ const tarPath = `/tmp/kind-image-mongo-latest.tar`;
362
+ shellExec('docker pull mongo:latest');
363
+ shellExec(`docker save mongo:latest -o ${tarPath}`);
364
+ const nodes = shellExec('kind get nodes', { stdout: true, silent: true }).trim().split('\n').filter(Boolean);
365
+ for (const node of nodes) {
366
+ shellExec(`cat ${tarPath} | docker exec -i ${node} ctr --namespace=k8s.io images import -`);
367
+ }
368
+ shellExec(`rm -f ${tarPath}`);
369
+ } else {
370
+ const criSock =
371
+ shellExec('test -S /var/run/crio/crio.sock && echo crio || echo containerd', {
372
+ stdout: true,
373
+ silent: true,
374
+ }).trim() === 'crio'
375
+ ? 'unix:///var/run/crio/crio.sock'
376
+ : 'unix:///run/containerd/containerd.sock';
377
+ shellExec(
378
+ `sudo env PATH="$PATH:/usr/local/bin:/usr/bin" crictl --runtime-endpoint ${criSock} pull mongo:latest`,
379
+ );
380
+ }
381
+ }
382
+
383
+ // Secrets
384
+ MongoBootstrap.ensureMongoSecrets(namespace, enginePrivateRoot);
385
+
386
+ // Tear down existing statefulset
387
+ shellExec(`kubectl delete statefulset ${MONGODB_STATEFULSET_NAME} -n ${namespace} --ignore-not-found`);
388
+ shellExec(`kubectl wait --for=delete pod -l app=mongodb -n ${namespace} --timeout=180s`, { silentOnError: true });
389
+
390
+ // Clean data if reset or kind
391
+ if (reset || isKind) {
392
+ shellExec(`kubectl delete pvc -l app=mongodb -n ${namespace} --ignore-not-found`);
393
+ shellExec(`kubectl delete pvc mongodb-pvc -n ${namespace} --ignore-not-found`);
394
+ shellExec(`kubectl delete pv -l app=mongodb --ignore-not-found`);
395
+ shellExec(`kubectl delete pv mongodb-pv --ignore-not-found`);
396
+
397
+ if (isKind) {
398
+ shellExec('sudo mkdir -p /data/mongodb');
399
+ for (let i = 0; i < effectiveReplicaCount; i++) {
400
+ shellExec(`sudo rm -rf /data/mongodb/v${i}`);
401
+ shellExec(`sudo mkdir -p /data/mongodb/v${i}`);
402
+ }
403
+ // Fix any stale bind mounts caused by prior deletion of /data/mongodb on the host
404
+ MongoBootstrap.remountKindMongoVolume(kindNodes);
405
+ }
406
+ }
407
+
408
+ // Apply manifests
409
+ shellExec(`kubectl apply -f ${underpostRoot}/manifests/mongodb/storage-class.yaml -n ${namespace}`);
410
+ shellExec(`kubectl apply -k ${underpostRoot}/manifests/mongodb -n ${namespace}`);
411
+ shellExec(
412
+ `kubectl scale statefulset/${MONGODB_STATEFULSET_NAME} --replicas=${effectiveReplicaCount} -n ${namespace}`,
413
+ );
414
+
415
+ // Wait for all pods
416
+ const failedCount = await MongoBootstrap.waitForPods(namespace, effectiveReplicaCount);
417
+ if (failedCount > 0) {
418
+ throw new Error(
419
+ `MongoDB replica pods did not reach Running state in time. ` +
420
+ `Ensure podManagementPolicy is set to OrderedReady in statefulset.yaml.`,
421
+ );
422
+ }
423
+
424
+ // Build the bootstrap script
425
+ const defaultHosts = Array.from(
426
+ { length: effectiveReplicaCount },
427
+ (_, i) => `${MONGODB_STATEFULSET_NAME}-${i}.${MONGODB_SERVICE_NAME}:27017`,
428
+ );
429
+ const desiredHosts = useExplicitHosts ? mongoReplicaHosts : defaultHosts;
430
+
431
+ const initScript = MongoBootstrap.buildMongoshInitScript({
432
+ replicaSetName: MONGODB_DEFAULT_REPLICA_SET,
433
+ replicaCount: effectiveReplicaCount,
434
+ statefulSetName: MONGODB_STATEFULSET_NAME,
435
+ serviceName: MONGODB_SERVICE_NAME,
436
+ desiredHosts,
437
+ rootUser: mongoRootUsername,
438
+ rootPassword: mongoRootPassword,
439
+ });
440
+ const inlineInitScript = initScript.replace(/\r?\n/g, ' ');
441
+
442
+ // Execute init with retry
443
+ const execMongoCmd = (auth = false) => {
444
+ const pod0 = `${MONGODB_STATEFULSET_NAME}-0`;
445
+ if (auth && mongoRootUsername && mongoRootPassword) {
446
+ return shellExec(
447
+ `sudo kubectl exec -i ${pod0} -n ${namespace} -- bash -lc ` +
448
+ `'mongosh --quiet --host localhost --authenticationDatabase admin ` +
449
+ `-u ${JSON.stringify(mongoRootUsername)} -p ${JSON.stringify(mongoRootPassword)} ` +
450
+ `--eval ${JSON.stringify(inlineInitScript)}'`,
451
+ { silentOnError: true },
452
+ );
453
+ }
454
+ return shellExec(
455
+ `sudo kubectl exec -i ${pod0} -n ${namespace} -- bash -lc ` +
456
+ `'mongosh --quiet --host localhost --eval ${JSON.stringify(inlineInitScript)}'`,
457
+ { silentOnError: true },
458
+ );
459
+ };
460
+
461
+ let success = false;
462
+ const maxAttempts = 5;
463
+
464
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
465
+ const noAuthResult = execMongoCmd(false);
466
+ if (noAuthResult.code === 0) {
467
+ if (mongoRootUsername && mongoRootPassword) {
468
+ const authResult = execMongoCmd(true);
469
+ if (authResult.code === 0) {
470
+ success = true;
471
+ break;
472
+ }
473
+ logger.warn('No-auth bootstrap succeeded but auth verify failed, retrying...', { attempt });
474
+ } else {
475
+ success = true;
476
+ break;
477
+ }
478
+ } else {
479
+ const authResult = execMongoCmd(true);
480
+ if (authResult.code === 0) {
481
+ success = true;
482
+ break;
483
+ }
484
+ logger.warn('Both bootstrap paths failed, retrying...', { attempt });
485
+ }
486
+
487
+ if (attempt < maxAttempts) {
488
+ await new Promise((r) => setTimeout(r, 3000));
489
+ }
490
+ }
491
+
492
+ if (!success) {
493
+ throw new Error(
494
+ 'MongoDB replica set initialization failed after max retries. ' + 'Check pod logs for mongodb-0 to diagnose.',
495
+ );
496
+ }
497
+
498
+ logger.info('MongoDB replica set initialized successfully.');
499
+ }
500
+
501
+ /**
502
+ * Performs a targeted, hard cleanup of only MongoDB-related Kubernetes resources
503
+ * and artifacts (StatefulSet, PVCs/PVs, Secrets, ConfigMaps, caches, YAML manifests, and
504
+ * hostPath data) without restarting the whole node or touching unrelated cluster resources.
505
+ * @param {object} [options] - Configuration options for the MongoDB reset.
506
+ * @param {string} [options.namespace='default'] - Kubernetes namespace.
507
+ * @param {string} [options.clusterType='kind'] - The type of cluster: 'kind', 'kubeadm', or 'k3s'.
508
+ * @param {string} [options.underpostRoot] - The root path of the underpost project (manifests location).
509
+ * @memberof MongoBootstrap
510
+ */
511
+ static async reset(options = { namespace: 'default', clusterType: 'kind', underpostRoot: '.' }) {
512
+ const { namespace = 'default', clusterType = 'kind', underpostRoot } = options;
513
+ const isKind = clusterType === 'kind' || !clusterType;
514
+ logger.info(`Starting MongoDB-only reset in namespace '${namespace}' (cluster type: ${clusterType})...`);
515
+
516
+ try {
517
+ // Phase 1: Delete MongoDB StatefulSet and Deployment (both current and legacy mongodb-4.4)
518
+ logger.info('Phase 1/6: Deleting MongoDB workloads...');
519
+ shellExec(`kubectl delete statefulset mongodb -n ${namespace} --ignore-not-found --wait=false`);
520
+ shellExec(`kubectl delete deployment mongodb-deployment -n ${namespace} --ignore-not-found --wait=false`);
521
+
522
+ // Phase 2: Delete MongoDB headless service (will be recreated on redeploy)
523
+ logger.info('Phase 2/6: Deleting MongoDB Services and ConfigMaps...');
524
+ shellExec(`kubectl delete service mongodb-service -n ${namespace} --ignore-not-found`);
525
+
526
+ // Phase 3: Delete MongoDB Secrets
527
+ logger.info('Phase 3/6: Deleting MongoDB Secrets...');
528
+ shellExec(`kubectl delete secret mongodb-secret -n ${namespace} --ignore-not-found`);
529
+ shellExec(`kubectl delete secret mongodb-keyfile -n ${namespace} --ignore-not-found`);
530
+
531
+ // Phase 4: Delete MongoDB PVCs and PVs (both current and legacy mongodb-4.4)
532
+ logger.info('Phase 4/6: Deleting MongoDB PersistentVolumeClaims and PersistentVolumes...');
533
+ // Delete PVCs from volumeClaimTemplates
534
+ for (let i = 0; i < 10; i++) {
535
+ shellExec(`kubectl delete pvc mongodb-storage-mongodb-${i} -n ${namespace} --ignore-not-found`);
536
+ }
537
+ shellExec(`kubectl delete pvc mongodb-pvc -n ${namespace} --ignore-not-found`);
538
+ shellExec(`kubectl delete pv mongodb-pv-0 --ignore-not-found`);
539
+ shellExec(`kubectl delete pv mongodb-pv-1 --ignore-not-found`);
540
+ shellExec(`kubectl delete pv mongodb-pv-2 --ignore-not-found`);
541
+ shellExec(`kubectl delete pv mongodb-pv --ignore-not-found`);
542
+ // Also catch any remaining PVs with the app=mongodb label
543
+ shellExec(`kubectl delete pv -l app=mongodb --ignore-not-found`);
544
+ // Wait for PVs to be fully deleted to avoid "object modified" conflict on re-apply
545
+ shellExec(`kubectl wait --for=delete pv mongodb-pv-0 mongodb-pv-1 mongodb-pv-2 mongodb-pv --timeout=60s`, {
546
+ silentOnError: true,
547
+ });
548
+
549
+ // Delete MongoDB StorageClass
550
+ shellExec(`kubectl delete storageclass mongodb-storage-class --ignore-not-found`);
551
+
552
+ // Phase 5: Clean up hostPath data
553
+ // IMPORTANT: Do NOT remove /data/mongodb itself — it is bind-mounted into Kind node
554
+ // containers by inode. Removing it makes the bind mount stale; only clear subdirs.
555
+ logger.info('Phase 5/6: Cleaning up MongoDB hostPath data...');
556
+ shellExec(`sudo mkdir -p /data/mongodb`);
557
+ for (let i = 0; i < 3; i++) {
558
+ shellExec(`sudo rm -rf /data/mongodb/v${i}`);
559
+ shellExec(`sudo mkdir -p /data/mongodb/v${i}`);
560
+ }
561
+ // For Kind: repair any stale bind mounts via nsenter (overmounts with current host inode)
562
+ if (isKind) {
563
+ const nodesRaw = shellExec('kind get nodes', { stdout: true, silent: true, silentOnError: true });
564
+ const kindResetNodes = nodesRaw
565
+ .split('\n')
566
+ .map((n) => n.trim())
567
+ .filter(Boolean);
568
+ MongoBootstrap.remountKindMongoVolume(kindResetNodes);
569
+ }
570
+
571
+ // Phase 6: Wait for pod deletion to complete
572
+ logger.info('Phase 6/6: Waiting for MongoDB pods to terminate...');
573
+ shellExec(`kubectl wait --for=delete pod -l app=mongodb -n ${namespace} --timeout=120s`, { silentOnError: true });
574
+
575
+ logger.info('MongoDB reset completed successfully. Ready for fresh MongoDB deployment.');
576
+ } catch (error) {
577
+ logger.error(`Error during MongoDB reset: ${error.message}`);
578
+ console.error(error);
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Gets the primary MongoDB pod name from replica set status.
584
+ * @param {object} [options] - Query options.
585
+ * @param {string} [options.namespace='default'] - Kubernetes namespace.
586
+ * @param {string} [options.podName='mongodb-0'] - Any MongoDB pod to query.
587
+ * @param {string} [options.username] - MongoDB admin username.
588
+ * @param {string} [options.password] - MongoDB admin password.
589
+ * @param {string} [options.authDatabase='admin'] - Auth database.
590
+ * @param {boolean} [options.disableAuth=false] - Whether to disable auth in the query (for testing).
591
+ * @returns {string|null} Primary pod name, or null if not found.
592
+ */
593
+ static getPrimaryPodName(options = {}) {
594
+ const {
595
+ namespace = 'default',
596
+ podName = 'mongodb-0',
597
+ username,
598
+ password,
599
+ authDatabase = 'admin',
600
+ disableAuth = false,
601
+ } = options;
602
+
603
+ const readTrimmedFile = (filePath) => {
604
+ try {
605
+ if (fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf8').trim();
606
+ } catch {
607
+ /* ignore */
608
+ }
609
+ return '';
610
+ };
611
+
612
+ const mongoUser =
613
+ username ||
614
+ process.env.MONGODB_USERNAME ||
615
+ process.env.DB_USER ||
616
+ readTrimmedFile('./engine-private/mongodb-username');
617
+ const mongoPass =
618
+ password ||
619
+ process.env.MONGODB_PASSWORD ||
620
+ process.env.DB_PASSWORD ||
621
+ readTrimmedFile('./engine-private/mongodb-password');
622
+
623
+ const evalExpr = 'rs.status().members.filter(m=>m.stateStr=="PRIMARY").map(m=>m.name)';
624
+
625
+ const cli = disableAuth ? 'mongo' : 'mongosh';
626
+
627
+ let output = shellExec(`sudo kubectl exec -n ${namespace} -i ${podName} -- ${cli} --quiet --eval '${evalExpr}'`, {
628
+ stdout: true,
629
+ silent: true,
630
+ silentOnError: true,
631
+ });
632
+
633
+ if (!disableAuth && (!output || output.trim() === '') && mongoUser && mongoPass) {
634
+ output = shellExec(
635
+ `sudo kubectl exec -n ${namespace} -i ${podName} -- mongosh --quiet ` +
636
+ `--authenticationDatabase ${JSON.stringify(authDatabase)} ` +
637
+ `-u ${JSON.stringify(mongoUser)} -p ${JSON.stringify(mongoPass)} --eval '${evalExpr}'`,
638
+ { stdout: true, silent: true, silentOnError: true },
639
+ );
640
+ }
641
+
642
+ if (!output || output.trim() === '') {
643
+ logger.warn('No MongoDB primary pod found.');
644
+ return null;
645
+ }
646
+
647
+ const match = output.match(/['"]([^'"]+)['"]/);
648
+ if (match && match[1]) {
649
+ const primary = match[1].split(':')[0].split('.')[0];
650
+ logger.info('Found MongoDB primary pod', { primary });
651
+ return primary;
652
+ }
653
+ return null;
654
+ }
655
+ }
656
+
657
+ export { MongoBootstrap };