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
|
@@ -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 };
|