underpost 2.90.4 → 2.95.1
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/pwa-microservices-template-page.cd.yml +5 -4
- package/.github/workflows/release.cd.yml +7 -7
- package/README.md +7 -8
- package/bin/build.js +6 -1
- package/bin/deploy.js +2 -196
- package/cli.md +154 -80
- package/manifests/deployment/dd-default-development/deployment.yaml +4 -4
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/scripts/disk-clean.sh +216 -0
- package/scripts/rocky-setup.sh +1 -0
- package/scripts/ssh-cluster-info.sh +4 -3
- package/src/cli/cluster.js +1 -1
- package/src/cli/db.js +1143 -201
- package/src/cli/deploy.js +93 -24
- package/src/cli/env.js +2 -2
- package/src/cli/image.js +198 -133
- package/src/cli/index.js +111 -44
- package/src/cli/lxd.js +73 -74
- package/src/cli/monitor.js +20 -9
- package/src/cli/repository.js +212 -5
- package/src/cli/run.js +207 -74
- package/src/cli/ssh.js +642 -14
- package/src/client/components/core/CommonJs.js +0 -1
- package/src/db/mongo/MongooseDB.js +5 -1
- package/src/index.js +1 -1
- package/src/monitor.js +11 -1
- package/src/server/backup.js +1 -1
- package/src/server/conf.js +1 -1
- package/src/server/dns.js +242 -1
- package/src/server/process.js +6 -1
- package/src/server/start.js +2 -0
- package/scripts/snap-clean.sh +0 -26
- package/src/client/public/default/plantuml/client-conf.svg +0 -1
- package/src/client/public/default/plantuml/client-schema.svg +0 -1
- package/src/client/public/default/plantuml/cron-conf.svg +0 -1
- package/src/client/public/default/plantuml/cron-schema.svg +0 -1
- package/src/client/public/default/plantuml/server-conf.svg +0 -1
- package/src/client/public/default/plantuml/server-schema.svg +0 -1
- package/src/client/public/default/plantuml/ssr-conf.svg +0 -1
- package/src/client/public/default/plantuml/ssr-schema.svg +0 -1
package/src/cli/db.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* UnderpostDB CLI
|
|
2
|
+
* UnderpostDB CLI module
|
|
3
3
|
* @module src/cli/db.js
|
|
4
4
|
* @namespace UnderpostDB
|
|
5
|
+
* @description Manages database operations, backups, and cluster metadata for Kubernetes deployments.
|
|
6
|
+
* Supports MariaDB and MongoDB with import/export capabilities, Git integration, and multi-pod operations.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import { mergeFile, splitFileFactory } from '../server/conf.js';
|
|
8
10
|
import { loggerFactory } from '../server/logger.js';
|
|
9
11
|
import { shellExec } from '../server/process.js';
|
|
10
12
|
import fs from 'fs-extra';
|
|
13
|
+
import os from 'os';
|
|
11
14
|
import UnderpostDeploy from './deploy.js';
|
|
12
15
|
import UnderpostCron from './cron.js';
|
|
13
16
|
import { DataBaseProvider } from '../db/DataBaseProvider.js';
|
|
@@ -16,33 +19,768 @@ import { loadReplicas, pathPortAssignmentFactory } from '../server/conf.js';
|
|
|
16
19
|
const logger = loggerFactory(import.meta);
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
|
-
*
|
|
20
|
-
* @
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
* Constants for database operations
|
|
23
|
+
* @constant {number} MAX_BACKUP_RETENTION - Maximum number of backups to retain
|
|
24
|
+
* @memberof UnderpostDB
|
|
25
|
+
*/
|
|
26
|
+
const MAX_BACKUP_RETENTION = 5;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Timeout for kubectl operations in milliseconds
|
|
30
|
+
* @constant {number} KUBECTL_TIMEOUT
|
|
31
|
+
* @memberof UnderpostDB
|
|
32
|
+
*/
|
|
33
|
+
const KUBECTL_TIMEOUT = 300000; // 5 minutes
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} DatabaseOptions
|
|
37
|
+
* @memberof UnderpostDB
|
|
38
|
+
* @property {boolean} [import=false] - Flag to import data from a backup
|
|
39
|
+
* @property {boolean} [export=false] - Flag to export data to a backup
|
|
40
|
+
* @property {string} [podName=''] - Comma-separated list of pod names or patterns
|
|
41
|
+
* @property {string} [nodeName=''] - Comma-separated list of node names for pod filtering
|
|
42
|
+
* @property {string} [ns='default'] - Kubernetes namespace
|
|
43
|
+
* @property {string} [collections=''] - Comma-separated list of collections to include
|
|
44
|
+
* @property {string} [outPath=''] - Output path for backup files
|
|
45
|
+
* @property {boolean} [drop=false] - Flag to drop the database before importing
|
|
46
|
+
* @property {boolean} [preserveUUID=false] - Flag to preserve UUIDs during import
|
|
47
|
+
* @property {boolean} [git=false] - Flag to enable Git integration
|
|
48
|
+
* @property {string} [hosts=''] - Comma-separated list of hosts to include
|
|
49
|
+
* @property {string} [paths=''] - Comma-separated list of paths to include
|
|
50
|
+
* @property {string} [labelSelector=''] - Kubernetes label selector for pods
|
|
51
|
+
* @property {boolean} [allPods=false] - Flag to target all matching pods
|
|
52
|
+
* @property {boolean} [primaryPod=false] - Flag to automatically detect and use MongoDB primary pod
|
|
53
|
+
* @property {boolean} [stats=false] - Flag to display collection/table statistics
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} PodInfo
|
|
58
|
+
* @memberof UnderpostDB
|
|
59
|
+
* @property {string} NAME - Pod name
|
|
60
|
+
* @property {string} NAMESPACE - Pod namespace
|
|
61
|
+
* @property {string} NODE - Node where pod is running
|
|
62
|
+
* @property {string} STATUS - Pod status
|
|
63
|
+
* @property {string} [IP] - Pod IP address
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {Object} DatabaseConfig
|
|
24
68
|
* @memberof UnderpostDB
|
|
69
|
+
* @property {string} provider - Database provider (mariadb, mongoose)
|
|
70
|
+
* @property {string} name - Database name
|
|
71
|
+
* @property {string} user - Database user
|
|
72
|
+
* @property {string} password - Database password
|
|
73
|
+
* @property {string} hostFolder - Host folder path
|
|
74
|
+
* @property {string} host - Host identifier
|
|
75
|
+
* @property {string} path - Path identifier
|
|
76
|
+
* @property {number} [currentBackupTimestamp] - Timestamp of current backup
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @class UnderpostDB
|
|
81
|
+
* @description Manages database operations and backups for Kubernetes-based deployments.
|
|
82
|
+
* Provides comprehensive database management including import/export, multi-pod targeting,
|
|
83
|
+
* Git integration, and cluster metadata management.
|
|
25
84
|
*/
|
|
26
85
|
class UnderpostDB {
|
|
27
86
|
static API = {
|
|
28
87
|
/**
|
|
88
|
+
* Helper: Validates namespace name
|
|
89
|
+
* @private
|
|
90
|
+
* @param {string} namespace - Namespace to validate
|
|
91
|
+
* @returns {boolean} True if valid
|
|
92
|
+
*/
|
|
93
|
+
_validateNamespace(namespace) {
|
|
94
|
+
if (!namespace || typeof namespace !== 'string') return false;
|
|
95
|
+
// Kubernetes namespace naming rules: lowercase alphanumeric, -, max 63 chars
|
|
96
|
+
return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(namespace) && namespace.length <= 63;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Helper: Validates pod name
|
|
101
|
+
* @private
|
|
102
|
+
* @param {string} podName - Pod name to validate
|
|
103
|
+
* @returns {boolean} True if valid
|
|
104
|
+
*/
|
|
105
|
+
_validatePodName(podName) {
|
|
106
|
+
if (!podName || typeof podName !== 'string') return false;
|
|
107
|
+
// Kubernetes pod naming rules: lowercase alphanumeric, -, max 253 chars
|
|
108
|
+
return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(podName) && podName.length <= 253;
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Helper: Gets filtered pods based on criteria
|
|
113
|
+
* @private
|
|
114
|
+
* @param {Object} criteria - Filter criteria
|
|
115
|
+
* @param {string} [criteria.podNames] - Comma-separated pod name patterns
|
|
116
|
+
* @param {string} [criteria.nodeNames] - Comma-separated node names
|
|
117
|
+
* @param {string} [criteria.namespace='default'] - Kubernetes namespace
|
|
118
|
+
* @param {string} [criteria.labelSelector] - Label selector
|
|
119
|
+
* @param {string} [criteria.deployId] - Deployment ID pattern
|
|
120
|
+
* @returns {Array<PodInfo>} Filtered pod list
|
|
121
|
+
*/
|
|
122
|
+
_getFilteredPods(criteria = {}) {
|
|
123
|
+
const { podNames, nodeNames, namespace = 'default', labelSelector, deployId } = criteria;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Get all pods using UnderpostDeploy.API.get
|
|
127
|
+
let pods = UnderpostDeploy.API.get(deployId || '', 'pods', namespace);
|
|
128
|
+
|
|
129
|
+
// Filter by pod names if specified
|
|
130
|
+
if (podNames) {
|
|
131
|
+
const patterns = podNames.split(',').map((p) => p.trim());
|
|
132
|
+
pods = pods.filter((pod) => {
|
|
133
|
+
return patterns.some((pattern) => {
|
|
134
|
+
// Support wildcards
|
|
135
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
136
|
+
return regex.test(pod.NAME);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Filter by node names if specified (only if NODE is not '<none>')
|
|
142
|
+
if (nodeNames) {
|
|
143
|
+
const nodes = nodeNames.split(',').map((n) => n.trim());
|
|
144
|
+
pods = pods.filter((pod) => {
|
|
145
|
+
// Skip filtering if NODE is '<none>' or undefined
|
|
146
|
+
if (!pod.NODE || pod.NODE === '<none>') {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return nodes.includes(pod.NODE);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Filter by label selector if specified
|
|
154
|
+
if (labelSelector) {
|
|
155
|
+
// Note: UnderpostDeploy.API.get doesn't support label selectors directly
|
|
156
|
+
// This would require a separate kubectl command
|
|
157
|
+
logger.warn('Label selector filtering requires additional implementation');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
logger.info(`Found ${pods.length} pod(s) matching criteria`, { criteria, podNames: pods.map((p) => p.NAME) });
|
|
161
|
+
return pods;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
logger.error('Error filtering pods', { error: error.message, criteria });
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Helper: Executes kubectl command with error handling
|
|
170
|
+
* @private
|
|
171
|
+
* @param {string} command - kubectl command to execute
|
|
172
|
+
* @param {Object} options - Execution options
|
|
173
|
+
* @param {string} [options.context=''] - Command context for logging
|
|
174
|
+
* @returns {string|null} Command output or null on error
|
|
175
|
+
*/
|
|
176
|
+
_executeKubectl(command, options = {}) {
|
|
177
|
+
const { context = '' } = options;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
logger.info(`Executing kubectl command`, { command, context });
|
|
181
|
+
return shellExec(command, { stdout: true });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
logger.error(`kubectl command failed`, { command, error: error.message, context });
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Helper: Copies file to pod
|
|
190
|
+
* @private
|
|
191
|
+
* @param {Object} params - Copy parameters
|
|
192
|
+
* @param {string} params.sourcePath - Source file path
|
|
193
|
+
* @param {string} params.podName - Target pod name
|
|
194
|
+
* @param {string} params.namespace - Pod namespace
|
|
195
|
+
* @param {string} params.destPath - Destination path in pod
|
|
196
|
+
* @returns {boolean} Success status
|
|
197
|
+
*/
|
|
198
|
+
_copyToPod({ sourcePath, podName, namespace, destPath }) {
|
|
199
|
+
try {
|
|
200
|
+
const command = `sudo kubectl cp ${sourcePath} ${namespace}/${podName}:${destPath}`;
|
|
201
|
+
UnderpostDB.API._executeKubectl(command, { context: `copy to pod ${podName}` });
|
|
202
|
+
return true;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logger.error('Failed to copy file to pod', { sourcePath, podName, destPath, error: error.message });
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Helper: Copies file from pod
|
|
211
|
+
* @private
|
|
212
|
+
* @param {Object} params - Copy parameters
|
|
213
|
+
* @param {string} params.podName - Source pod name
|
|
214
|
+
* @param {string} params.namespace - Pod namespace
|
|
215
|
+
* @param {string} params.sourcePath - Source path in pod
|
|
216
|
+
* @param {string} params.destPath - Destination file path
|
|
217
|
+
* @returns {boolean} Success status
|
|
218
|
+
*/
|
|
219
|
+
_copyFromPod({ podName, namespace, sourcePath, destPath }) {
|
|
220
|
+
try {
|
|
221
|
+
const command = `sudo kubectl cp ${namespace}/${podName}:${sourcePath} ${destPath}`;
|
|
222
|
+
UnderpostDB.API._executeKubectl(command, { context: `copy from pod ${podName}` });
|
|
223
|
+
return true;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
logger.error('Failed to copy file from pod', { podName, sourcePath, destPath, error: error.message });
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Helper: Executes command in pod
|
|
232
|
+
* @private
|
|
233
|
+
* @param {Object} params - Execution parameters
|
|
234
|
+
* @param {string} params.podName - Pod name
|
|
235
|
+
* @param {string} params.namespace - Pod namespace
|
|
236
|
+
* @param {string} params.command - Command to execute
|
|
237
|
+
* @returns {string|null} Command output or null
|
|
238
|
+
*/
|
|
239
|
+
_execInPod({ podName, namespace, command }) {
|
|
240
|
+
try {
|
|
241
|
+
const kubectlCmd = `sudo kubectl exec -n ${namespace} -i ${podName} -- sh -c "${command}"`;
|
|
242
|
+
return UnderpostDB.API._executeKubectl(kubectlCmd, { context: `exec in pod ${podName}` });
|
|
243
|
+
} catch (error) {
|
|
244
|
+
logger.error('Failed to execute command in pod', { podName, command, error: error.message });
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Helper: Manages Git repository for backups
|
|
251
|
+
* @private
|
|
252
|
+
* @param {Object} params - Git parameters
|
|
253
|
+
* @param {string} params.repoName - Repository name
|
|
254
|
+
* @param {string} params.operation - Operation (clone, pull, commit, push)
|
|
255
|
+
* @param {string} [params.message=''] - Commit message
|
|
256
|
+
* @returns {boolean} Success status
|
|
257
|
+
*/
|
|
258
|
+
_manageGitRepo({ repoName, operation, message = '' }) {
|
|
259
|
+
try {
|
|
260
|
+
const username = process.env.GITHUB_USERNAME;
|
|
261
|
+
if (!username) {
|
|
262
|
+
logger.error('GITHUB_USERNAME environment variable not set');
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const repoPath = `../${repoName}`;
|
|
267
|
+
|
|
268
|
+
switch (operation) {
|
|
269
|
+
case 'clone':
|
|
270
|
+
if (!fs.existsSync(repoPath)) {
|
|
271
|
+
shellExec(`cd .. && underpost clone ${username}/${repoName}`);
|
|
272
|
+
logger.info(`Cloned repository: ${repoName}`);
|
|
273
|
+
}
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case 'pull':
|
|
277
|
+
if (fs.existsSync(repoPath)) {
|
|
278
|
+
shellExec(`cd ${repoPath} && git checkout . && git clean -f -d`);
|
|
279
|
+
shellExec(`cd ${repoPath} && underpost pull . ${username}/${repoName}`, {
|
|
280
|
+
silent: true,
|
|
281
|
+
});
|
|
282
|
+
logger.info(`Pulled repository: ${repoName}`);
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
|
|
286
|
+
case 'commit':
|
|
287
|
+
if (fs.existsSync(repoPath)) {
|
|
288
|
+
shellExec(`cd ${repoPath} && git add .`);
|
|
289
|
+
shellExec(`underpost cmt ${repoPath} backup '' '${message}'`);
|
|
290
|
+
logger.info(`Committed to repository: ${repoName}`, { message });
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
|
|
294
|
+
case 'push':
|
|
295
|
+
if (fs.existsSync(repoPath)) {
|
|
296
|
+
shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName}`, { silent: true });
|
|
297
|
+
logger.info(`Pushed repository: ${repoName}`);
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
default:
|
|
302
|
+
logger.warn(`Unknown git operation: ${operation}`);
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return true;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
logger.error(`Git operation failed`, { repoName, operation, error: error.message });
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Helper: Manages backup timestamps and cleanup
|
|
315
|
+
* @private
|
|
316
|
+
* @param {string} backupPath - Backup directory path
|
|
317
|
+
* @param {number} newTimestamp - New backup timestamp
|
|
318
|
+
* @param {boolean} shouldCleanup - Whether to cleanup old backups
|
|
319
|
+
* @returns {Object} Backup info with current and removed timestamps
|
|
320
|
+
*/
|
|
321
|
+
_manageBackupTimestamps(backupPath, newTimestamp, shouldCleanup) {
|
|
322
|
+
try {
|
|
323
|
+
if (!fs.existsSync(backupPath)) {
|
|
324
|
+
fs.mkdirSync(backupPath, { recursive: true });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Delete empty folders
|
|
328
|
+
shellExec(`cd ${backupPath} && find . -type d -empty -delete`);
|
|
329
|
+
|
|
330
|
+
const times = fs.readdirSync(backupPath);
|
|
331
|
+
const validTimes = times.map((t) => parseInt(t)).filter((t) => !isNaN(t));
|
|
332
|
+
|
|
333
|
+
const currentBackupTimestamp = validTimes.length > 0 ? Math.max(...validTimes) : null;
|
|
334
|
+
const removeBackupTimestamp = validTimes.length > 0 ? Math.min(...validTimes) : null;
|
|
335
|
+
|
|
336
|
+
// Cleanup old backups if we have too many
|
|
337
|
+
if (shouldCleanup && validTimes.length >= MAX_BACKUP_RETENTION && removeBackupTimestamp) {
|
|
338
|
+
const removeDir = `${backupPath}/${removeBackupTimestamp}`;
|
|
339
|
+
logger.info('Removing old backup', { path: removeDir });
|
|
340
|
+
fs.removeSync(removeDir);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Create new backup directory
|
|
344
|
+
if (shouldCleanup) {
|
|
345
|
+
const newBackupDir = `${backupPath}/${newTimestamp}`;
|
|
346
|
+
logger.info('Creating new backup directory', { path: newBackupDir });
|
|
347
|
+
fs.mkdirSync(newBackupDir, { recursive: true });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
current: currentBackupTimestamp,
|
|
352
|
+
removed: removeBackupTimestamp,
|
|
353
|
+
count: validTimes.length,
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
logger.error('Error managing backup timestamps', { backupPath, error: error.message });
|
|
357
|
+
return { current: null, removed: null, count: 0 };
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Helper: Performs MariaDB import operation
|
|
363
|
+
* @private
|
|
364
|
+
* @param {Object} params - Import parameters
|
|
365
|
+
* @param {PodInfo} params.pod - Target pod
|
|
366
|
+
* @param {string} params.namespace - Namespace
|
|
367
|
+
* @param {string} params.dbName - Database name
|
|
368
|
+
* @param {string} params.user - Database user
|
|
369
|
+
* @param {string} params.password - Database password
|
|
370
|
+
* @param {string} params.sqlPath - SQL file path
|
|
371
|
+
* @returns {boolean} Success status
|
|
372
|
+
*/
|
|
373
|
+
_importMariaDB({ pod, namespace, dbName, user, password, sqlPath }) {
|
|
374
|
+
try {
|
|
375
|
+
const podName = pod.NAME;
|
|
376
|
+
const containerSqlPath = `/${dbName}.sql`;
|
|
377
|
+
|
|
378
|
+
logger.info('Importing MariaDB database', { podName, dbName });
|
|
379
|
+
|
|
380
|
+
// Remove existing SQL file in container
|
|
381
|
+
UnderpostDB.API._execInPod({
|
|
382
|
+
podName,
|
|
383
|
+
namespace,
|
|
384
|
+
command: `rm -rf ${containerSqlPath}`,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Copy SQL file to pod
|
|
388
|
+
if (
|
|
389
|
+
!UnderpostDB.API._copyToPod({
|
|
390
|
+
sourcePath: sqlPath,
|
|
391
|
+
podName,
|
|
392
|
+
namespace,
|
|
393
|
+
destPath: containerSqlPath,
|
|
394
|
+
})
|
|
395
|
+
) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Create database if it doesn't exist
|
|
400
|
+
UnderpostDB.API._executeKubectl(
|
|
401
|
+
`kubectl exec -n ${namespace} -i ${podName} -- mariadb -p${password} -e 'CREATE DATABASE IF NOT EXISTS ${dbName};'`,
|
|
402
|
+
{ context: `create database ${dbName}` },
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// Import SQL file
|
|
406
|
+
const importCmd = `mariadb -u ${user} -p${password} ${dbName} < ${containerSqlPath}`;
|
|
407
|
+
UnderpostDB.API._execInPod({ podName, namespace, command: importCmd });
|
|
408
|
+
|
|
409
|
+
logger.info('Successfully imported MariaDB database', { podName, dbName });
|
|
410
|
+
return true;
|
|
411
|
+
} catch (error) {
|
|
412
|
+
logger.error('MariaDB import failed', { podName: pod.NAME, dbName, error: error.message });
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Helper: Performs MariaDB export operation
|
|
419
|
+
* @private
|
|
420
|
+
* @param {Object} params - Export parameters
|
|
421
|
+
* @param {PodInfo} params.pod - Source pod
|
|
422
|
+
* @param {string} params.namespace - Namespace
|
|
423
|
+
* @param {string} params.dbName - Database name
|
|
424
|
+
* @param {string} params.user - Database user
|
|
425
|
+
* @param {string} params.password - Database password
|
|
426
|
+
* @param {string} params.outputPath - Output file path
|
|
427
|
+
* @returns {boolean} Success status
|
|
428
|
+
*/
|
|
429
|
+
async _exportMariaDB({ pod, namespace, dbName, user, password, outputPath }) {
|
|
430
|
+
try {
|
|
431
|
+
const podName = pod.NAME;
|
|
432
|
+
const containerSqlPath = `/home/${dbName}.sql`;
|
|
433
|
+
|
|
434
|
+
logger.info('Exporting MariaDB database', { podName, dbName });
|
|
435
|
+
|
|
436
|
+
// Remove existing SQL file in container
|
|
437
|
+
UnderpostDB.API._execInPod({
|
|
438
|
+
podName,
|
|
439
|
+
namespace,
|
|
440
|
+
command: `rm -rf ${containerSqlPath}`,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Dump database
|
|
444
|
+
const dumpCmd = `mariadb-dump --user=${user} --password=${password} --lock-tables ${dbName} > ${containerSqlPath}`;
|
|
445
|
+
UnderpostDB.API._execInPod({ podName, namespace, command: dumpCmd });
|
|
446
|
+
|
|
447
|
+
// Copy SQL file from pod
|
|
448
|
+
if (
|
|
449
|
+
!UnderpostDB.API._copyFromPod({
|
|
450
|
+
podName,
|
|
451
|
+
namespace,
|
|
452
|
+
sourcePath: containerSqlPath,
|
|
453
|
+
destPath: outputPath,
|
|
454
|
+
})
|
|
455
|
+
) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Split file if it exists
|
|
460
|
+
if (fs.existsSync(outputPath)) {
|
|
461
|
+
await splitFileFactory(dbName, outputPath);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
logger.info('Successfully exported MariaDB database', { podName, dbName, outputPath });
|
|
465
|
+
return true;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
logger.error('MariaDB export failed', { podName: pod.NAME, dbName, error: error.message });
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Helper: Performs MongoDB import operation
|
|
474
|
+
* @private
|
|
475
|
+
* @param {Object} params - Import parameters
|
|
476
|
+
* @param {PodInfo} params.pod - Target pod
|
|
477
|
+
* @param {string} params.namespace - Namespace
|
|
478
|
+
* @param {string} params.dbName - Database name
|
|
479
|
+
* @param {string} params.bsonPath - BSON directory path
|
|
480
|
+
* @param {boolean} params.drop - Whether to drop existing database
|
|
481
|
+
* @param {boolean} params.preserveUUID - Whether to preserve UUIDs
|
|
482
|
+
* @returns {boolean} Success status
|
|
483
|
+
*/
|
|
484
|
+
_importMongoDB({ pod, namespace, dbName, bsonPath, drop, preserveUUID }) {
|
|
485
|
+
try {
|
|
486
|
+
const podName = pod.NAME;
|
|
487
|
+
const containerBsonPath = `/${dbName}`;
|
|
488
|
+
|
|
489
|
+
logger.info('Importing MongoDB database', { podName, dbName });
|
|
490
|
+
|
|
491
|
+
// Remove existing BSON directory in container
|
|
492
|
+
UnderpostDB.API._execInPod({
|
|
493
|
+
podName,
|
|
494
|
+
namespace,
|
|
495
|
+
command: `rm -rf ${containerBsonPath}`,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Copy BSON directory to pod
|
|
499
|
+
if (
|
|
500
|
+
!UnderpostDB.API._copyToPod({
|
|
501
|
+
sourcePath: bsonPath,
|
|
502
|
+
podName,
|
|
503
|
+
namespace,
|
|
504
|
+
destPath: containerBsonPath,
|
|
505
|
+
})
|
|
506
|
+
) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Restore database
|
|
511
|
+
const restoreCmd = `mongorestore -d ${dbName} ${containerBsonPath}${drop ? ' --drop' : ''}${
|
|
512
|
+
preserveUUID ? ' --preserveUUID' : ''
|
|
513
|
+
}`;
|
|
514
|
+
UnderpostDB.API._execInPod({ podName, namespace, command: restoreCmd });
|
|
515
|
+
|
|
516
|
+
logger.info('Successfully imported MongoDB database', { podName, dbName });
|
|
517
|
+
return true;
|
|
518
|
+
} catch (error) {
|
|
519
|
+
logger.error('MongoDB import failed', { podName: pod.NAME, dbName, error: error.message });
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Helper: Performs MongoDB export operation
|
|
526
|
+
* @private
|
|
527
|
+
* @param {Object} params - Export parameters
|
|
528
|
+
* @param {PodInfo} params.pod - Source pod
|
|
529
|
+
* @param {string} params.namespace - Namespace
|
|
530
|
+
* @param {string} params.dbName - Database name
|
|
531
|
+
* @param {string} params.outputPath - Output directory path
|
|
532
|
+
* @param {string} [params.collections=''] - Comma-separated collection list
|
|
533
|
+
* @returns {boolean} Success status
|
|
534
|
+
*/
|
|
535
|
+
_exportMongoDB({ pod, namespace, dbName, outputPath, collections = '' }) {
|
|
536
|
+
try {
|
|
537
|
+
const podName = pod.NAME;
|
|
538
|
+
const containerBsonPath = `/${dbName}`;
|
|
539
|
+
|
|
540
|
+
logger.info('Exporting MongoDB database', { podName, dbName, collections });
|
|
541
|
+
|
|
542
|
+
// Remove existing BSON directory in container
|
|
543
|
+
UnderpostDB.API._execInPod({
|
|
544
|
+
podName,
|
|
545
|
+
namespace,
|
|
546
|
+
command: `rm -rf ${containerBsonPath}`,
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Dump database or specific collections
|
|
550
|
+
if (collections) {
|
|
551
|
+
const collectionList = collections.split(',').map((c) => c.trim());
|
|
552
|
+
for (const collection of collectionList) {
|
|
553
|
+
const dumpCmd = `mongodump -d ${dbName} --collection ${collection} -o /`;
|
|
554
|
+
UnderpostDB.API._execInPod({ podName, namespace, command: dumpCmd });
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
const dumpCmd = `mongodump -d ${dbName} -o /`;
|
|
558
|
+
UnderpostDB.API._execInPod({ podName, namespace, command: dumpCmd });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Copy BSON directory from pod
|
|
562
|
+
if (
|
|
563
|
+
!UnderpostDB.API._copyFromPod({
|
|
564
|
+
podName,
|
|
565
|
+
namespace,
|
|
566
|
+
sourcePath: containerBsonPath,
|
|
567
|
+
destPath: outputPath,
|
|
568
|
+
})
|
|
569
|
+
) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
logger.info('Successfully exported MongoDB database', { podName, dbName, outputPath });
|
|
574
|
+
return true;
|
|
575
|
+
} catch (error) {
|
|
576
|
+
logger.error('MongoDB export failed', { podName: pod.NAME, dbName, error: error.message });
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Helper: Gets MongoDB collection statistics
|
|
583
|
+
* @private
|
|
584
|
+
* @param {Object} params - Parameters
|
|
585
|
+
* @param {string} params.podName - Pod name
|
|
586
|
+
* @param {string} params.namespace - Namespace
|
|
587
|
+
* @param {string} params.dbName - Database name
|
|
588
|
+
* @returns {Object|null} Collection statistics or null on error
|
|
589
|
+
*/
|
|
590
|
+
_getMongoStats({ podName, namespace, dbName }) {
|
|
591
|
+
try {
|
|
592
|
+
logger.info('Getting MongoDB collection statistics', { podName, dbName });
|
|
593
|
+
|
|
594
|
+
// Use db.getSiblingDB() instead of 'use' command
|
|
595
|
+
const script = `db.getSiblingDB('${dbName}').getCollectionNames().map(function(c) { return { collection: c, count: db.getSiblingDB('${dbName}')[c].countDocuments() }; })`;
|
|
596
|
+
|
|
597
|
+
// Execute the script
|
|
598
|
+
const command = `sudo kubectl exec -n ${namespace} -i ${podName} -- mongosh --quiet --eval "${script}"`;
|
|
599
|
+
const output = shellExec(command, { stdout: true, silent: true });
|
|
600
|
+
|
|
601
|
+
if (!output || output.trim() === '') {
|
|
602
|
+
logger.warn('No collections found or empty output');
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Clean the output: remove newlines, handle EJSON format, replace single quotes with double quotes
|
|
607
|
+
let cleanedOutput = output
|
|
608
|
+
.trim()
|
|
609
|
+
.replace(/\n/g, '')
|
|
610
|
+
.replace(/\s+/g, ' ')
|
|
611
|
+
.replace(/NumberLong\("(\d+)"\)/g, '$1')
|
|
612
|
+
.replace(/NumberLong\((\d+)\)/g, '$1')
|
|
613
|
+
.replace(/NumberInt\("(\d+)"\)/g, '$1')
|
|
614
|
+
.replace(/NumberInt\((\d+)\)/g, '$1')
|
|
615
|
+
.replace(/ISODate\("([^"]+)"\)/g, '"$1"')
|
|
616
|
+
.replace(/'/g, '"')
|
|
617
|
+
.replace(/(\w+):/g, '"$1":');
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
const stats = JSON.parse(cleanedOutput);
|
|
621
|
+
logger.info('MongoDB statistics retrieved', { dbName, collections: stats.length });
|
|
622
|
+
return stats;
|
|
623
|
+
} catch (parseError) {
|
|
624
|
+
logger.error('Failed to parse MongoDB output', {
|
|
625
|
+
podName,
|
|
626
|
+
dbName,
|
|
627
|
+
error: parseError.message,
|
|
628
|
+
rawOutput: output.substring(0, 200),
|
|
629
|
+
cleanedOutput: cleanedOutput.substring(0, 200),
|
|
630
|
+
});
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
} catch (error) {
|
|
634
|
+
logger.error('Failed to get MongoDB statistics', { podName, dbName, error: error.message });
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Helper: Gets MariaDB table statistics
|
|
641
|
+
* @private
|
|
642
|
+
* @param {Object} params - Parameters
|
|
643
|
+
* @param {string} params.podName - Pod name
|
|
644
|
+
* @param {string} params.namespace - Namespace
|
|
645
|
+
* @param {string} params.dbName - Database name
|
|
646
|
+
* @param {string} params.user - Database user
|
|
647
|
+
* @param {string} params.password - Database password
|
|
648
|
+
* @returns {Object|null} Table statistics or null on error
|
|
649
|
+
*/
|
|
650
|
+
_getMariaDBStats({ podName, namespace, dbName, user, password }) {
|
|
651
|
+
try {
|
|
652
|
+
logger.info('Getting MariaDB table statistics', { podName, dbName });
|
|
653
|
+
|
|
654
|
+
const command = `sudo kubectl exec -n ${namespace} -i ${podName} -- mariadb -u ${user} -p${password} ${dbName} -e "SELECT TABLE_NAME as 'table', TABLE_ROWS as 'count' FROM information_schema.TABLES WHERE TABLE_SCHEMA = '${dbName}' ORDER BY TABLE_NAME;" --skip-column-names --batch`;
|
|
655
|
+
const output = shellExec(command, { stdout: true, silent: true });
|
|
656
|
+
|
|
657
|
+
if (!output || output.trim() === '') {
|
|
658
|
+
logger.warn('No tables found or empty output');
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Parse the output (tab-separated values)
|
|
663
|
+
const lines = output.trim().split('\n');
|
|
664
|
+
const stats = lines.map((line) => {
|
|
665
|
+
const [table, count] = line.split('\t');
|
|
666
|
+
return { table, count: parseInt(count) || 0 };
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
logger.info('MariaDB statistics retrieved', { dbName, tables: stats.length });
|
|
670
|
+
return stats;
|
|
671
|
+
} catch (error) {
|
|
672
|
+
logger.error('Failed to get MariaDB statistics', { podName, dbName, error: error.message });
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Helper: Displays database statistics in table format
|
|
679
|
+
* @private
|
|
680
|
+
* @param {Object} params - Parameters
|
|
681
|
+
* @param {string} params.provider - Database provider
|
|
682
|
+
* @param {string} params.dbName - Database name
|
|
683
|
+
* @param {Array<Object>} params.stats - Statistics array
|
|
684
|
+
*/
|
|
685
|
+
_displayStats({ provider, dbName, stats }) {
|
|
686
|
+
if (!stats || stats.length === 0) {
|
|
687
|
+
logger.warn('No statistics to display', { provider, dbName });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const title = provider === 'mongoose' ? 'Collections' : 'Tables';
|
|
692
|
+
const itemKey = provider === 'mongoose' ? 'collection' : 'table';
|
|
693
|
+
|
|
694
|
+
console.log('\n' + '='.repeat(70));
|
|
695
|
+
console.log(`DATABASE: ${dbName} (${provider.toUpperCase()})`);
|
|
696
|
+
console.log('='.repeat(70));
|
|
697
|
+
console.log(`${title.padEnd(50)} ${'Documents/Rows'.padStart(18)}`);
|
|
698
|
+
console.log('-'.repeat(70));
|
|
699
|
+
|
|
700
|
+
let totalCount = 0;
|
|
701
|
+
stats.forEach((item) => {
|
|
702
|
+
const name = item[itemKey] || 'Unknown';
|
|
703
|
+
const count = item.count || 0;
|
|
704
|
+
totalCount += count;
|
|
705
|
+
console.log(`${name.padEnd(50)} ${count.toString().padStart(18)}`);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
console.log('-'.repeat(70));
|
|
709
|
+
console.log(`${'TOTAL'.padEnd(50)} ${totalCount.toString().padStart(18)}`);
|
|
710
|
+
console.log('='.repeat(70) + '\n');
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Public API: Gets MongoDB primary pod name
|
|
715
|
+
* @public
|
|
716
|
+
* @param {Object} options - Options for getting primary pod
|
|
717
|
+
* @param {string} [options.namespace='default'] - Kubernetes namespace
|
|
718
|
+
* @param {string} [options.podName='mongodb-0'] - Initial pod name to query replica set status
|
|
719
|
+
* @returns {string|null} Primary pod name or null if not found
|
|
720
|
+
* @memberof UnderpostDB
|
|
721
|
+
* @example
|
|
722
|
+
* const primaryPod = UnderpostDB.API.getMongoPrimaryPodName({ namespace: 'production' });
|
|
723
|
+
* console.log(primaryPod); // 'mongodb-1'
|
|
724
|
+
*/
|
|
725
|
+
getMongoPrimaryPodName(options = { namespace: 'default', podName: 'mongodb-0' }) {
|
|
726
|
+
const { namespace = 'default', podName = 'mongodb-0' } = options;
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
logger.info('Checking for MongoDB primary pod', { namespace, checkingPod: podName });
|
|
730
|
+
|
|
731
|
+
const command = `sudo kubectl exec -n ${namespace} -i ${podName} -- mongosh --quiet --eval 'rs.status().members.filter(m => m.stateStr=="PRIMARY").map(m=>m.name)'`;
|
|
732
|
+
const output = shellExec(command, { stdout: true, silent: true });
|
|
733
|
+
|
|
734
|
+
if (!output || output.trim() === '') {
|
|
735
|
+
logger.warn('No primary pod found in replica set');
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Parse the output to get the primary pod name
|
|
740
|
+
// Output format: [ 'mongodb-0:27017' ] or [ 'mongodb-1.mongodb-service:27017' ] or similar
|
|
741
|
+
const match = output.match(/['"]([^'"]+)['"]/);
|
|
742
|
+
if (match && match[1]) {
|
|
743
|
+
let primaryName = match[1].split(':')[0]; // Extract pod name without port
|
|
744
|
+
// Remove service suffix if present (e.g., "mongodb-1.mongodb-service" -> "mongodb-1")
|
|
745
|
+
primaryName = primaryName.split('.')[0];
|
|
746
|
+
logger.info('Found MongoDB primary pod', { primaryPod: primaryName });
|
|
747
|
+
return primaryName;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
logger.warn('Could not parse primary pod from replica set status', { output });
|
|
751
|
+
return null;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
logger.error('Failed to get MongoDB primary pod', { error: error.message });
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Main callback: Initiates database backup workflow
|
|
29
760
|
* @method callback
|
|
30
|
-
* @description
|
|
31
|
-
* This method orchestrates the backup process for multiple deployments, handling
|
|
761
|
+
* @description Orchestrates the backup process for multiple deployments, handling
|
|
32
762
|
* database connections, backup storage, and optional Git integration for version control.
|
|
33
|
-
*
|
|
34
|
-
* @param {
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {boolean} [options.
|
|
37
|
-
* @param {
|
|
38
|
-
* @param {string} [options.
|
|
39
|
-
* @param {string} [options.
|
|
40
|
-
* @param {string} [options.
|
|
41
|
-
* @param {
|
|
42
|
-
* @param {
|
|
43
|
-
* @param {boolean} [options.
|
|
44
|
-
* @param {
|
|
45
|
-
* @param {
|
|
763
|
+
* Supports targeting multiple specific pods, nodes, and namespaces with advanced filtering.
|
|
764
|
+
* @param {string} [deployList='default'] - Comma-separated list of deployment IDs
|
|
765
|
+
* @param {Object} options - Backup options
|
|
766
|
+
* @param {boolean} [options.import=false] - Whether to perform import operation
|
|
767
|
+
* @param {boolean} [options.export=false] - Whether to perform export operation
|
|
768
|
+
* @param {string} [options.podName=''] - Comma-separated pod name patterns to target
|
|
769
|
+
* @param {string} [options.nodeName=''] - Comma-separated node names to target
|
|
770
|
+
* @param {string} [options.ns='default'] - Kubernetes namespace
|
|
771
|
+
* @param {string} [options.collections=''] - Comma-separated MongoDB collections for export
|
|
772
|
+
* @param {string} [options.outPath=''] - Output path for backups
|
|
773
|
+
* @param {boolean} [options.drop=false] - Whether to drop existing database on import
|
|
774
|
+
* @param {boolean} [options.preserveUUID=false] - Whether to preserve UUIDs on MongoDB import
|
|
775
|
+
* @param {boolean} [options.git=false] - Whether to use Git for backup versioning
|
|
776
|
+
* @param {string} [options.hosts=''] - Comma-separated list of hosts to filter databases
|
|
777
|
+
* @param {string} [options.paths=''] - Comma-separated list of paths to filter databases
|
|
778
|
+
* @param {string} [options.labelSelector=''] - Label selector for pod filtering
|
|
779
|
+
* @param {boolean} [options.allPods=false] - Whether to target all pods in deployment
|
|
780
|
+
* @param {boolean} [options.primaryPod=false] - Whether to target MongoDB primary pod only
|
|
781
|
+
* @param {boolean} [options.stats=false] - Whether to display database statistics
|
|
782
|
+
* @param {number} [options.macroRollbackExport=1] - Number of commits to rollback in macro export
|
|
783
|
+
* @returns {Promise<void>} Resolves when operation is complete
|
|
46
784
|
* @memberof UnderpostDB
|
|
47
785
|
*/
|
|
48
786
|
async callback(
|
|
@@ -50,8 +788,9 @@ class UnderpostDB {
|
|
|
50
788
|
options = {
|
|
51
789
|
import: false,
|
|
52
790
|
export: false,
|
|
53
|
-
podName:
|
|
54
|
-
|
|
791
|
+
podName: '',
|
|
792
|
+
nodeName: '',
|
|
793
|
+
ns: 'default',
|
|
55
794
|
collections: '',
|
|
56
795
|
outPath: '',
|
|
57
796
|
drop: false,
|
|
@@ -59,17 +798,51 @@ class UnderpostDB {
|
|
|
59
798
|
git: false,
|
|
60
799
|
hosts: '',
|
|
61
800
|
paths: '',
|
|
801
|
+
labelSelector: '',
|
|
802
|
+
allPods: false,
|
|
803
|
+
primaryPod: false,
|
|
804
|
+
stats: false,
|
|
805
|
+
macroRollbackExport: 1,
|
|
62
806
|
},
|
|
63
807
|
) {
|
|
64
808
|
const newBackupTimestamp = new Date().getTime();
|
|
65
|
-
const
|
|
809
|
+
const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
|
|
810
|
+
|
|
811
|
+
// Validate namespace
|
|
812
|
+
if (!UnderpostDB.API._validateNamespace(namespace)) {
|
|
813
|
+
logger.error('Invalid namespace format', { namespace });
|
|
814
|
+
throw new Error(`Invalid namespace: ${namespace}`);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
|
|
818
|
+
|
|
819
|
+
logger.info('Starting database operation', {
|
|
820
|
+
deployList,
|
|
821
|
+
namespace,
|
|
822
|
+
import: options.import,
|
|
823
|
+
export: options.export,
|
|
824
|
+
});
|
|
825
|
+
|
|
66
826
|
for (const _deployId of deployList.split(',')) {
|
|
67
827
|
const deployId = _deployId.trim();
|
|
68
828
|
if (!deployId) continue;
|
|
829
|
+
|
|
830
|
+
logger.info('Processing deployment', { deployId });
|
|
831
|
+
|
|
832
|
+
/** @type {Object.<string, Object.<string, DatabaseConfig>>} */
|
|
69
833
|
const dbs = {};
|
|
70
834
|
const repoName = `engine-${deployId.split('dd-')[1]}-cron-backups`;
|
|
71
835
|
|
|
72
|
-
|
|
836
|
+
// Load server configuration
|
|
837
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
838
|
+
if (!fs.existsSync(confServerPath)) {
|
|
839
|
+
logger.error('Configuration file not found', { path: confServerPath });
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const confServer = JSON.parse(fs.readFileSync(confServerPath, 'utf8'));
|
|
844
|
+
|
|
845
|
+
// Build database configuration map
|
|
73
846
|
for (const host of Object.keys(confServer)) {
|
|
74
847
|
for (const path of Object.keys(confServer[host])) {
|
|
75
848
|
const { db } = confServer[host][path];
|
|
@@ -77,193 +850,307 @@ class UnderpostDB {
|
|
|
77
850
|
const { provider, name, user, password } = db;
|
|
78
851
|
if (!dbs[provider]) dbs[provider] = {};
|
|
79
852
|
|
|
80
|
-
if (!(name in dbs[provider]))
|
|
81
|
-
dbs[provider][name] = {
|
|
853
|
+
if (!(name in dbs[provider])) {
|
|
854
|
+
dbs[provider][name] = {
|
|
855
|
+
user,
|
|
856
|
+
password,
|
|
857
|
+
hostFolder: host + path.replaceAll('/', '-'),
|
|
858
|
+
host,
|
|
859
|
+
path,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
82
862
|
}
|
|
83
863
|
}
|
|
84
864
|
}
|
|
85
865
|
|
|
866
|
+
// Handle Git operations
|
|
86
867
|
if (options.git === true) {
|
|
87
|
-
|
|
88
|
-
|
|
868
|
+
UnderpostDB.API._manageGitRepo({ repoName, operation: 'clone' });
|
|
869
|
+
UnderpostDB.API._manageGitRepo({ repoName, operation: 'pull' });
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (options.macroRollbackExport) {
|
|
873
|
+
UnderpostDB.API._manageGitRepo({ repoName, operation: 'clone' });
|
|
874
|
+
UnderpostDB.API._manageGitRepo({ repoName, operation: 'pull' });
|
|
875
|
+
|
|
876
|
+
const nCommits = parseInt(options.macroRollbackExport);
|
|
877
|
+
const repoPath = `../${repoName}`;
|
|
878
|
+
const username = process.env.GITHUB_USERNAME;
|
|
879
|
+
|
|
880
|
+
if (fs.existsSync(repoPath) && username) {
|
|
881
|
+
logger.info('Executing macro rollback export', { repoName, nCommits });
|
|
882
|
+
shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
|
|
883
|
+
shellExec(`cd ${repoPath} && git reset`);
|
|
884
|
+
shellExec(`cd ${repoPath} && git checkout .`);
|
|
885
|
+
shellExec(`cd ${repoPath} && git clean -f -d`);
|
|
886
|
+
shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
|
|
89
887
|
} else {
|
|
90
|
-
|
|
91
|
-
|
|
888
|
+
if (!username) logger.error('GITHUB_USERNAME environment variable not set');
|
|
889
|
+
logger.warn('Repository not found for macro rollback', { repoPath });
|
|
92
890
|
}
|
|
93
891
|
}
|
|
94
892
|
|
|
893
|
+
// Process each database provider
|
|
95
894
|
for (const provider of Object.keys(dbs)) {
|
|
96
895
|
for (const dbName of Object.keys(dbs[provider])) {
|
|
97
896
|
const { hostFolder, user, password, host, path } = dbs[provider][dbName];
|
|
897
|
+
|
|
898
|
+
// Filter by hosts and paths if specified
|
|
98
899
|
if (
|
|
99
|
-
(options.hosts &&
|
|
100
|
-
|
|
101
|
-
|
|
900
|
+
(options.hosts &&
|
|
901
|
+
!options.hosts
|
|
902
|
+
.split(',')
|
|
903
|
+
.map((h) => h.trim())
|
|
904
|
+
.includes(host)) ||
|
|
905
|
+
(options.paths &&
|
|
906
|
+
!options.paths
|
|
907
|
+
.split(',')
|
|
908
|
+
.map((p) => p.trim())
|
|
909
|
+
.includes(path))
|
|
910
|
+
) {
|
|
911
|
+
logger.info('Skipping database due to host/path filter', { dbName, host, path });
|
|
102
912
|
continue;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (!hostFolder) {
|
|
916
|
+
logger.warn('No hostFolder defined for database', { dbName, provider });
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
logger.info('Processing database', { hostFolder, provider, dbName });
|
|
921
|
+
|
|
922
|
+
const backUpPath = `../${repoName}/${hostFolder}`;
|
|
923
|
+
const backupInfo = UnderpostDB.API._manageBackupTimestamps(
|
|
924
|
+
backUpPath,
|
|
925
|
+
newBackupTimestamp,
|
|
926
|
+
options.export === true,
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
dbs[provider][dbName].currentBackupTimestamp = backupInfo.current;
|
|
930
|
+
|
|
931
|
+
const currentTimestamp = backupInfo.current || newBackupTimestamp;
|
|
932
|
+
const sqlContainerPath = `/home/${dbName}.sql`;
|
|
933
|
+
const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
|
|
934
|
+
const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
|
|
935
|
+
const toNewSqlPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}.sql`;
|
|
936
|
+
const toBsonPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}`;
|
|
937
|
+
const toNewBsonPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}`;
|
|
938
|
+
|
|
939
|
+
// Merge split SQL files if needed for import
|
|
940
|
+
if (options.import === true && fs.existsSync(fromPartsPath) && !fs.existsSync(toSqlPath)) {
|
|
941
|
+
const names = JSON.parse(fs.readFileSync(fromPartsPath, 'utf8')).map((_path) => {
|
|
942
|
+
return `../${repoName}/${hostFolder}/${currentTimestamp}/${_path.split('/').pop()}`;
|
|
943
|
+
});
|
|
944
|
+
logger.info('Merging backup parts', { fromPartsPath, toSqlPath, parts: names.length });
|
|
945
|
+
await mergeFile(names, toSqlPath);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Get target pods based on provider and options
|
|
949
|
+
let targetPods = [];
|
|
950
|
+
const podCriteria = {
|
|
951
|
+
podNames: options.podName,
|
|
952
|
+
nodeNames: options.nodeName,
|
|
953
|
+
namespace,
|
|
954
|
+
labelSelector: options.labelSelector,
|
|
955
|
+
deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
targetPods = UnderpostDB.API._getFilteredPods(podCriteria);
|
|
959
|
+
|
|
960
|
+
// Fallback to default if no custom pods specified
|
|
961
|
+
if (targetPods.length === 0 && !options.podName && !options.nodeName) {
|
|
962
|
+
const defaultPods = UnderpostDeploy.API.get(
|
|
963
|
+
provider === 'mariadb' ? 'mariadb' : 'mongo',
|
|
964
|
+
'pods',
|
|
965
|
+
namespace,
|
|
966
|
+
);
|
|
967
|
+
console.log('defaultPods', defaultPods);
|
|
968
|
+
targetPods = defaultPods;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (targetPods.length === 0) {
|
|
972
|
+
logger.warn('No pods found matching criteria', { provider, criteria: podCriteria });
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Handle primary pod detection for MongoDB
|
|
977
|
+
let podsToProcess = [];
|
|
978
|
+
if (provider === 'mongoose' && !options.allPods) {
|
|
979
|
+
// For MongoDB, always use primary pod unless allPods is true
|
|
980
|
+
if (!targetPods || targetPods.length === 0) {
|
|
981
|
+
logger.warn('No MongoDB pods available to check for primary');
|
|
982
|
+
podsToProcess = [];
|
|
983
|
+
} else {
|
|
984
|
+
const firstPod = targetPods[0].NAME;
|
|
985
|
+
const primaryPodName = UnderpostDB.API.getMongoPrimaryPodName({ namespace, podName: firstPod });
|
|
132
986
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
987
|
+
if (primaryPodName) {
|
|
988
|
+
const primaryPod = targetPods.find((p) => p.NAME === primaryPodName);
|
|
989
|
+
if (primaryPod) {
|
|
990
|
+
podsToProcess = [primaryPod];
|
|
991
|
+
logger.info('Using MongoDB primary pod', { primaryPod: primaryPodName });
|
|
992
|
+
} else {
|
|
993
|
+
logger.warn('Primary pod not in filtered list, using first pod', { primaryPodName });
|
|
994
|
+
podsToProcess = [targetPods[0]];
|
|
995
|
+
}
|
|
996
|
+
} else {
|
|
997
|
+
logger.warn('Could not detect primary pod, using first pod');
|
|
998
|
+
podsToProcess = [targetPods[0]];
|
|
999
|
+
}
|
|
138
1000
|
}
|
|
1001
|
+
} else {
|
|
1002
|
+
// For MariaDB or when allPods is true, limit to first pod unless allPods is true
|
|
1003
|
+
podsToProcess = options.allPods === true ? targetPods : [targetPods[0]];
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
logger.info(`Processing ${podsToProcess.length} pod(s) for ${provider}`, {
|
|
1007
|
+
dbName,
|
|
1008
|
+
pods: podsToProcess.map((p) => p.NAME),
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// Process each pod
|
|
1012
|
+
for (const pod of podsToProcess) {
|
|
1013
|
+
logger.info('Processing pod', { podName: pod.NAME, node: pod.NODE, status: pod.STATUS });
|
|
139
1014
|
|
|
140
1015
|
switch (provider) {
|
|
141
1016
|
case 'mariadb': {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const cmd = `mariadb -u ${user} -p${password} ${dbName} < /${dbName}.sql`;
|
|
153
|
-
shellExec(
|
|
154
|
-
`kubectl exec -n ${nameSpace} -i ${podName} -- ${serviceName} -p${password} -e 'CREATE DATABASE ${dbName};'`,
|
|
155
|
-
);
|
|
156
|
-
shellExec(`sudo kubectl exec -n ${nameSpace} -i ${podName} -- sh -c "${cmd}"`);
|
|
157
|
-
}
|
|
158
|
-
if (options.export === true) {
|
|
159
|
-
shellExec(
|
|
160
|
-
`sudo kubectl exec -n ${nameSpace} -i ${podName} -- sh -c "rm -rf ${sqlContainerPath}"`,
|
|
161
|
-
);
|
|
162
|
-
const cmd = `mariadb-dump --user=${user} --password=${password} --lock-tables ${dbName} > ${sqlContainerPath}`;
|
|
163
|
-
shellExec(`sudo kubectl exec -n ${nameSpace} -i ${podName} -- sh -c "${cmd}"`);
|
|
164
|
-
shellExec(
|
|
165
|
-
`sudo kubectl cp ${nameSpace}/${podName}:${sqlContainerPath} ${
|
|
166
|
-
options.outPath ? options.outPath : _toNewSqlPath
|
|
167
|
-
}`,
|
|
168
|
-
);
|
|
169
|
-
await splitFileFactory(dbName, options.outPath ? options.outPath : _toNewSqlPath);
|
|
1017
|
+
if (options.stats === true) {
|
|
1018
|
+
const stats = UnderpostDB.API._getMariaDBStats({
|
|
1019
|
+
podName: pod.NAME,
|
|
1020
|
+
namespace,
|
|
1021
|
+
dbName,
|
|
1022
|
+
user,
|
|
1023
|
+
password,
|
|
1024
|
+
});
|
|
1025
|
+
if (stats) {
|
|
1026
|
+
UnderpostDB.API._displayStats({ provider, dbName, stats });
|
|
170
1027
|
}
|
|
171
1028
|
}
|
|
1029
|
+
|
|
1030
|
+
if (options.import === true) {
|
|
1031
|
+
UnderpostDB.API._importMariaDB({
|
|
1032
|
+
pod,
|
|
1033
|
+
namespace,
|
|
1034
|
+
dbName,
|
|
1035
|
+
user,
|
|
1036
|
+
password,
|
|
1037
|
+
sqlPath: toSqlPath,
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (options.export === true) {
|
|
1042
|
+
const outputPath = options.outPath || toNewSqlPath;
|
|
1043
|
+
await UnderpostDB.API._exportMariaDB({
|
|
1044
|
+
pod,
|
|
1045
|
+
namespace,
|
|
1046
|
+
dbName,
|
|
1047
|
+
user,
|
|
1048
|
+
password,
|
|
1049
|
+
outputPath,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
172
1052
|
break;
|
|
173
1053
|
}
|
|
174
1054
|
|
|
175
1055
|
case 'mongoose': {
|
|
176
|
-
if (options.
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
shellExec(`sudo kubectl exec -n ${nameSpace} -i ${podName} -- sh -c "rm -rf /${dbName}"`);
|
|
185
|
-
shellExec(
|
|
186
|
-
`sudo kubectl cp ${
|
|
187
|
-
options.outPath ? options.outPath : _toBsonPath
|
|
188
|
-
} ${nameSpace}/${podName}:/${dbName}`,
|
|
189
|
-
);
|
|
190
|
-
const cmd = `mongorestore -d ${dbName} /${dbName}${options.drop ? ' --drop' : ''}${
|
|
191
|
-
options.preserveUUID ? ' --preserveUUID' : ''
|
|
192
|
-
}`;
|
|
193
|
-
shellExec(`sudo kubectl exec -n ${nameSpace} -i ${podName} -- sh -c "${cmd}"`);
|
|
1056
|
+
if (options.stats === true) {
|
|
1057
|
+
const stats = UnderpostDB.API._getMongoStats({
|
|
1058
|
+
podName: pod.NAME,
|
|
1059
|
+
namespace,
|
|
1060
|
+
dbName,
|
|
1061
|
+
});
|
|
1062
|
+
if (stats) {
|
|
1063
|
+
UnderpostDB.API._displayStats({ provider, dbName, stats });
|
|
194
1064
|
}
|
|
195
1065
|
}
|
|
1066
|
+
|
|
1067
|
+
if (options.import === true) {
|
|
1068
|
+
const bsonPath = options.outPath || toBsonPath;
|
|
1069
|
+
UnderpostDB.API._importMongoDB({
|
|
1070
|
+
pod,
|
|
1071
|
+
namespace,
|
|
1072
|
+
dbName,
|
|
1073
|
+
bsonPath,
|
|
1074
|
+
drop: options.drop,
|
|
1075
|
+
preserveUUID: options.preserveUUID,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
196
1079
|
if (options.export === true) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
for (const collection of options.collections.split(','))
|
|
206
|
-
shellExec(
|
|
207
|
-
`sudo kubectl exec -n ${nameSpace} -i ${podName} -- sh -c "mongodump -d ${dbName} --collection ${collection} -o /"`,
|
|
208
|
-
);
|
|
209
|
-
else
|
|
210
|
-
shellExec(
|
|
211
|
-
`sudo kubectl exec -n ${nameSpace} -i ${podName} -- sh -c "mongodump -d ${dbName} -o /"`,
|
|
212
|
-
);
|
|
213
|
-
shellExec(
|
|
214
|
-
`sudo kubectl cp ${nameSpace}/${podName}:/${dbName} ${
|
|
215
|
-
options.outPath ? options.outPath : _toNewBsonPath
|
|
216
|
-
}`,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
1080
|
+
const outputPath = options.outPath || toNewBsonPath;
|
|
1081
|
+
UnderpostDB.API._exportMongoDB({
|
|
1082
|
+
pod,
|
|
1083
|
+
namespace,
|
|
1084
|
+
dbName,
|
|
1085
|
+
outputPath,
|
|
1086
|
+
collections: options.collections,
|
|
1087
|
+
});
|
|
219
1088
|
}
|
|
220
1089
|
break;
|
|
221
1090
|
}
|
|
222
1091
|
|
|
223
1092
|
default:
|
|
1093
|
+
logger.warn('Unsupported database provider', { provider });
|
|
224
1094
|
break;
|
|
225
1095
|
}
|
|
226
1096
|
}
|
|
227
1097
|
}
|
|
228
1098
|
}
|
|
1099
|
+
|
|
1100
|
+
// Commit and push to Git if enabled
|
|
229
1101
|
if (options.export === true && options.git === true) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
shellExec(`cd ../${repoName} && underpost push . ${process.env.GITHUB_USERNAME}/${repoName}`, {
|
|
237
|
-
disableLog: true,
|
|
238
|
-
});
|
|
1102
|
+
const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
|
|
1103
|
+
newBackupTimestamp,
|
|
1104
|
+
).toLocaleTimeString()}`;
|
|
1105
|
+
UnderpostDB.API._manageGitRepo({ repoName, operation: 'commit', message: commitMessage });
|
|
1106
|
+
UnderpostDB.API._manageGitRepo({ repoName, operation: 'push' });
|
|
239
1107
|
}
|
|
240
1108
|
}
|
|
1109
|
+
|
|
1110
|
+
logger.info('Database operation completed successfully');
|
|
241
1111
|
},
|
|
242
1112
|
|
|
243
1113
|
/**
|
|
1114
|
+
* Creates cluster metadata for the specified deployment
|
|
244
1115
|
* @method clusterMetadataFactory
|
|
245
|
-
* @description
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* @param {string} [
|
|
249
|
-
* @param {string} [
|
|
250
|
-
* @
|
|
1116
|
+
* @description Loads database configuration and initializes cluster metadata including
|
|
1117
|
+
* instances and cron jobs. This method populates the database with deployment information.
|
|
1118
|
+
* @param {string} [deployId=process.env.DEFAULT_DEPLOY_ID] - The deployment ID
|
|
1119
|
+
* @param {string} [host=process.env.DEFAULT_DEPLOY_HOST] - The host identifier
|
|
1120
|
+
* @param {string} [path=process.env.DEFAULT_DEPLOY_PATH] - The path identifier
|
|
1121
|
+
* @returns {Promise<void>}
|
|
251
1122
|
* @memberof UnderpostDB
|
|
1123
|
+
* @throws {Error} If database configuration is invalid or connection fails
|
|
252
1124
|
*/
|
|
253
1125
|
async clusterMetadataFactory(
|
|
254
1126
|
deployId = process.env.DEFAULT_DEPLOY_ID,
|
|
255
1127
|
host = process.env.DEFAULT_DEPLOY_HOST,
|
|
256
1128
|
path = process.env.DEFAULT_DEPLOY_PATH,
|
|
257
1129
|
) {
|
|
258
|
-
deployId = deployId
|
|
259
|
-
host = host
|
|
260
|
-
path = path
|
|
1130
|
+
deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
|
|
1131
|
+
host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
|
|
1132
|
+
path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
|
|
1133
|
+
|
|
1134
|
+
logger.info('Creating cluster metadata', { deployId, host, path });
|
|
1135
|
+
|
|
261
1136
|
const env = 'production';
|
|
262
|
-
const
|
|
1137
|
+
const deployListPath = './engine-private/deploy/dd.router';
|
|
1138
|
+
|
|
1139
|
+
if (!fs.existsSync(deployListPath)) {
|
|
1140
|
+
logger.error('Deploy router file not found', { path: deployListPath });
|
|
1141
|
+
throw new Error(`Deploy router file not found: ${deployListPath}`);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const deployList = fs.readFileSync(deployListPath, 'utf8').split(',');
|
|
1145
|
+
|
|
1146
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
1147
|
+
if (!fs.existsSync(confServerPath)) {
|
|
1148
|
+
logger.error('Server configuration not found', { path: confServerPath });
|
|
1149
|
+
throw new Error(`Server configuration not found: ${confServerPath}`);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const { db } = JSON.parse(fs.readFileSync(confServerPath, 'utf8'))[host][path];
|
|
263
1153
|
|
|
264
|
-
const { db } = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.json`, 'utf8'))[host][
|
|
265
|
-
path
|
|
266
|
-
];
|
|
267
1154
|
try {
|
|
268
1155
|
await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
|
|
269
1156
|
|
|
@@ -271,14 +1158,21 @@ class UnderpostDB {
|
|
|
271
1158
|
const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
|
|
272
1159
|
|
|
273
1160
|
await Instance.deleteMany();
|
|
1161
|
+
logger.info('Cleared existing instance metadata');
|
|
274
1162
|
|
|
275
1163
|
for (const _deployId of deployList) {
|
|
276
1164
|
const deployId = _deployId.trim();
|
|
277
1165
|
if (!deployId) continue;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
1166
|
+
|
|
1167
|
+
logger.info('Processing deployment for metadata', { deployId });
|
|
1168
|
+
|
|
1169
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
1170
|
+
if (!fs.existsSync(confServerPath)) {
|
|
1171
|
+
logger.warn('Configuration not found for deployment', { deployId, path: confServerPath });
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const confServer = loadReplicas(deployId, JSON.parse(fs.readFileSync(confServerPath, 'utf8')));
|
|
282
1176
|
const router = await UnderpostDeploy.API.routerFactory(deployId, env);
|
|
283
1177
|
const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
|
|
284
1178
|
|
|
@@ -287,6 +1181,8 @@ class UnderpostDB {
|
|
|
287
1181
|
if (!confServer[host][path]) continue;
|
|
288
1182
|
|
|
289
1183
|
const { client, runtime, apis, peer } = confServer[host][path];
|
|
1184
|
+
|
|
1185
|
+
// Save main instance
|
|
290
1186
|
{
|
|
291
1187
|
const body = {
|
|
292
1188
|
deployId,
|
|
@@ -298,10 +1194,11 @@ class UnderpostDB {
|
|
|
298
1194
|
apis,
|
|
299
1195
|
};
|
|
300
1196
|
|
|
301
|
-
logger.info('
|
|
1197
|
+
logger.info('Saving instance metadata', body);
|
|
302
1198
|
await new Instance(body).save();
|
|
303
1199
|
}
|
|
304
1200
|
|
|
1201
|
+
// Save peer instance if exists
|
|
305
1202
|
if (peer) {
|
|
306
1203
|
const body = {
|
|
307
1204
|
deployId,
|
|
@@ -311,15 +1208,16 @@ class UnderpostDB {
|
|
|
311
1208
|
runtime: 'nodejs',
|
|
312
1209
|
};
|
|
313
1210
|
|
|
314
|
-
logger.info('
|
|
1211
|
+
logger.info('Saving peer instance metadata', body);
|
|
315
1212
|
await new Instance(body).save();
|
|
316
1213
|
}
|
|
317
1214
|
}
|
|
318
1215
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
1216
|
+
|
|
1217
|
+
// Process additional instances
|
|
1218
|
+
const confInstancesPath = `./engine-private/conf/${deployId}/conf.instances.json`;
|
|
1219
|
+
if (fs.existsSync(confInstancesPath)) {
|
|
1220
|
+
const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
|
|
323
1221
|
for (const instance of confInstances) {
|
|
324
1222
|
const { id, host, path, fromPort, metadata } = instance;
|
|
325
1223
|
const { runtime } = metadata;
|
|
@@ -331,18 +1229,31 @@ class UnderpostDB {
|
|
|
331
1229
|
client: id,
|
|
332
1230
|
runtime,
|
|
333
1231
|
};
|
|
334
|
-
logger.info('
|
|
1232
|
+
logger.info('Saving additional instance metadata', body);
|
|
335
1233
|
await new Instance(body).save();
|
|
336
1234
|
}
|
|
337
1235
|
}
|
|
338
1236
|
}
|
|
339
1237
|
} catch (error) {
|
|
340
|
-
logger.error(error, error.stack);
|
|
1238
|
+
logger.error('Failed to create instance metadata', { error: error.message, stack: error.stack });
|
|
1239
|
+
throw error;
|
|
341
1240
|
}
|
|
342
1241
|
|
|
343
1242
|
try {
|
|
344
|
-
const
|
|
1243
|
+
const cronDeployPath = './engine-private/deploy/dd.cron';
|
|
1244
|
+
if (!fs.existsSync(cronDeployPath)) {
|
|
1245
|
+
logger.warn('Cron deploy file not found', { path: cronDeployPath });
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const cronDeployId = fs.readFileSync(cronDeployPath, 'utf8').trim();
|
|
345
1250
|
const confCronPath = `./engine-private/conf/${cronDeployId}/conf.cron.json`;
|
|
1251
|
+
|
|
1252
|
+
if (!fs.existsSync(confCronPath)) {
|
|
1253
|
+
logger.warn('Cron configuration not found', { path: confCronPath });
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
346
1257
|
const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
|
|
347
1258
|
|
|
348
1259
|
await DataBaseProvider.load({ apis: ['cron'], host, path, db });
|
|
@@ -351,6 +1262,7 @@ class UnderpostDB {
|
|
|
351
1262
|
const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
|
|
352
1263
|
|
|
353
1264
|
await Cron.deleteMany();
|
|
1265
|
+
logger.info('Cleared existing cron metadata');
|
|
354
1266
|
|
|
355
1267
|
for (const jobId of Object.keys(confCron.jobs)) {
|
|
356
1268
|
const body = {
|
|
@@ -359,33 +1271,36 @@ class UnderpostDB {
|
|
|
359
1271
|
expression: confCron.jobs[jobId].expression,
|
|
360
1272
|
enabled: confCron.jobs[jobId].enabled,
|
|
361
1273
|
};
|
|
362
|
-
logger.info('
|
|
1274
|
+
logger.info('Saving cron metadata', body);
|
|
363
1275
|
await new Cron(body).save();
|
|
364
1276
|
}
|
|
365
1277
|
} catch (error) {
|
|
366
|
-
logger.error(error, error.stack);
|
|
1278
|
+
logger.error('Failed to create cron metadata', { error: error.message, stack: error.stack });
|
|
367
1279
|
}
|
|
1280
|
+
|
|
368
1281
|
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
1282
|
+
logger.info('Cluster metadata creation completed');
|
|
369
1283
|
},
|
|
370
1284
|
|
|
371
1285
|
/**
|
|
1286
|
+
* Handles backup of cluster metadata
|
|
372
1287
|
* @method clusterMetadataBackupCallback
|
|
373
|
-
* @description
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* @param {string} [
|
|
377
|
-
* @param {string} [
|
|
378
|
-
* @param {
|
|
379
|
-
* @param {
|
|
380
|
-
* @param {boolean} [options.
|
|
381
|
-
* @param {boolean} [options.
|
|
382
|
-
* @param {boolean} [options.
|
|
383
|
-
* @param {boolean} [options.
|
|
384
|
-
* @param {boolean} [options.
|
|
385
|
-
* @
|
|
1288
|
+
* @description Orchestrates backup and restore operations for cluster metadata including
|
|
1289
|
+
* instances and cron jobs. Supports import/export and metadata generation.
|
|
1290
|
+
* @param {string} [deployId=process.env.DEFAULT_DEPLOY_ID] - The deployment ID
|
|
1291
|
+
* @param {string} [host=process.env.DEFAULT_DEPLOY_HOST] - The host identifier
|
|
1292
|
+
* @param {string} [path=process.env.DEFAULT_DEPLOY_PATH] - The path identifier
|
|
1293
|
+
* @param {Object} [options] - Backup operation options
|
|
1294
|
+
* @param {boolean} [options.generate=false] - Generate cluster metadata
|
|
1295
|
+
* @param {boolean} [options.itc=false] - Execute in container context
|
|
1296
|
+
* @param {boolean} [options.import=false] - Import metadata from backup
|
|
1297
|
+
* @param {boolean} [options.export=false] - Export metadata to backup
|
|
1298
|
+
* @param {boolean} [options.instances=false] - Process instances collection
|
|
1299
|
+
* @param {boolean} [options.crons=false] - Process crons collection
|
|
1300
|
+
* @returns {void}
|
|
386
1301
|
* @memberof UnderpostDB
|
|
387
1302
|
*/
|
|
388
|
-
clusterMetadataBackupCallback(
|
|
1303
|
+
async clusterMetadataBackupCallback(
|
|
389
1304
|
deployId = process.env.DEFAULT_DEPLOY_ID,
|
|
390
1305
|
host = process.env.DEFAULT_DEPLOY_HOST,
|
|
391
1306
|
path = process.env.DEFAULT_DEPLOY_PATH,
|
|
@@ -398,40 +1313,67 @@ class UnderpostDB {
|
|
|
398
1313
|
crons: false,
|
|
399
1314
|
},
|
|
400
1315
|
) {
|
|
401
|
-
deployId = deployId
|
|
402
|
-
host = host
|
|
403
|
-
path = path
|
|
1316
|
+
deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
|
|
1317
|
+
host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
|
|
1318
|
+
path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
|
|
1319
|
+
|
|
1320
|
+
logger.info('Starting cluster metadata backup operation', {
|
|
1321
|
+
deployId,
|
|
1322
|
+
host,
|
|
1323
|
+
path,
|
|
1324
|
+
options,
|
|
1325
|
+
});
|
|
404
1326
|
|
|
405
1327
|
if (options.generate === true) {
|
|
406
|
-
|
|
1328
|
+
logger.info('Generating cluster metadata');
|
|
1329
|
+
await UnderpostDB.API.clusterMetadataFactory(deployId, host, path);
|
|
407
1330
|
}
|
|
408
1331
|
|
|
409
1332
|
if (options.instances === true) {
|
|
410
1333
|
const outputPath = './engine-private/instances';
|
|
411
|
-
if (fs.existsSync(outputPath))
|
|
1334
|
+
if (!fs.existsSync(outputPath)) {
|
|
1335
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
1336
|
+
}
|
|
412
1337
|
const collection = 'instances';
|
|
413
|
-
|
|
1338
|
+
|
|
1339
|
+
if (options.export === true) {
|
|
1340
|
+
logger.info('Exporting instances collection', { outputPath });
|
|
414
1341
|
shellExec(
|
|
415
|
-
`node bin db --export --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1342
|
+
`node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
416
1343
|
);
|
|
417
|
-
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (options.import === true) {
|
|
1347
|
+
logger.info('Importing instances collection', { outputPath });
|
|
418
1348
|
shellExec(
|
|
419
|
-
`node bin db --import --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1349
|
+
`node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
420
1350
|
);
|
|
1351
|
+
}
|
|
421
1352
|
}
|
|
1353
|
+
|
|
422
1354
|
if (options.crons === true) {
|
|
423
1355
|
const outputPath = './engine-private/crons';
|
|
424
|
-
if (fs.existsSync(outputPath))
|
|
1356
|
+
if (!fs.existsSync(outputPath)) {
|
|
1357
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
1358
|
+
}
|
|
425
1359
|
const collection = 'crons';
|
|
426
|
-
|
|
1360
|
+
|
|
1361
|
+
if (options.export === true) {
|
|
1362
|
+
logger.info('Exporting crons collection', { outputPath });
|
|
427
1363
|
shellExec(
|
|
428
|
-
`node bin db --export --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1364
|
+
`node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
429
1365
|
);
|
|
430
|
-
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (options.import === true) {
|
|
1369
|
+
logger.info('Importing crons collection', { outputPath });
|
|
431
1370
|
shellExec(
|
|
432
|
-
`node bin db --import --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1371
|
+
`node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
433
1372
|
);
|
|
1373
|
+
}
|
|
434
1374
|
}
|
|
1375
|
+
|
|
1376
|
+
logger.info('Cluster metadata backup operation completed');
|
|
435
1377
|
},
|
|
436
1378
|
};
|
|
437
1379
|
}
|